veza/apps/web/src/features/auth/components/register-page/RegisterPageForm.tsx

199 lines
6.8 KiB
TypeScript
Raw Normal View History

import { Link } from 'react-router-dom';
import { Checkbox } from '@/components/ui/checkbox';
import { AlertCircle, Loader2, Check, X } from 'lucide-react';
import { AuthInput } from '../AuthInput';
import { AuthButton } from '../AuthButton';
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"
data-testid="register-form"
>
{error && (
<div
className="bg-destructive/10 border border-destructive/30 text-destructive px-4 py-3 rounded-lg text-sm flex items-center gap-2 animate-in fade-in slide-in-from-top-1"
role="alert"
aria-live="assertive"
>
<AlertCircle className="w-4 h-4 flex-shrink-0" />
<p>{error.message}</p>
</div>
)}
<div className="space-y-4">
{/* Username */}
<div>
<AuthInput
id="register-username"
type="text"
label="Nom d'utilisateur"
value={formData.username}
onChange={(e) => onFieldChange('username', e.target.value)}
onBlur={() => onFieldBlur('username')}
required
autoComplete="username"
error={errors.username}
/>
{formData.username.length >= 3 && (
<div className="mt-1.5" aria-live="polite" aria-atomic="true">
{checkingUsername ? (
<p className="text-xs text-muted-foreground flex items-center gap-1.5" role="status">
<span className="h-3 w-3 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" />
<span>Vérification...</span>
</p>
) : usernameAvailable === true ? (
<p className="text-xs text-success flex items-center gap-1.5" role="status">
<Check className="h-3 w-3" />
<span>Ce nom d'utilisateur est disponible</span>
</p>
) : usernameAvailable === false ? (
<p className="text-xs text-destructive flex items-center gap-1.5" role="alert">
<X className="h-3 w-3" />
<span>Ce nom d'utilisateur est déjà pris</span>
</p>
) : null}
</div>
)}
</div>
{/* Email */}
<AuthInput
id="register-email"
type="email"
label="Email"
value={formData.email}
onChange={(e) => onFieldChange('email', e.target.value)}
onBlur={() => onFieldBlur('email')}
required
autoComplete="email"
error={errors.email}
/>
{/* Password */}
<div>
<AuthInput
id="register-password"
type="password"
label="Mot de passe"
value={formData.password}
onChange={(e) => onFieldChange('password', e.target.value)}
onBlur={() => onFieldBlur('password')}
required
autoComplete="new-password"
showPasswordToggle
error={errors.password}
/>
<PasswordStrengthIndicator password={formData.password} />
</div>
{/* Confirm password */}
<AuthInput
id="register-password_confirm"
type="password"
label="Confirmer le mot de passe"
value={formData.password_confirm}
onChange={(e) => onFieldChange('password_confirm', e.target.value)}
onBlur={() => onFieldBlur('password_confirm')}
required
autoComplete="new-password"
showPasswordToggle
error={errors.password_confirm}
/>
</div>
{/* Terms */}
<div className="flex items-start gap-3 pt-1">
<div className="pt-0.5">
<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'}
/>
</div>
<label htmlFor="register-terms" className="text-sm text-muted-foreground leading-relaxed cursor-pointer inline-flex flex-wrap gap-x-1">
<span>J'accepte les</span>
<Link
to="/terms"
className="text-foreground hover:underline underline-offset-4 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded"
aria-label="Lire les conditions d'utilisation"
>
conditions d'utilisation
</Link>
<span>et la</span>
<Link
to="/privacy"
className="text-foreground hover:underline underline-offset-4 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 animate-shake" role="alert">
{errors.terms}
</p>
)}
<AuthButton
type="submit"
loading={loading}
className="w-full bg-primary text-primary-foreground hover:opacity-90 shadow-sm"
data-testid="register-submit"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Inscription en cours...
</>
) : (
"S'inscrire"
)}
</AuthButton>
</form>
);
}