343 lines
12 KiB
TypeScript
343 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Navigate, Link } from 'react-router-dom';
|
|
import { useAuthStore } from '../store/authStore';
|
|
import { AuthLayout } from '../components/AuthLayout';
|
|
import { AuthInput } from '../components/AuthInput';
|
|
import { AuthButton } from '../components/AuthButton';
|
|
import { PasswordStrengthIndicator } from '../components/PasswordStrengthIndicator';
|
|
import { useRegister } from '../hooks/useRegister';
|
|
import { useUsernameAvailability } from '../hooks/useUsernameAvailability';
|
|
import { resendVerificationEmail } from '../services/authService';
|
|
import type { RegisterFormData } from '../types';
|
|
|
|
export function RegisterPage() {
|
|
const { isAuthenticated } = useAuthStore();
|
|
const {
|
|
mutate: handleRegister,
|
|
isPending: loading,
|
|
error,
|
|
isSuccess: success,
|
|
} = useRegister();
|
|
const [formData, setFormData] = useState<RegisterFormData>({
|
|
email: '',
|
|
password: '',
|
|
password_confirm: '',
|
|
username: '',
|
|
});
|
|
const { available: usernameAvailable, checking: checkingUsername } =
|
|
useUsernameAvailability(formData.username);
|
|
const [errors, setErrors] = useState<
|
|
Partial<Record<keyof RegisterFormData | 'terms', string>>
|
|
>({});
|
|
const [acceptedTerms, setAcceptedTerms] = useState(false);
|
|
const [showVerificationNotice, setShowVerificationNotice] = useState(false);
|
|
const [resendLoading, setResendLoading] = useState(false);
|
|
const [resendSuccess, setResendSuccess] = useState(false);
|
|
|
|
// Détecter quand l'inscription réussit
|
|
useEffect(() => {
|
|
if (success) {
|
|
setShowVerificationNotice(true);
|
|
}
|
|
}, [success]);
|
|
|
|
// Rediriger si déjà connecté
|
|
if (isAuthenticated) {
|
|
return <Navigate to="/dashboard" replace />;
|
|
}
|
|
|
|
const validate = (): boolean => {
|
|
const newErrors: Partial<Record<keyof RegisterFormData | 'terms', string>> =
|
|
{};
|
|
|
|
if (!formData.email) {
|
|
newErrors.email = 'Email requis';
|
|
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
|
newErrors.email = 'Email invalide';
|
|
}
|
|
|
|
if (!formData.username) {
|
|
newErrors.username = "Nom d'utilisateur requis";
|
|
} else if (formData.username.length < 3) {
|
|
newErrors.username =
|
|
"Le nom d'utilisateur doit contenir au moins 3 caractères";
|
|
} else if (usernameAvailable === false) {
|
|
newErrors.username = "Ce nom d'utilisateur est déjà pris";
|
|
}
|
|
|
|
if (!formData.password) {
|
|
newErrors.password = 'Mot de passe requis';
|
|
} else if (formData.password.length < 12) {
|
|
newErrors.password =
|
|
'Le mot de passe doit contenir au moins 12 caractères';
|
|
}
|
|
|
|
if (formData.password !== formData.password_confirm) {
|
|
newErrors.password_confirm = 'Les mots de passe ne correspondent pas';
|
|
}
|
|
|
|
if (!acceptedTerms) {
|
|
newErrors.terms =
|
|
"Vous devez accepter les conditions d'utilisation et la politique de confidentialité";
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const handleChange = (field: keyof RegisterFormData, value: string) => {
|
|
setFormData({ ...formData, [field]: value });
|
|
// Clear error for this field when user starts typing
|
|
if (errors[field]) {
|
|
setErrors({ ...errors, [field]: undefined });
|
|
}
|
|
};
|
|
|
|
const handleBlur = (field: keyof RegisterFormData) => {
|
|
const value = formData[field];
|
|
let error: string | undefined;
|
|
|
|
switch (field) {
|
|
case 'email':
|
|
if (!value) {
|
|
error = 'Email requis';
|
|
} else if (!/\S+@\S+\.\S+/.test(value)) {
|
|
error = 'Email invalide';
|
|
}
|
|
break;
|
|
case 'username':
|
|
if (!value) {
|
|
error = "Nom d'utilisateur requis";
|
|
} else if (value.length < 3) {
|
|
error = "Le nom d'utilisateur doit contenir au moins 3 caractères";
|
|
}
|
|
break;
|
|
case 'password':
|
|
if (!value) {
|
|
error = 'Mot de passe requis';
|
|
} else if (value.length < 12) {
|
|
error = 'Le mot de passe doit contenir au moins 12 caractères';
|
|
}
|
|
break;
|
|
case 'password_confirm':
|
|
if (formData.password !== value) {
|
|
error = 'Les mots de passe ne correspondent pas';
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (error) {
|
|
setErrors({ ...errors, [field]: error });
|
|
} else {
|
|
setErrors({ ...errors, [field]: undefined });
|
|
}
|
|
};
|
|
|
|
const onSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (validate()) {
|
|
await handleRegister(formData);
|
|
}
|
|
};
|
|
|
|
const handleResendVerificationEmail = async () => {
|
|
try {
|
|
setResendLoading(true);
|
|
setResendSuccess(false);
|
|
await resendVerificationEmail(formData.email);
|
|
setResendSuccess(true);
|
|
} catch (err) {
|
|
// Gérer l'erreur silencieusement ou afficher un message
|
|
console.error("Erreur lors du renvoi de l'email:", err);
|
|
} finally {
|
|
setResendLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AuthLayout
|
|
title="Inscription"
|
|
subtitle="Créez votre compte"
|
|
footerLinks={[{ label: 'Déjà un compte ? Se connecter', to: '/login' }]}
|
|
>
|
|
{showVerificationNotice ? (
|
|
<div className="text-center space-y-4" role="status" aria-live="polite">
|
|
<div
|
|
className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded"
|
|
role="alert"
|
|
aria-live="assertive"
|
|
>
|
|
<p className="font-medium">Inscription réussie !</p>
|
|
<p className="text-sm mt-1">
|
|
Un email de vérification a été envoyé à {formData.email}
|
|
</p>
|
|
</div>
|
|
<p className="text-sm text-gray-600">
|
|
Veuillez vérifier votre boîte mail et cliquer sur le lien de
|
|
vérification.
|
|
</p>
|
|
{resendSuccess && (
|
|
<p
|
|
className="text-sm text-green-600"
|
|
role="status"
|
|
aria-live="polite"
|
|
>
|
|
Email de vérification renvoyé avec succès !
|
|
</p>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={handleResendVerificationEmail}
|
|
disabled={resendLoading}
|
|
className="text-blue-600 hover:underline text-sm disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"
|
|
aria-label="Renvoyer l'email de vérification"
|
|
aria-busy={resendLoading}
|
|
>
|
|
{resendLoading ? (
|
|
<>
|
|
<span className="sr-only">Envoi en cours</span>
|
|
<span aria-hidden="true">Envoi en cours...</span>
|
|
</>
|
|
) : (
|
|
"Renvoyer l'email de vérification"
|
|
)}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<form
|
|
onSubmit={onSubmit}
|
|
className="space-y-4"
|
|
aria-label="Formulaire d'inscription"
|
|
>
|
|
{error && (
|
|
<div
|
|
className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded"
|
|
role="alert"
|
|
aria-live="assertive"
|
|
>
|
|
{error.message}
|
|
</div>
|
|
)}
|
|
<div>
|
|
<AuthInput
|
|
type="text"
|
|
label="Nom d'utilisateur"
|
|
value={formData.username}
|
|
onChange={(e) => handleChange('username', e.target.value)}
|
|
onBlur={() => handleBlur('username')}
|
|
error={errors.username}
|
|
required
|
|
autoComplete="username"
|
|
/>
|
|
{formData.username.length >= 3 && (
|
|
<div className="mt-1" aria-live="polite" aria-atomic="true">
|
|
{checkingUsername ? (
|
|
<p className="text-xs text-gray-500" role="status">
|
|
<span className="sr-only">Vérification en cours</span>
|
|
<span aria-hidden="true">Vérification en cours...</span>
|
|
</p>
|
|
) : usernameAvailable === true ? (
|
|
<p className="text-xs text-green-600" role="status">
|
|
<span className="sr-only">Disponible:</span>
|
|
<span aria-hidden="true">✓</span> Ce nom d'utilisateur est
|
|
disponible
|
|
</p>
|
|
) : usernameAvailable === false ? (
|
|
<p className="text-xs text-red-600" role="alert">
|
|
<span className="sr-only">Indisponible:</span>
|
|
<span aria-hidden="true">✗</span> Ce nom d'utilisateur est
|
|
déjà pris
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<AuthInput
|
|
type="email"
|
|
label="Email"
|
|
value={formData.email}
|
|
onChange={(e) => handleChange('email', e.target.value)}
|
|
onBlur={() => handleBlur('email')}
|
|
error={errors.email}
|
|
required
|
|
autoComplete="email"
|
|
/>
|
|
<div>
|
|
<AuthInput
|
|
type="password"
|
|
label="Mot de passe"
|
|
value={formData.password}
|
|
onChange={(e) => handleChange('password', e.target.value)}
|
|
onBlur={() => handleBlur('password')}
|
|
error={errors.password}
|
|
required
|
|
autoComplete="new-password"
|
|
/>
|
|
<PasswordStrengthIndicator password={formData.password} />
|
|
</div>
|
|
<AuthInput
|
|
type="password"
|
|
label="Confirmer le mot de passe"
|
|
value={formData.password_confirm}
|
|
onChange={(e) => handleChange('password_confirm', e.target.value)}
|
|
onBlur={() => handleBlur('password_confirm')}
|
|
error={errors.password_confirm}
|
|
required
|
|
autoComplete="new-password"
|
|
/>
|
|
<div className="flex items-start">
|
|
<input
|
|
type="checkbox"
|
|
id="terms"
|
|
checked={acceptedTerms}
|
|
onChange={(e) => {
|
|
setAcceptedTerms(e.target.checked);
|
|
if (errors.terms) {
|
|
setErrors({ ...errors, terms: undefined });
|
|
}
|
|
}}
|
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mt-1"
|
|
aria-invalid={errors.terms ? 'true' : 'false'}
|
|
aria-describedby={
|
|
errors.terms ? 'terms-error' : 'terms-description'
|
|
}
|
|
required
|
|
/>
|
|
<label htmlFor="terms" className="ml-2 block text-sm text-gray-900">
|
|
J'accepte les{' '}
|
|
<Link
|
|
to="/terms"
|
|
className="text-blue-600 hover:underline focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"
|
|
aria-label="Lire les conditions d'utilisation"
|
|
>
|
|
conditions d'utilisation
|
|
</Link>{' '}
|
|
et la{' '}
|
|
<Link
|
|
to="/privacy"
|
|
className="text-blue-600 hover:underline focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"
|
|
aria-label="Lire la politique de confidentialité"
|
|
>
|
|
politique de confidentialité
|
|
</Link>
|
|
</label>
|
|
</div>
|
|
<p id="terms-description" className="sr-only">
|
|
Vous devez accepter les conditions d'utilisation et la politique de
|
|
confidentialité pour créer un compte
|
|
</p>
|
|
{errors.terms && (
|
|
<p id="terms-error" className="text-sm text-red-600" role="alert">
|
|
{errors.terms}
|
|
</p>
|
|
)}
|
|
<AuthButton type="submit" loading={loading}>
|
|
S'inscrire
|
|
</AuthButton>
|
|
</form>
|
|
)}
|
|
</AuthLayout>
|
|
);
|
|
}
|
|
|
|
export default RegisterPage;
|