211 lines
8.8 KiB
JavaScript
211 lines
8.8 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import UserNav from '../components/UserNav'
|
|
import { changePasswordApi, meApi, forgotPasswordApi } from '../services/auth'
|
|
import { useHandleAuthError } from '../hooks/useHandleAuthError'
|
|
|
|
export default function Profile() {
|
|
const user = JSON.parse(localStorage.getItem('user') || '{}')
|
|
const initials = `${(user.firstname || '?')[0]}${(user.lastname || '?')[0]}`.toUpperCase()
|
|
const handleAuthError = useHandleAuthError()
|
|
|
|
const [pwForm, setPwForm] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' })
|
|
const [showPw, setShowPw] = useState({ current: false, new: false, confirm: false })
|
|
const [pwLoading, setPwLoading] = useState(false)
|
|
const [pwError, setPwError] = useState('')
|
|
const [pwSuccess, setPwSuccess] = useState(false)
|
|
|
|
// Un compte créé via Google peut ne pas avoir de mot de passe : le
|
|
// formulaire de modification n'aurait alors aucun sens. L'information vient
|
|
// de l'API, car le profil stocké en localStorage ne la contient pas.
|
|
const [hasPassword, setHasPassword] = useState(null)
|
|
const [linkSending, setLinkSending] = useState(false)
|
|
const [linkSent, setLinkSent] = useState(false)
|
|
const [linkError, setLinkError] = useState('')
|
|
|
|
useEffect(() => {
|
|
meApi()
|
|
.then((data) => setHasPassword(data.user.hasPassword))
|
|
.catch((e) => {
|
|
if (!handleAuthError(e)) setHasPassword(true)
|
|
})
|
|
}, [handleAuthError])
|
|
|
|
const handleSendSetupLink = async () => {
|
|
setLinkError('')
|
|
setLinkSending(true)
|
|
try {
|
|
await forgotPasswordApi(user.email)
|
|
setLinkSent(true)
|
|
} catch (e) {
|
|
if (!handleAuthError(e)) setLinkError(e.message)
|
|
} finally {
|
|
setLinkSending(false)
|
|
}
|
|
}
|
|
|
|
const handlePwChange = (e) => setPwForm({ ...pwForm, [e.target.name]: e.target.value })
|
|
|
|
const handlePwSubmit = async (e) => {
|
|
e.preventDefault()
|
|
setPwError('')
|
|
setPwSuccess(false)
|
|
|
|
if (pwForm.newPassword !== pwForm.confirmPassword) {
|
|
setPwError('Les nouveaux mots de passe ne correspondent pas.')
|
|
return
|
|
}
|
|
if (pwForm.newPassword.length < 8) {
|
|
setPwError('Le nouveau mot de passe doit faire au moins 8 caractères.')
|
|
return
|
|
}
|
|
|
|
setPwLoading(true)
|
|
try {
|
|
await changePasswordApi({ currentPassword: pwForm.currentPassword, newPassword: pwForm.newPassword })
|
|
setPwSuccess(true)
|
|
setPwForm({ currentPassword: '', newPassword: '', confirmPassword: '' })
|
|
} catch (err) {
|
|
if (!handleAuthError(err)) setPwError(err.message)
|
|
} finally {
|
|
setPwLoading(false)
|
|
}
|
|
}
|
|
|
|
const EyeIcon = ({ visible }) => (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth={1.5} viewBox="0 0 24 24">
|
|
{visible
|
|
? <><path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" /><path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></>
|
|
: <path strokeLinecap="round" strokeLinejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
|
}
|
|
</svg>
|
|
)
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 flex flex-col px-8 py-12 max-w-lg mx-auto">
|
|
<UserNav active="profile" />
|
|
|
|
{/* Avatar */}
|
|
<div className="mb-6">
|
|
<div className="w-14 h-14 rounded-full bg-[#006B6B] flex items-center justify-center text-white text-lg font-bold">
|
|
{initials}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Infos du compte (lecture seule) */}
|
|
<div className="space-y-4 mb-8">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Adresse email</label>
|
|
<div className="w-full border border-gray-100 rounded-lg px-4 py-3 text-sm text-gray-500 bg-gray-50 cursor-not-allowed select-none">
|
|
{user.email || '—'}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Nom</label>
|
|
<div className="w-full border border-gray-100 rounded-lg px-4 py-3 text-sm text-gray-500 bg-gray-50">
|
|
{user.lastname || '—'}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wider mb-1">Prénom</label>
|
|
<div className="w-full border border-gray-100 rounded-lg px-4 py-3 text-sm text-gray-500 bg-gray-50">
|
|
{user.firstname || '—'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Séparateur */}
|
|
<div className="border-t border-gray-100 mb-6" />
|
|
|
|
{/* Section changement de mot de passe */}
|
|
<div>
|
|
<h2 className="text-sm font-semibold text-gray-800 mb-4">
|
|
{hasPassword === false ? 'Définir un mot de passe' : 'Changer mon mot de passe'}
|
|
</h2>
|
|
|
|
{hasPassword === false && (
|
|
<div className="px-4 py-4 rounded-lg bg-gray-50 border border-gray-200">
|
|
<p className="text-sm text-gray-600">
|
|
Votre compte a été créé avec Google et n'a pas encore de mot de passe.
|
|
Définissez-en un pour pouvoir aussi vous connecter avec votre email.
|
|
</p>
|
|
|
|
{linkSent ? (
|
|
<p className="mt-3 text-sm text-green-700">
|
|
Lien envoyé à {user.email}. Consultez votre boîte mail.
|
|
</p>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={handleSendSetupLink}
|
|
disabled={linkSending}
|
|
className="mt-3 bg-[#006B6B] hover:bg-[#005959] disabled:opacity-50 text-white font-medium px-4 py-2.5 rounded-lg transition-colors text-sm"
|
|
>
|
|
{linkSending ? 'Envoi…' : 'Recevoir un lien par email'}
|
|
</button>
|
|
)}
|
|
|
|
{linkError && <p className="mt-2 text-sm text-red-600">{linkError}</p>}
|
|
</div>
|
|
)}
|
|
|
|
{hasPassword !== false && pwSuccess && (
|
|
<div className="mb-4 px-4 py-3 rounded-lg bg-green-50 border border-green-200 text-green-700 text-sm flex items-center gap-2">
|
|
<svg className="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" strokeWidth={2.5} viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
Mot de passe modifié avec succès.
|
|
</div>
|
|
)}
|
|
|
|
{hasPassword !== false && pwError && (
|
|
<div className="mb-4 px-4 py-3 rounded-lg bg-red-50 border border-red-200 text-red-600 text-sm">
|
|
{pwError}
|
|
</div>
|
|
)}
|
|
|
|
{hasPassword !== false && (
|
|
<form onSubmit={handlePwSubmit} className="space-y-4">
|
|
{[
|
|
{ name: 'currentPassword', label: 'Mot de passe actuel', key: 'current' },
|
|
{ name: 'newPassword', label: 'Nouveau mot de passe', key: 'new' },
|
|
{ name: 'confirmPassword', label: 'Confirmer le nouveau mot de passe', key: 'confirm' },
|
|
].map(({ name, label, key }) => (
|
|
<div key={name}>
|
|
<label htmlFor={name} className="block text-sm font-medium text-gray-800 mb-1">{label}</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showPw[key] ? 'text' : 'password'}
|
|
id={name}
|
|
name={name}
|
|
value={pwForm[name]}
|
|
onChange={handlePwChange}
|
|
required
|
|
placeholder="••••••••"
|
|
className="w-full border border-gray-200 rounded-lg px-4 py-3 pr-11 text-sm text-gray-700 placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPw((s) => ({ ...s, [key]: !s[key] }))}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-600"
|
|
aria-label="Afficher/masquer"
|
|
>
|
|
<EyeIcon visible={showPw[key]} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={pwLoading}
|
|
className="w-full bg-[#006B6B] hover:bg-[#005959] disabled:opacity-50 text-white font-medium py-3 rounded-lg transition-colors text-sm"
|
|
>
|
|
{pwLoading ? 'Modification…' : 'Modifier le mot de passe'}
|
|
</button>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|