refactor(auth): extract RegisterPage into register-page module

- Add register-page/ with useRegisterPage, RegisterPageForm,
  RegisterPageVerificationNotice, RegisterPageSkeleton
- Layout primitives (min-h-layout-page-sm), tokens (success, destructive)
- Stories: Default, Loading, WithError; re-export from pages/RegisterPage

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
senke 2026-02-05 21:50:17 +01:00
parent 2701ce18d2
commit f622fad9fc
9 changed files with 537 additions and 478 deletions

View file

@ -0,0 +1,49 @@
import type { Meta, StoryObj } from '@storybook/react';
import { RegisterPage } from './RegisterPage';
import { RegisterPageSkeleton } from './RegisterPageSkeleton';
const meta: Meta<typeof RegisterPage> = {
title: 'App/Pages/Auth/RegisterPage',
component: RegisterPage,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: "Page d'inscription avec validation complète et vérification email.",
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="min-h-layout-page-sm">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/** Formulaire vide prêt à être rempli. */
export const Default: Story = { name: 'Par défaut' };
/** Skeleton pendant chargement (layout aligné). */
export const Loading: Story = {
name: 'Loading',
render: () => <RegisterPageSkeleton />,
};
/** Erreur soumission (simulée via MSW si besoin). */
export const WithError: Story = {
name: 'Avec erreur',
parameters: {
docs: {
description: {
story: "Démontre l'affichage d'erreurs de validation en temps réel.",
},
},
},
};

View file

@ -0,0 +1,65 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuthStore } from '../../store/authStore';
import { AuthLayout } from '../AuthLayout';
import { useRegisterPage } from './useRegisterPage';
import { RegisterPageForm } from './RegisterPageForm';
import { RegisterPageVerificationNotice } from './RegisterPageVerificationNotice';
export function RegisterPage() {
const { isAuthenticated } = useAuthStore();
const {
formData,
errors,
acceptedTerms,
setAcceptedTerms,
setErrors,
showVerificationNotice,
usernameAvailable,
checkingUsername,
loading,
error,
resendLoading,
resendSuccess,
handleChange,
handleBlur,
onSubmit,
handleResendVerificationEmail,
} = useRegisterPage();
if (isAuthenticated) {
return <Navigate to="/dashboard" replace />;
}
return (
<AuthLayout
title="Inscription"
subtitle="Créez votre compte"
footerLinks={[{ label: 'Déjà un compte ? Se connecter', to: '/login' }]}
>
{showVerificationNotice ? (
<RegisterPageVerificationNotice
email={formData.email}
resendLoading={resendLoading}
resendSuccess={resendSuccess}
onResend={handleResendVerificationEmail}
/>
) : (
<RegisterPageForm
formData={formData}
errors={errors}
acceptedTerms={acceptedTerms}
onAcceptedTermsChange={setAcceptedTerms}
onErrorsChange={setErrors}
loading={loading}
error={error instanceof Error ? error : null}
usernameAvailable={usernameAvailable}
checkingUsername={checkingUsername}
onFieldChange={handleChange}
onFieldBlur={handleBlur}
onSubmit={onSubmit}
/>
)}
</AuthLayout>
);
}

View file

@ -0,0 +1,199 @@
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Loader2 } from 'lucide-react';
import { PasswordStrengthIndicator } from '../PasswordStrengthIndicator';
import type { RegisterFormData } from '../../types';
import type { FormErrors } from './useRegisterPage';
interface RegisterPageFormProps {
formData: RegisterFormData;
errors: FormErrors;
acceptedTerms: boolean;
onAcceptedTermsChange: (checked: boolean) => void;
onErrorsChange: (updater: (prev: FormErrors) => FormErrors) => void;
loading: boolean;
error: Error | null;
usernameAvailable: boolean | null;
checkingUsername: boolean;
onFieldChange: (field: keyof RegisterFormData, value: string) => void;
onFieldBlur: (field: keyof RegisterFormData) => void;
onSubmit: (e: React.FormEvent) => void;
}
export function RegisterPageForm({
formData,
errors,
acceptedTerms,
onAcceptedTermsChange,
onErrorsChange,
loading,
error,
usernameAvailable,
checkingUsername,
onFieldChange,
onFieldBlur,
onSubmit,
}: RegisterPageFormProps) {
return (
<form
onSubmit={onSubmit}
className="space-y-4"
aria-label="Formulaire d'inscription"
>
{error && (
<div
className="bg-destructive/10 border border-destructive text-destructive px-4 py-4 rounded-lg"
role="alert"
aria-live="assertive"
>
{error.message}
</div>
)}
<div className="space-y-2">
<Label htmlFor="register-username">Nom d'utilisateur *</Label>
<Input
id="register-username"
type="text"
value={formData.username}
onChange={(e) => onFieldChange('username', e.target.value)}
onBlur={() => onFieldBlur('username')}
required
autoComplete="username"
aria-invalid={errors.username ? 'true' : 'false'}
/>
{errors.username && (
<p className="mt-1 text-sm text-destructive" role="alert">
{errors.username}
</p>
)}
{formData.username.length >= 3 && (
<div className="mt-1" aria-live="polite" aria-atomic="true">
{checkingUsername ? (
<p className="text-xs text-muted-foreground" role="status">
<span className="sr-only">Vérification en cours</span>
<span aria-hidden>Vérification en cours...</span>
</p>
) : usernameAvailable === true ? (
<p className="text-xs text-success" role="status">
<span className="sr-only">Disponible:</span>
<span aria-hidden></span> Ce nom d'utilisateur est disponible
</p>
) : usernameAvailable === false ? (
<p className="text-xs text-destructive" role="alert">
<span className="sr-only">Indisponible:</span>
<span aria-hidden></span> Ce nom d'utilisateur est déjà pris
</p>
) : null}
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="register-email">Email *</Label>
<Input
id="register-email"
type="email"
value={formData.email}
onChange={(e) => onFieldChange('email', e.target.value)}
onBlur={() => onFieldBlur('email')}
required
autoComplete="email"
aria-invalid={errors.email ? 'true' : 'false'}
/>
{errors.email && (
<p className="mt-1 text-sm text-destructive" role="alert">
{errors.email}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="register-password">Mot de passe *</Label>
<Input
id="register-password"
type="password"
value={formData.password}
onChange={(e) => onFieldChange('password', e.target.value)}
onBlur={() => onFieldBlur('password')}
required
autoComplete="new-password"
aria-invalid={errors.password ? 'true' : 'false'}
/>
{errors.password && (
<p className="mt-1 text-sm text-destructive" role="alert">
{errors.password}
</p>
)}
<PasswordStrengthIndicator password={formData.password} />
</div>
<div className="space-y-2">
<Label htmlFor="register-password_confirm">Confirmer le mot de passe *</Label>
<Input
id="register-password_confirm"
type="password"
value={formData.password_confirm}
onChange={(e) => onFieldChange('password_confirm', e.target.value)}
onBlur={() => onFieldBlur('password_confirm')}
required
autoComplete="new-password"
aria-invalid={errors.password_confirm ? 'true' : 'false'}
/>
{errors.password_confirm && (
<p className="mt-1 text-sm text-destructive" role="alert">
{errors.password_confirm}
</p>
)}
</div>
<div className="flex items-start">
<Checkbox
id="register-terms"
checked={acceptedTerms}
onCheckedChange={(checked) => {
onAcceptedTermsChange(checked as boolean);
if (errors.terms) onErrorsChange((prev) => ({ ...prev, terms: undefined }));
}}
required
aria-invalid={errors.terms ? 'true' : 'false'}
aria-describedby={errors.terms ? 'terms-error' : 'terms-description'}
/>
<label htmlFor="register-terms" className="ml-2 block text-sm text-foreground">
J'accepte les{' '}
<Link
to="/terms"
className="text-primary hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded"
aria-label="Lire les conditions d'utilisation"
>
conditions d'utilisation
</Link>{' '}
et la{' '}
<Link
to="/privacy"
className="text-primary hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-primary 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-destructive" role="alert">
{errors.terms}
</p>
)}
<Button type="submit" disabled={loading} className="w-full">
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Inscription en cours...
</>
) : (
"S'inscrire"
)}
</Button>
</form>
);
}

View file

@ -0,0 +1,30 @@
/**
* Skeleton aligned with RegisterPage form layout (AuthLayout + form fields).
*/
export function RegisterPageSkeleton() {
return (
<div className="space-y-4 min-h-layout-page-sm flex flex-col justify-center">
<div className="h-8 w-48 rounded bg-muted animate-pulse mx-auto" />
<div className="h-4 w-64 rounded bg-muted animate-pulse mx-auto" />
<div className="space-y-4 pt-4">
<div className="space-y-2">
<div className="h-4 w-24 rounded bg-muted animate-pulse" />
<div className="h-11 w-full rounded-lg bg-muted animate-pulse" />
</div>
<div className="space-y-2">
<div className="h-4 w-16 rounded bg-muted animate-pulse" />
<div className="h-11 w-full rounded-lg bg-muted animate-pulse" />
</div>
<div className="space-y-2">
<div className="h-4 w-28 rounded bg-muted animate-pulse" />
<div className="h-11 w-full rounded-lg bg-muted animate-pulse" />
</div>
<div className="space-y-2">
<div className="h-4 w-40 rounded bg-muted animate-pulse" />
<div className="h-11 w-full rounded-lg bg-muted animate-pulse" />
</div>
<div className="h-10 w-full rounded-lg bg-muted animate-pulse" />
</div>
</div>
);
}

View file

@ -0,0 +1,56 @@
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';
interface RegisterPageVerificationNoticeProps {
email: string;
resendLoading: boolean;
resendSuccess: boolean;
onResend: () => void;
}
export function RegisterPageVerificationNotice({
email,
resendLoading,
resendSuccess,
onResend,
}: RegisterPageVerificationNoticeProps) {
return (
<div className="text-center space-y-4" role="status" aria-live="polite">
<div
className="bg-success/10 border border-success text-success px-4 py-4 rounded-lg"
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é à {email}
</p>
</div>
<p className="text-sm text-muted-foreground">
Veuillez vérifier votre boîte mail et cliquer sur le lien de vérification.
</p>
{resendSuccess && (
<p className="text-sm text-success" role="status" aria-live="polite">
Email de vérification renvoyé avec succès !
</p>
)}
<Button
type="button"
variant="ghost"
onClick={onResend}
disabled={resendLoading}
className="text-primary hover:underline text-sm disabled:opacity-50"
aria-label="Renvoyer l'email de vérification"
>
{resendLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Envoi en cours...
</>
) : (
"Renvoyer l'email de vérification"
)}
</Button>
</div>
);
}

View file

@ -0,0 +1,2 @@
export { RegisterPage } from './RegisterPage';
export { RegisterPageSkeleton } from './RegisterPageSkeleton';

View file

@ -0,0 +1,132 @@
import { useState, useEffect } from 'react';
import { useRegister } from '../../hooks/useRegister';
import { useUsernameAvailability } from '../../hooks/useUsernameAvailability';
import { authApi } from '@/services/api/auth';
import { logger } from '@/utils/logger';
import type { RegisterFormData } from '../../types';
export type FormErrors = Partial<Record<keyof RegisterFormData | 'terms', string>>;
const initialFormData: RegisterFormData = {
email: '',
password: '',
password_confirm: '',
username: '',
};
export function useRegisterPage() {
const {
mutate: handleRegister,
isPending: loading,
error,
isSuccess: success,
} = useRegister();
const [formData, setFormData] = useState<RegisterFormData>(initialFormData);
const { available: usernameAvailable, checking: checkingUsername } =
useUsernameAvailability(formData.username);
const [errors, setErrors] = useState<FormErrors>({});
const [acceptedTerms, setAcceptedTerms] = useState(false);
const [showVerificationNotice, setShowVerificationNotice] = useState(false);
const [resendLoading, setResendLoading] = useState(false);
const [resendSuccess, setResendSuccess] = useState(false);
useEffect(() => {
if (success) setShowVerificationNotice(true);
}, [success]);
const validate = (): boolean => {
const newErrors: FormErrors = {};
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((prev) => ({ ...prev, [field]: value }));
if (errors[field]) setErrors((prev) => ({ ...prev, [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;
}
setErrors((prev) => ({ ...prev, [field]: error }));
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validate()) handleRegister(formData);
};
const handleResendVerificationEmail = async () => {
try {
setResendLoading(true);
setResendSuccess(false);
await authApi.resendVerification({ email: formData.email });
setResendSuccess(true);
} catch (err) {
logger.error("Erreur lors du renvoi de l'email:", { error: err });
} finally {
setResendLoading(false);
}
};
return {
formData,
errors,
acceptedTerms,
setAcceptedTerms,
setErrors,
showVerificationNotice,
usernameAvailable,
checkingUsername,
loading,
error,
resendLoading,
resendSuccess,
handleChange,
handleBlur,
onSubmit,
handleResendVerificationEmail,
};
}

View file

@ -1,98 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent } from '@storybook/test';
import { RegisterPage } from './RegisterPage';
import { withRouter, withQueryClient, withToast } from '../../../stories/decorators';
const meta: Meta<typeof RegisterPage> = {
title: 'App/Pages/Auth/RegisterPage',
component: RegisterPage,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Page d\'inscription avec validation complète et vérification email.',
},
},
},
tags: ['autodocs'],
decorators: [
withRouter,
withQueryClient,
withToast,
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut de la page d'inscription.
* Formulaire vide prêt à être rempli.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* Simulation d'une erreur d'inscription.
* Par exemple, nom d'utilisateur déjà pris.
*/
export const WithError: Story = {
name: 'Avec erreur',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Remplir les champs
const usernameInput = canvas.getByLabelText(/nom d'utilisateur/i);
const emailInput = canvas.getByLabelText(/email/i);
const passwordInput = canvas.getByLabelText(/^mot de passe \*/i);
const confirmPasswordInput = canvas.getByLabelText(/confirmer le mot de passe/i);
await userEvent.type(usernameInput, 'existinguser');
await userEvent.type(emailInput, 'new@veza.music');
await userEvent.type(passwordInput, 'SecurePass123!@#');
await userEvent.type(confirmPasswordInput, 'SecurePass123!@#');
// L'erreur de disponibilité sera affichée via le hook useUsernameAvailability
},
parameters: {
docs: {
description: {
story: 'Démontre l\'affichage d\'erreurs de validation en temps réel.',
},
},
},
};
/**
* État de succès après inscription.
* Affiche le message de vérification email.
*/
export const Success: Story = {
name: 'Succès - Vérification email',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Remplir tous les champs avec des données valides
const usernameInput = canvas.getByLabelText(/nom d'utilisateur/i);
const emailInput = canvas.getByLabelText(/email/i);
const passwordInput = canvas.getByLabelText(/^mot de passe \*/i);
const confirmPasswordInput = canvas.getByLabelText(/confirmer le mot de passe/i);
const termsCheckbox = canvas.getByRole('checkbox');
await userEvent.type(usernameInput, 'newartist');
await userEvent.type(emailInput, 'artist@veza.music');
await userEvent.type(passwordInput, 'SuperSecure123!@#');
await userEvent.type(confirmPasswordInput, 'SuperSecure123!@#');
await userEvent.click(termsCheckbox);
// Le succès sera affiché après la mutation réussie
},
parameters: {
docs: {
description: {
story: 'Montre le message de succès avec instructions de vérification email.',
},
},
},
};

View file

@ -1,380 +1,4 @@
import React, { useState, useEffect } from 'react';
import { Navigate, Link } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
import { AuthLayout } from '../components/AuthLayout';
import { PasswordStrengthIndicator } from '../components/PasswordStrengthIndicator';
import { useRegister } from '../hooks/useRegister';
import { useUsernameAvailability } from '../hooks/useUsernameAvailability';
import { authApi } from '@/services/api/auth';
import { logger } from '@/utils/logger';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Loader2 } from 'lucide-react';
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 authApi.resendVerification({ email: formData.email });
setResendSuccess(true);
} catch (err) {
// Gérer l'erreur silencieusement ou afficher un message
logger.error("Erreur lors du renvoi de l'email:", { error: 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-kodo-lime/10 border border-kodo-lime text-kodo-lime px-4 py-4 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-kodo-content-dim">
Veuillez vérifier votre boîte mail et cliquer sur le lien de
vérification.
</p>
{resendSuccess && (
<p
className="text-sm text-kodo-lime"
role="status"
aria-live="polite"
>
Email de vérification renvoyé avec succès !
</p>
)}
<Button
type="button"
variant="ghost"
onClick={handleResendVerificationEmail}
disabled={resendLoading}
className="text-kodo-cyan hover:underline text-sm disabled:opacity-50"
aria-label="Renvoyer l'email de vérification"
>
{resendLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Envoi en cours...
</>
) : (
"Renvoyer l'email de vérification"
)}
</Button>
</div>
) : (
<form
onSubmit={onSubmit}
className="space-y-4"
aria-label="Formulaire d'inscription"
>
{error && (
<div
className="bg-kodo-red/10 border border-kodo-red text-kodo-red px-4 py-4 rounded"
role="alert"
aria-live="assertive"
>
{error.message}
</div>
)}
<div className="space-y-2">
<Label htmlFor="username">Nom d'utilisateur *</Label>
<Input
id="username"
type="text"
value={formData.username}
onChange={(e) => handleChange('username', e.target.value)}
onBlur={() => handleBlur('username')}
required
autoComplete="username"
aria-invalid={errors.username ? 'true' : 'false'}
/>
{errors.username && (
<p className="mt-1 text-sm text-kodo-red" role="alert">
{errors.username}
</p>
)}
{formData.username.length >= 3 && (
<div className="mt-1" aria-live="polite" aria-atomic="true">
{checkingUsername ? (
<p className="text-xs text-kodo-content-dim" 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-kodo-lime" 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-kodo-red" 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>
<div className="space-y-2">
<Label htmlFor="email">Email *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
required
autoComplete="email"
aria-invalid={errors.email ? 'true' : 'false'}
/>
{errors.email && (
<p className="mt-1 text-sm text-kodo-red" role="alert">
{errors.email}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="password">Mot de passe *</Label>
<Input
id="password"
type="password"
value={formData.password}
onChange={(e) => handleChange('password', e.target.value)}
onBlur={() => handleBlur('password')}
required
autoComplete="new-password"
aria-invalid={errors.password ? 'true' : 'false'}
/>
{errors.password && (
<p className="mt-1 text-sm text-kodo-red" role="alert">
{errors.password}
</p>
)}
<PasswordStrengthIndicator password={formData.password} />
</div>
<div className="space-y-2">
<Label htmlFor="password_confirm">Confirmer le mot de passe *</Label>
<Input
id="password_confirm"
type="password"
value={formData.password_confirm}
onChange={(e) => handleChange('password_confirm', e.target.value)}
onBlur={() => handleBlur('password_confirm')}
required
autoComplete="new-password"
aria-invalid={errors.password_confirm ? 'true' : 'false'}
/>
{errors.password_confirm && (
<p className="mt-1 text-sm text-kodo-red" role="alert">
{errors.password_confirm}
</p>
)}
</div>
<div className="flex items-start">
<Checkbox
id="terms"
checked={acceptedTerms}
onCheckedChange={(checked) => {
setAcceptedTerms(checked as boolean);
if (errors.terms) {
setErrors({ ...errors, terms: undefined });
}
}}
required
aria-invalid={errors.terms ? 'true' : 'false'}
aria-describedby={
errors.terms ? 'terms-error' : 'terms-description'
}
/>
<label htmlFor="terms" className="ml-2 block text-sm text-kodo-text-main">
J'accepte les{' '}
<Link
to="/terms"
className="text-kodo-cyan 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-kodo-cyan 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-kodo-red" role="alert">
{errors.terms}
</p>
)}
<Button type="submit" disabled={loading} className="w-full">
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Inscription en cours...
</>
) : (
"S'inscrire"
)}
</Button>
</form>
)}
</AuthLayout>
);
}
export default RegisterPage;
/**
* Register page re-export from feature module.
*/
export { RegisterPage } from '../components/register-page';