1e529a6741
- use `min-h-dvh`, `flex-col`, and top-aligned justify on small screens in AuthPage - add `mx-auto` to all auth page cards for consistent centering
227 lines
8.3 KiB
JavaScript
227 lines
8.3 KiB
JavaScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Card, Input, Button, PasswordStrengthIndicator } from '@zen/core/shared/components';
|
|
import AuthPageHeader from '../components/AuthPageHeader.js';
|
|
|
|
export default function RegisterPage({ onSubmit, onNavigate, currentUser = null }) {
|
|
const [error, setError] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [success, setSuccess] = useState('');
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
email: '',
|
|
password: '',
|
|
confirmPassword: ''
|
|
});
|
|
const [honeypot, setHoneypot] = useState('');
|
|
const [formLoadedAt, setFormLoadedAt] = useState(0);
|
|
|
|
useEffect(() => {
|
|
setFormLoadedAt(Date.now());
|
|
}, []);
|
|
|
|
const validateEmail = (email) => {
|
|
const errors = [];
|
|
if (email.length > 254) errors.push('L\'e-mail doit contenir 254 caractères ou moins');
|
|
return errors;
|
|
};
|
|
|
|
const validatePassword = (password) => {
|
|
const errors = [];
|
|
if (password.length < 8) errors.push('Le mot de passe doit contenir au moins 8 caractères');
|
|
if (password.length > 128) errors.push('Le mot de passe doit contenir 128 caractères ou moins');
|
|
if (!/[A-Z]/.test(password)) errors.push('Le mot de passe doit contenir au moins une majuscule');
|
|
if (!/[a-z]/.test(password)) errors.push('Le mot de passe doit contenir au moins une minuscule');
|
|
if (!/\d/.test(password)) errors.push('Le mot de passe doit contenir au moins un chiffre');
|
|
return errors;
|
|
};
|
|
|
|
const validateName = (name) => {
|
|
const errors = [];
|
|
if (name.trim().length === 0) errors.push('Le nom ne peut pas être vide');
|
|
if (name.length > 100) errors.push('Le nom doit contenir 100 caractères ou moins');
|
|
return errors;
|
|
};
|
|
|
|
const isFormValid = () => {
|
|
return validateEmail(formData.email).length === 0 &&
|
|
validatePassword(formData.password).length === 0 &&
|
|
validateName(formData.name).length === 0 &&
|
|
formData.password === formData.confirmPassword &&
|
|
formData.email.trim().length > 0;
|
|
};
|
|
|
|
async function handleSubmit(e) {
|
|
e.preventDefault();
|
|
setError('');
|
|
setSuccess('');
|
|
setIsLoading(true);
|
|
|
|
const emailErrors = validateEmail(formData.email);
|
|
const passwordErrors = validatePassword(formData.password);
|
|
const nameErrors = validateName(formData.name);
|
|
|
|
if (emailErrors.length > 0) { setError(emailErrors[0]); setIsLoading(false); return; }
|
|
if (passwordErrors.length > 0) { setError(passwordErrors[0]); setIsLoading(false); return; }
|
|
if (nameErrors.length > 0) { setError(nameErrors[0]); setIsLoading(false); return; }
|
|
if (formData.password !== formData.confirmPassword) {
|
|
setError('Les mots de passe ne correspondent pas');
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
const submitData = new FormData();
|
|
submitData.append('name', formData.name);
|
|
submitData.append('email', formData.email);
|
|
submitData.append('password', formData.password);
|
|
submitData.append('confirmPassword', formData.confirmPassword);
|
|
submitData.append('_hp', honeypot);
|
|
submitData.append('_t', String(formLoadedAt));
|
|
|
|
try {
|
|
const result = await onSubmit(submitData);
|
|
|
|
if (result.success) {
|
|
setSuccess(result.message);
|
|
setIsLoading(false);
|
|
} else {
|
|
setError(result.error || 'Échec de l\'inscription');
|
|
setIsLoading(false);
|
|
}
|
|
} catch (err) {
|
|
console.error('Registration error:', err);
|
|
setError('Une erreur inattendue s\'est produite');
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Card variant="default" padding="md" spacing="none" className="w-full max-w-md mx-auto">
|
|
<AuthPageHeader title="Créer un compte" description="Inscrivez-vous pour commencer." />
|
|
|
|
{currentUser && (
|
|
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg dark:bg-blue-500/10 dark:border-blue-500/20">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full flex-shrink-0"></div>
|
|
<span className="text-xs text-blue-700 dark:text-blue-400">
|
|
Vous êtes connecté en tant que <span className="font-medium">{currentUser.name}</span>.{' '}
|
|
<a href="/auth/logout" className="underline hover:text-blue-600 dark:hover:text-blue-300 transition-colors duration-200">
|
|
Se déconnecter ?
|
|
</a>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{error && !currentUser && !success && (
|
|
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg dark:bg-red-500/10 dark:border-red-500/20">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-1.5 h-1.5 bg-red-500 rounded-full flex-shrink-0"></div>
|
|
<span className="text-xs text-red-700 dark:text-red-400">{error}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{success && !currentUser && (
|
|
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg dark:bg-green-500/10 dark:border-green-500/20">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-1.5 h-1.5 bg-green-500 rounded-full flex-shrink-0"></div>
|
|
<span className="text-xs text-green-700 dark:text-green-400">{success}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className={`flex flex-col gap-4 transition-opacity duration-200 ${currentUser ? 'opacity-50 pointer-events-none' : ''}`}>
|
|
<div aria-hidden="true" style={{ position: 'absolute', left: '-9999px', top: '-9999px', width: 0, height: 0, overflow: 'hidden' }}>
|
|
<label htmlFor="_hp_register">Website</label>
|
|
<input id="_hp_register" name="_hp" type="text" tabIndex={-1} autoComplete="off" value={honeypot} onChange={(e) => setHoneypot(e.target.value)} />
|
|
</div>
|
|
|
|
<Input
|
|
id="name"
|
|
name="name"
|
|
type="text"
|
|
label="Nom complet"
|
|
value={formData.name}
|
|
onChange={(value) => setFormData(prev => ({ ...prev, name: value }))}
|
|
placeholder="Lou Doe"
|
|
disabled={!!success || !!currentUser}
|
|
maxLength="100"
|
|
autoComplete="name"
|
|
required
|
|
/>
|
|
|
|
<Input
|
|
id="email"
|
|
name="email"
|
|
type="email"
|
|
label="Courriel"
|
|
value={formData.email}
|
|
onChange={(value) => setFormData(prev => ({ ...prev, email: value }))}
|
|
placeholder="votre@courriel.com"
|
|
disabled={!!success || !!currentUser}
|
|
maxLength="254"
|
|
autoComplete="email"
|
|
required
|
|
/>
|
|
|
|
<div>
|
|
<Input
|
|
id="password"
|
|
name="password"
|
|
type="password"
|
|
label="Mot de passe"
|
|
value={formData.password}
|
|
onChange={(value) => setFormData(prev => ({ ...prev, password: value }))}
|
|
placeholder="••••••••"
|
|
disabled={!!success || !!currentUser}
|
|
minLength="8"
|
|
maxLength="128"
|
|
autoComplete="new-password"
|
|
required
|
|
/>
|
|
<PasswordStrengthIndicator password={formData.password} showRequirements={true} />
|
|
</div>
|
|
|
|
<Input
|
|
id="confirmPassword"
|
|
name="confirmPassword"
|
|
type="password"
|
|
label="Confirmer le mot de passe"
|
|
value={formData.confirmPassword}
|
|
onChange={(value) => setFormData(prev => ({ ...prev, confirmPassword: value }))}
|
|
placeholder="••••••••"
|
|
disabled={!!success || !!currentUser}
|
|
minLength="8"
|
|
maxLength="128"
|
|
autoComplete="new-password"
|
|
required
|
|
/>
|
|
|
|
<Button
|
|
type="submit"
|
|
variant="primary"
|
|
size="lg"
|
|
loading={isLoading}
|
|
disabled={!!success || !!currentUser || !isFormValid()}
|
|
className="w-full mt-2"
|
|
>
|
|
Créer un compte
|
|
</Button>
|
|
</form>
|
|
|
|
<div className={`mt-6 flex justify-center transition-opacity duration-200 ${currentUser ? 'opacity-50 pointer-events-none' : ''}`}>
|
|
<Button
|
|
type="button"
|
|
variant="fullghost"
|
|
disabled={!!currentUser}
|
|
onClick={() => { if (!currentUser) onNavigate('login'); }}
|
|
>
|
|
Vous avez déjà un compte ? Se connecter
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|