veza/apps/web/src/features/auth/pages/RegisterPage.tsx

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 é 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;