style(auth): replace inline card styles with Card component and clean up comments in ConfirmEmailPage
This commit is contained in:
@@ -1,10 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm Email Page Component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Card } from '@zen/core/shared/components';
|
||||||
|
|
||||||
export default function ConfirmEmailPage({ onSubmit, onNavigate, email, token }) {
|
export default function ConfirmEmailPage({ onSubmit, onNavigate, email, token }) {
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -15,26 +12,20 @@ export default function ConfirmEmailPage({ onSubmit, onNavigate, email, token })
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('ConfirmEmailPage useEffect triggered', { email, token, hasVerified });
|
console.log('ConfirmEmailPage useEffect triggered', { email, token, hasVerified });
|
||||||
|
|
||||||
// Check for persisted success message on mount
|
|
||||||
const persistedSuccess = sessionStorage.getItem('emailVerificationSuccess');
|
const persistedSuccess = sessionStorage.getItem('emailVerificationSuccess');
|
||||||
console.log('Persisted success message:', persistedSuccess);
|
console.log('Persisted success message:', persistedSuccess);
|
||||||
|
|
||||||
if (persistedSuccess) {
|
if (persistedSuccess) {
|
||||||
console.log('Restoring persisted success message');
|
console.log('Restoring persisted success message');
|
||||||
setSuccess(persistedSuccess);
|
setSuccess(persistedSuccess);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setHasVerified(true); // Mark as verified to prevent re-verification
|
setHasVerified(true);
|
||||||
// Clear the persisted message after showing it
|
|
||||||
sessionStorage.removeItem('emailVerificationSuccess');
|
sessionStorage.removeItem('emailVerificationSuccess');
|
||||||
// Redirect after showing the message
|
setTimeout(() => onNavigate('login'), 3000);
|
||||||
setTimeout(() => {
|
|
||||||
onNavigate('login');
|
|
||||||
}, 3000);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-verify on mount, but only once
|
|
||||||
if (email && token && !hasVerified && !isVerifyingRef.current) {
|
if (email && token && !hasVerified && !isVerifyingRef.current) {
|
||||||
console.log('Starting email verification');
|
console.log('Starting email verification');
|
||||||
verifyEmail();
|
verifyEmail();
|
||||||
@@ -46,22 +37,18 @@ export default function ConfirmEmailPage({ onSubmit, onNavigate, email, token })
|
|||||||
}, [email, token, hasVerified, onNavigate]);
|
}, [email, token, hasVerified, onNavigate]);
|
||||||
|
|
||||||
async function verifyEmail() {
|
async function verifyEmail() {
|
||||||
// Prevent multiple calls
|
|
||||||
if (hasVerified || isVerifyingRef.current) {
|
if (hasVerified || isVerifyingRef.current) {
|
||||||
console.log('Email verification already attempted or in progress');
|
console.log('Email verification already attempted or in progress');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set flags IMMEDIATELY to prevent multiple calls
|
|
||||||
isVerifyingRef.current = true;
|
isVerifyingRef.current = true;
|
||||||
setHasVerified(true);
|
setHasVerified(true);
|
||||||
|
|
||||||
// Clear any existing states at the start
|
|
||||||
setError('');
|
setError('');
|
||||||
setSuccess('');
|
setSuccess('');
|
||||||
|
|
||||||
console.log('Starting email verification for:', email);
|
console.log('Starting email verification for:', email);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.set('email', email);
|
formData.set('email', email);
|
||||||
formData.set('token', token);
|
formData.set('token', token);
|
||||||
@@ -69,20 +56,14 @@ export default function ConfirmEmailPage({ onSubmit, onNavigate, email, token })
|
|||||||
try {
|
try {
|
||||||
const result = await onSubmit(formData);
|
const result = await onSubmit(formData);
|
||||||
console.log('Verification result:', result);
|
console.log('Verification result:', result);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('Verification successful');
|
console.log('Verification successful');
|
||||||
const successMessage = result.message || 'E-mail vérifié avec succès. Vous pouvez maintenant vous connecter.';
|
const successMessage = result.message || 'E-mail vérifié avec succès. Vous pouvez maintenant vous connecter.';
|
||||||
|
|
||||||
// Persist success message in sessionStorage
|
|
||||||
sessionStorage.setItem('emailVerificationSuccess', successMessage);
|
sessionStorage.setItem('emailVerificationSuccess', successMessage);
|
||||||
|
|
||||||
setSuccess(successMessage);
|
setSuccess(successMessage);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
// Redirect to login after 3 seconds
|
setTimeout(() => onNavigate('login'), 3000);
|
||||||
setTimeout(() => {
|
|
||||||
onNavigate('login');
|
|
||||||
}, 3000);
|
|
||||||
} else {
|
} else {
|
||||||
console.log('Verification failed:', result.error);
|
console.log('Verification failed:', result.error);
|
||||||
setError(result.error || 'Échec de la vérification de l\'e-mail');
|
setError(result.error || 'Échec de la vérification de l\'e-mail');
|
||||||
@@ -98,65 +79,55 @@ export default function ConfirmEmailPage({ onSubmit, onNavigate, email, token })
|
|||||||
console.log('ConfirmEmailPage render', { success, error, isLoading, hasVerified });
|
console.log('ConfirmEmailPage render', { success, error, isLoading, hasVerified });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group bg-white/80 dark:bg-[#0B0B0B] backdrop-blur-sm border border-neutral-200/80 dark:border-neutral-800/50 rounded-2xl px-4 py-6 md:px-6 md:py-8 hover:bg-neutral-50/90 hover:border-neutral-300/80 dark:hover:bg-neutral-900/50 dark:hover:border-neutral-700/50 transition-all duration-300 w-full max-w-md">
|
<Card variant="default" padding="md" spacing="none" className="w-full max-w-md">
|
||||||
{/* Header */}
|
<div className="text-center mb-6">
|
||||||
<div className="text-center mb-6">
|
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
Vérification de l'e-mail
|
||||||
Vérification de l'e-mail
|
</h1>
|
||||||
</h1>
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
Nous vérifions votre adresse e-mail...
|
||||||
Nous vérifions votre adresse e-mail...
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLoading && (
|
|
||||||
<div className="flex flex-col items-center py-10">
|
|
||||||
<div className="w-12 h-12 border-4 border-neutral-300 border-t-neutral-900 rounded-full animate-spin dark:border-neutral-700/50 dark:border-t-white"></div>
|
|
||||||
<p className="mt-5 text-neutral-600 dark:text-neutral-400 text-sm">Vérification de votre e-mail en cours...</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Success Message - Only show if success and no error */}
|
|
||||||
{success && !error && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error Message - Only show if error and no success */}
|
|
||||||
{error && !success && (
|
|
||||||
<div>
|
|
||||||
<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>
|
|
||||||
<div className="mt-6 pt-6 border-t border-neutral-200 dark:border-neutral-800/50 text-center">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onNavigate('login');
|
|
||||||
}}
|
|
||||||
className="text-sm text-neutral-900 hover:text-neutral-600 font-medium transition-colors duration-200 dark:text-white dark:hover:text-neutral-300"
|
|
||||||
>
|
|
||||||
← Retour à la connexion
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Redirect message - Only show if success and no error */}
|
|
||||||
{success && !error && (
|
|
||||||
<p className="text-center text-neutral-600 dark:text-neutral-400 text-sm mt-3">Redirection vers la connexion...</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex flex-col items-center py-10">
|
||||||
|
<div className="w-12 h-12 border-4 border-neutral-300 border-t-neutral-900 rounded-full animate-spin dark:border-neutral-700/50 dark:border-t-white"></div>
|
||||||
|
<p className="mt-5 text-neutral-600 dark:text-neutral-400 text-sm">Vérification de votre e-mail en cours...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && !error && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && !success && (
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
<div className="mt-6 pt-6 border-t border-neutral-200 dark:border-neutral-800/50 text-center">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => { e.preventDefault(); onNavigate('login'); }}
|
||||||
|
className="text-sm text-neutral-900 hover:text-neutral-600 font-medium transition-colors duration-200 dark:text-white dark:hover:text-neutral-300"
|
||||||
|
>
|
||||||
|
← Retour à la connexion
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && !error && (
|
||||||
|
<p className="text-center text-neutral-600 dark:text-neutral-400 text-sm mt-3">Redirection vers la connexion...</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
/**
|
|
||||||
* Forgot Password Page Component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Card, Input, Button } from '@zen/core/shared/components';
|
||||||
|
|
||||||
export default function ForgotPasswordPage({ onSubmit, onNavigate, currentUser = null }) {
|
export default function ForgotPasswordPage({ onSubmit, onNavigate, currentUser = null }) {
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
const [formData, setFormData] = useState({
|
const [email, setEmail] = useState('');
|
||||||
email: ''
|
|
||||||
});
|
|
||||||
const [honeypot, setHoneypot] = useState('');
|
const [honeypot, setHoneypot] = useState('');
|
||||||
const [formLoadedAt, setFormLoadedAt] = useState(0);
|
const [formLoadedAt, setFormLoadedAt] = useState(0);
|
||||||
|
|
||||||
@@ -20,14 +15,6 @@ export default function ForgotPasswordPage({ onSubmit, onNavigate, currentUser =
|
|||||||
setFormLoadedAt(Date.now());
|
setFormLoadedAt(Date.now());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleChange = (e) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[name]: value
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
@@ -35,13 +22,13 @@ export default function ForgotPasswordPage({ onSubmit, onNavigate, currentUser =
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const submitData = new FormData();
|
const submitData = new FormData();
|
||||||
submitData.append('email', formData.email);
|
submitData.append('email', email);
|
||||||
submitData.append('_hp', honeypot);
|
submitData.append('_hp', honeypot);
|
||||||
submitData.append('_t', String(formLoadedAt));
|
submitData.append('_t', String(formLoadedAt));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await onSubmit(submitData);
|
const result = await onSubmit(submitData);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setSuccess(result.message);
|
setSuccess(result.message);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -56,119 +43,91 @@ export default function ForgotPasswordPage({ onSubmit, onNavigate, currentUser =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputClasses = 'w-full px-3 py-2.5 rounded-lg text-sm focus:outline-none transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group bg-white/80 dark:bg-[#0B0B0B] backdrop-blur-sm border border-neutral-200/80 dark:border-neutral-800/50 rounded-2xl px-4 py-6 md:px-6 md:py-8 hover:bg-neutral-50/90 hover:border-neutral-300/80 dark:hover:bg-neutral-900/50 dark:hover:border-neutral-700/50 transition-all duration-300 w-full max-w-md">
|
<Card variant="default" padding="md" spacing="none" className="w-full max-w-md">
|
||||||
{/* Header */}
|
<div className="text-center mb-6">
|
||||||
<div className="text-center mb-6">
|
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
Mot de passe oublié
|
||||||
Mot de passe oublié
|
</h1>
|
||||||
</h1>
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
Entrez votre adresse e-mail et nous vous enverrons un lien pour réinitialiser votre mot de passe.
|
||||||
Entrez votre adresse e-mail et nous vous enverrons un lien pour réinitialiser votre mot de passe.
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Already Connected Message */}
|
|
||||||
{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 justify-between">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
|
||||||
<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 Message */}
|
|
||||||
{success && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Forgot Password Form */}
|
|
||||||
<form onSubmit={handleSubmit} className={`flex flex-col gap-4 transition-opacity duration-200 ${currentUser ? 'opacity-50 pointer-events-none' : ''}`}>
|
|
||||||
{/* Honeypot — invisible to humans, filled by bots */}
|
|
||||||
<div aria-hidden="true" style={{ position: 'absolute', left: '-9999px', top: '-9999px', width: 0, height: 0, overflow: 'hidden' }}>
|
|
||||||
<label htmlFor="_hp_forgot">Website</label>
|
|
||||||
<input id="_hp_forgot" name="_hp" type="text" tabIndex={-1} autoComplete="off" value={honeypot} onChange={(e) => setHoneypot(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-xs font-medium text-neutral-700 dark:text-white mb-1.5">
|
|
||||||
E-mail
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={success || currentUser}
|
|
||||||
className={inputClasses}
|
|
||||||
placeholder="your@email.com"
|
|
||||||
autoComplete="email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading || success || currentUser}
|
|
||||||
className="cursor-pointer w-full bg-neutral-900 text-white mt-2 py-2.5 px-4 rounded-lg text-sm font-medium hover:bg-neutral-800 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-neutral-900/20 dark:bg-white dark:text-black dark:hover:bg-neutral-100 dark:focus:ring-white/20"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex items-center justify-center space-x-2">
|
|
||||||
<div className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin dark:border-black/20 dark:border-t-black"></div>
|
|
||||||
<span>Envoi en cours...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
'Envoyer le lien de réinitialisation'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Back to Login Link */}
|
|
||||||
<div className={`mt-6 text-center transition-opacity duration-200 ${currentUser ? 'opacity-50 pointer-events-none' : ''}`}>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!currentUser) {
|
|
||||||
onNavigate('login');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="text-sm text-neutral-900 hover:text-neutral-600 font-medium transition-colors duration-200 dark:text-white dark:hover:text-neutral-300"
|
|
||||||
>
|
|
||||||
← Retour à la connexion
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{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 && (
|
||||||
|
<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 && (
|
||||||
|
<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_forgot">Website</label>
|
||||||
|
<input id="_hp_forgot" name="_hp" type="text" tabIndex={-1} autoComplete="off" value={honeypot} onChange={(e) => setHoneypot(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
label="E-mail"
|
||||||
|
value={email}
|
||||||
|
onChange={setEmail}
|
||||||
|
placeholder="your@email.com"
|
||||||
|
disabled={!!success || !!currentUser}
|
||||||
|
autoComplete="email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
loading={isLoading}
|
||||||
|
disabled={!!success || !!currentUser}
|
||||||
|
className="w-full mt-2"
|
||||||
|
>
|
||||||
|
Envoyer le lien de réinitialisation
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className={`mt-6 text-center transition-opacity duration-200 ${currentUser ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!currentUser) onNavigate('login');
|
||||||
|
}}
|
||||||
|
className="text-sm text-neutral-900 hover:text-neutral-600 font-medium transition-colors duration-200 dark:text-white dark:hover:text-neutral-300"
|
||||||
|
>
|
||||||
|
← Retour à la connexion
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
/**
|
|
||||||
* Login Page Component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { Card, Input, Button } from '@zen/core/shared/components';
|
||||||
|
|
||||||
export default function LoginPage({ onSubmit, onNavigate, onSetSessionCookie, redirectAfterLogin = '/', currentUser = null }) {
|
export default function LoginPage({ onSubmit, onNavigate, onSetSessionCookie, redirectAfterLogin = '/', currentUser = null }) {
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({ email: '', password: '' });
|
||||||
email: '',
|
|
||||||
password: ''
|
|
||||||
});
|
|
||||||
const [honeypot, setHoneypot] = useState('');
|
const [honeypot, setHoneypot] = useState('');
|
||||||
const [formLoadedAt, setFormLoadedAt] = useState(0);
|
const [formLoadedAt, setFormLoadedAt] = useState(0);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -23,21 +17,12 @@ export default function LoginPage({ onSubmit, onNavigate, onSetSessionCookie, re
|
|||||||
setFormLoadedAt(Date.now());
|
setFormLoadedAt(Date.now());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// If already logged in, redirect to redirectAfterLogin
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
router.replace(redirectAfterLogin);
|
router.replace(redirectAfterLogin);
|
||||||
}
|
}
|
||||||
}, [currentUser, redirectAfterLogin, router]);
|
}, [currentUser, redirectAfterLogin, router]);
|
||||||
|
|
||||||
const handleChange = (e) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[name]: value
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyPress = (e) => {
|
const handleKeyPress = (e) => {
|
||||||
if (e.key === 'Enter' && !isLoading && !success) {
|
if (e.key === 'Enter' && !isLoading && !success) {
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
@@ -57,21 +42,16 @@ export default function LoginPage({ onSubmit, onNavigate, onSetSessionCookie, re
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await onSubmit(submitData);
|
const result = await onSubmit(submitData);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const successMsg = result.message || 'Connexion réussie ! Redirection...';
|
const successMsg = result.message || 'Connexion réussie ! Redirection...';
|
||||||
|
|
||||||
// Display success message immediately (no page refresh because we didn't set cookie yet)
|
|
||||||
setSuccess(successMsg);
|
setSuccess(successMsg);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
// Wait for user to see the success message
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
// Now set the session cookie (this might cause a refresh, but we're redirecting anyway)
|
|
||||||
if (result.sessionToken && onSetSessionCookie) {
|
if (result.sessionToken && onSetSessionCookie) {
|
||||||
await onSetSessionCookie(result.sessionToken);
|
await onSetSessionCookie(result.sessionToken);
|
||||||
}
|
}
|
||||||
// Then navigate
|
|
||||||
router.push(redirectAfterLogin);
|
router.push(redirectAfterLogin);
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} else {
|
} else {
|
||||||
@@ -85,144 +65,114 @@ export default function LoginPage({ onSubmit, onNavigate, onSetSessionCookie, re
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputClasses = 'w-full px-[10px] py-[7px] rounded text-[13px] focus:outline-none transition-all duration-[120ms] ease-out disabled:opacity-50 disabled:cursor-not-allowed bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-[#0B0B0B] border border-neutral-200 dark:border-neutral-800/50 rounded-md px-4 py-6 md:px-6 md:py-8 w-full max-w-md">
|
<Card variant="default" padding="md" spacing="none" className="w-full max-w-md">
|
||||||
{/* Header */}
|
<div className="text-center mb-6">
|
||||||
<div className="text-center mb-6">
|
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
Connexion
|
||||||
Connexion
|
</h1>
|
||||||
</h1>
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
Veuillez vous connecter pour continuer.
|
||||||
Veuillez vous connecter pour continuer.
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Already logged in: redirecting (brief message while redirect runs) */}
|
|
||||||
{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-3.5 h-3.5 border-2 border-blue-400/50 border-t-blue-600 rounded-full animate-spin flex-shrink-0 dark:border-blue-500/30 dark:border-t-blue-500"></div>
|
|
||||||
<span className="text-xs text-blue-700 dark:text-blue-400">Redirection...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Success Message */}
|
|
||||||
{success && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Login Form */}
|
|
||||||
<div className={`flex flex-col gap-4 transition-opacity duration-200 ${currentUser ? 'opacity-50 pointer-events-none' : ''}`}>
|
|
||||||
{/* Honeypot — invisible to humans, filled by bots */}
|
|
||||||
<div aria-hidden="true" style={{ position: 'absolute', left: '-9999px', top: '-9999px', width: 0, height: 0, overflow: 'hidden' }}>
|
|
||||||
<label htmlFor="_hp_login">Website</label>
|
|
||||||
<input id="_hp_login" name="_hp" type="text" tabIndex={-1} autoComplete="off" value={honeypot} onChange={(e) => setHoneypot(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-xs font-medium text-neutral-700 dark:text-white mb-1.5">
|
|
||||||
E-mail
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
onKeyDown={handleKeyPress}
|
|
||||||
disabled={success || currentUser}
|
|
||||||
className={inputClasses}
|
|
||||||
placeholder="your@email.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
|
||||||
<label htmlFor="password" className="block text-xs font-medium text-neutral-700 dark:text-white">
|
|
||||||
Mot de passe
|
|
||||||
</label>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!currentUser) {
|
|
||||||
onNavigate('forgot');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="text-xs text-neutral-600 hover:text-neutral-900 transition-colors duration-200 dark:text-neutral-300 dark:hover:text-white"
|
|
||||||
>
|
|
||||||
Mot de passe oublié ?
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
value={formData.password}
|
|
||||||
onChange={handleChange}
|
|
||||||
onKeyDown={handleKeyPress}
|
|
||||||
disabled={success || currentUser}
|
|
||||||
className={inputClasses}
|
|
||||||
placeholder="••••••••"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={isLoading || success || currentUser}
|
|
||||||
className="cursor-pointer w-full bg-neutral-900 text-white mt-2 py-[7px] px-4 rounded text-[13px] font-medium hover:bg-neutral-800 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-[120ms] ease-out focus:outline-none focus:ring-1 focus:ring-neutral-900/20 dark:bg-white dark:text-black dark:hover:bg-neutral-100 dark:focus:ring-white/20"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex items-center justify-center space-x-2">
|
|
||||||
<div className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin dark:border-black/20 dark:border-t-black"></div>
|
|
||||||
<span>Connexion en cours...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
'Se connecter'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Register Link */}
|
|
||||||
<div className={`mt-6 text-center transition-opacity duration-200 ${currentUser ? 'opacity-50 pointer-events-none' : ''}`}>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!currentUser) {
|
|
||||||
onNavigate('register');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="group flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">Pas de compte ? </span>
|
|
||||||
<span className="text-sm text-neutral-900 group-hover:text-neutral-600 font-medium transition-colors duration-200 dark:text-white dark:group-hover:text-neutral-300">S'inscrire</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{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-3.5 h-3.5 border-2 border-blue-400/50 border-t-blue-600 rounded-full animate-spin flex-shrink-0 dark:border-blue-500/30 dark:border-t-blue-500"></div>
|
||||||
|
<span className="text-xs text-blue-700 dark:text-blue-400">Redirection...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div 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_login">Website</label>
|
||||||
|
<input id="_hp_login" name="_hp" type="text" tabIndex={-1} autoComplete="off" value={honeypot} onChange={(e) => setHoneypot(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
label="E-mail"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(value) => setFormData(prev => ({ ...prev, email: value }))}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
|
placeholder="your@email.com"
|
||||||
|
disabled={!!success || !!currentUser}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
|
<span className="text-xs font-medium text-neutral-700 dark:text-white">Mot de passe</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!currentUser) onNavigate('forgot');
|
||||||
|
}}
|
||||||
|
className="text-xs text-neutral-600 hover:text-neutral-900 transition-colors duration-200 dark:text-neutral-300 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
Mot de passe oublié ?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={(value) => setFormData(prev => ({ ...prev, password: value }))}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
|
placeholder="••••••••"
|
||||||
|
disabled={!!success || !!currentUser}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="primary"
|
||||||
|
loading={isLoading}
|
||||||
|
disabled={!!success || !!currentUser}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="w-full mt-2"
|
||||||
|
>
|
||||||
|
Se connecter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`mt-6 text-center transition-opacity duration-200 ${currentUser ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!currentUser) onNavigate('register');
|
||||||
|
}}
|
||||||
|
className="group flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<span className="text-sm text-neutral-600 dark:text-neutral-400">Pas de compte ? </span>
|
||||||
|
<span className="text-sm text-neutral-900 group-hover:text-neutral-600 font-medium transition-colors duration-200 dark:text-white dark:group-hover:text-neutral-300">S'inscrire</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
/**
|
|
||||||
* Logout Page Component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { Card, Button } from '@zen/core/shared/components';
|
||||||
|
|
||||||
export default function LogoutPage({ onLogout, onSetSessionCookie }) {
|
export default function LogoutPage({ onLogout, onSetSessionCookie }) {
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -19,10 +16,8 @@ export default function LogoutPage({ onLogout, onSetSessionCookie }) {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call the logout action if provided
|
|
||||||
if (onLogout) {
|
if (onLogout) {
|
||||||
const result = await onLogout();
|
const result = await onLogout();
|
||||||
|
|
||||||
if (result && !result.success) {
|
if (result && !result.success) {
|
||||||
setError(result.error || 'Échec de la déconnexion');
|
setError(result.error || 'Échec de la déconnexion');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -30,20 +25,14 @@ export default function LogoutPage({ onLogout, onSetSessionCookie }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear session cookie if provided
|
|
||||||
if (onSetSessionCookie) {
|
if (onSetSessionCookie) {
|
||||||
await onSetSessionCookie('', { expires: new Date(0) });
|
await onSetSessionCookie('', { expires: new Date(0) });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show success message
|
|
||||||
setSuccess('Vous avez été déconnecté. Redirection...');
|
setSuccess('Vous avez été déconnecté. Redirection...');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
// Wait for user to see the success message, then redirect
|
setTimeout(() => router.push('/'), 100);
|
||||||
setTimeout(() => {
|
|
||||||
router.push('/');
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Logout error:', err);
|
console.error('Logout error:', err);
|
||||||
setError('Une erreur inattendue s\'est produite lors de la déconnexion');
|
setError('Une erreur inattendue s\'est produite lors de la déconnexion');
|
||||||
@@ -52,8 +41,7 @@ export default function LogoutPage({ onLogout, onSetSessionCookie }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group bg-white/80 dark:bg-[#0B0B0B] backdrop-blur-sm border border-neutral-200/80 dark:border-neutral-800/50 rounded-2xl px-4 py-6 md:px-6 md:py-8 hover:bg-neutral-50/90 hover:border-neutral-300/80 dark:hover:bg-neutral-900/50 dark:hover:border-neutral-700/50 transition-all duration-300 w-full max-w-md">
|
<Card variant="default" padding="md" spacing="none" className="w-full max-w-md">
|
||||||
{/* Header */}
|
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
||||||
Prêt à vous déconnecter ?
|
Prêt à vous déconnecter ?
|
||||||
@@ -63,7 +51,6 @@ export default function LogoutPage({ onLogout, onSetSessionCookie }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Success Message */}
|
|
||||||
{success && (
|
{success && (
|
||||||
<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="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="flex items-center space-x-2">
|
||||||
@@ -73,7 +60,6 @@ export default function LogoutPage({ onLogout, onSetSessionCookie }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
{error && (
|
||||||
<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="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="flex items-center space-x-2">
|
||||||
@@ -83,35 +69,26 @@ export default function LogoutPage({ onLogout, onSetSessionCookie }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Logout Button */}
|
<Button
|
||||||
<div className="flex flex-col gap-4">
|
type="button"
|
||||||
<button
|
variant="danger"
|
||||||
type="button"
|
loading={isLoading}
|
||||||
onClick={handleLogout}
|
disabled={!!success}
|
||||||
disabled={isLoading || success}
|
onClick={handleLogout}
|
||||||
className="cursor-pointer w-full bg-red-600 text-white mt-2 py-2.5 px-4 rounded-lg text-sm font-medium hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-red-500/20 dark:focus:ring-red-400/30"
|
className="w-full mt-2"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
Se déconnecter
|
||||||
<div className="flex items-center justify-center space-x-2">
|
</Button>
|
||||||
<div className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
|
||||||
<span>Déconnexion en cours...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
'Se déconnecter'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cancel Link */}
|
|
||||||
<div className="mt-6 text-center">
|
<div className="mt-6 text-center">
|
||||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">Vous avez changé d'avis ? </span>
|
<span className="text-sm text-neutral-600 dark:text-neutral-400">Vous avez changé d'avis ? </span>
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
className="text-sm text-neutral-900 hover:text-neutral-600 font-medium transition-colors duration-200 dark:text-white dark:hover:text-neutral-300"
|
className="text-sm text-neutral-900 hover:text-neutral-600 font-medium transition-colors duration-200 dark:text-white dark:hover:text-neutral-300"
|
||||||
>
|
>
|
||||||
Retour
|
Retour
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
/**
|
|
||||||
* Register Page Component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { PasswordStrengthIndicator } from '@zen/core/shared/components';
|
import { Card, Input, Button, PasswordStrengthIndicator } from '@zen/core/shared/components';
|
||||||
|
|
||||||
export default function RegisterPage({ onSubmit, onNavigate, currentUser = null }) {
|
export default function RegisterPage({ onSubmit, onNavigate, currentUser = null }) {
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -24,107 +20,50 @@ export default function RegisterPage({ onSubmit, onNavigate, currentUser = null
|
|||||||
setFormLoadedAt(Date.now());
|
setFormLoadedAt(Date.now());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Validation functions
|
|
||||||
const validateEmail = (email) => {
|
const validateEmail = (email) => {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
if (email.length > 254) errors.push('L\'e-mail doit contenir 254 caractères ou moins');
|
||||||
if (email.length > 254) {
|
|
||||||
errors.push('L\'e-mail doit contenir 254 caractères ou moins');
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validatePassword = (password) => {
|
const validatePassword = (password) => {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
if (password.length < 8) errors.push('Le mot de passe doit contenir au moins 8 caractères');
|
||||||
if (password.length < 8) {
|
if (password.length > 128) errors.push('Le mot de passe doit contenir 128 caractères ou moins');
|
||||||
errors.push('Le mot de passe doit contenir au moins 8 caractères');
|
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');
|
||||||
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;
|
return errors;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateName = (name) => {
|
const validateName = (name) => {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
if (name.trim().length === 0) errors.push('Le nom ne peut pas être vide');
|
||||||
if (name.trim().length === 0) {
|
if (name.length > 100) errors.push('Le nom doit contenir 100 caractères ou moins');
|
||||||
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;
|
return errors;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFormValid = () => {
|
const isFormValid = () => {
|
||||||
const emailErrors = validateEmail(formData.email);
|
return validateEmail(formData.email).length === 0 &&
|
||||||
const passwordErrors = validatePassword(formData.password);
|
validatePassword(formData.password).length === 0 &&
|
||||||
const nameErrors = validateName(formData.name);
|
validateName(formData.name).length === 0 &&
|
||||||
|
|
||||||
return emailErrors.length === 0 &&
|
|
||||||
passwordErrors.length === 0 &&
|
|
||||||
nameErrors.length === 0 &&
|
|
||||||
formData.password === formData.confirmPassword &&
|
formData.password === formData.confirmPassword &&
|
||||||
formData.email.trim().length > 0;
|
formData.email.trim().length > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[name]: value
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
setSuccess('');
|
setSuccess('');
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// Frontend validation
|
|
||||||
const emailErrors = validateEmail(formData.email);
|
const emailErrors = validateEmail(formData.email);
|
||||||
const passwordErrors = validatePassword(formData.password);
|
const passwordErrors = validatePassword(formData.password);
|
||||||
const nameErrors = validateName(formData.name);
|
const nameErrors = validateName(formData.name);
|
||||||
|
|
||||||
if (emailErrors.length > 0) {
|
|
||||||
setError(emailErrors[0]); // Show first error
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (passwordErrors.length > 0) {
|
|
||||||
setError(passwordErrors[0]); // Show first error
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nameErrors.length > 0) {
|
|
||||||
setError(nameErrors[0]); // Show first error
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate password match
|
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) {
|
if (formData.password !== formData.confirmPassword) {
|
||||||
setError('Les mots de passe ne correspondent pas');
|
setError('Les mots de passe ne correspondent pas');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -141,7 +80,7 @@ export default function RegisterPage({ onSubmit, onNavigate, currentUser = null
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await onSubmit(submitData);
|
const result = await onSubmit(submitData);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setSuccess(result.message);
|
setSuccess(result.message);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -156,182 +95,140 @@ export default function RegisterPage({ onSubmit, onNavigate, currentUser = null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputClasses = 'w-full px-3 py-2.5 rounded-lg text-sm focus:outline-none transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white/80 dark:bg-[#0B0B0B] backdrop-blur-sm border border-neutral-200/80 dark:border-neutral-800/50 rounded-2xl px-4 py-6 md:px-6 md:py-8 hover:bg-neutral-50/90 hover:border-neutral-300/80 dark:hover:bg-neutral-900/50 dark:hover:border-neutral-700/50 transition-all duration-300 w-full max-w-md">
|
<Card variant="default" padding="md" spacing="none" className="w-full max-w-md">
|
||||||
{/* Header */}
|
<div className="text-center mb-6">
|
||||||
<div className="text-center mb-6">
|
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
Créer un compte
|
||||||
Créer un compte
|
</h1>
|
||||||
</h1>
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
Inscrivez-vous pour commencer.
|
||||||
Inscrivez-vous pour commencer.
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Already Connected Message */}
|
|
||||||
{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 justify-between">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
|
||||||
<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 Message */}
|
|
||||||
{success && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Registration Form */}
|
|
||||||
<form onSubmit={handleSubmit} className={`flex flex-col gap-4 transition-opacity duration-200 ${currentUser ? 'opacity-50 pointer-events-none' : ''}`}>
|
|
||||||
{/* Honeypot — invisible to humans, filled by bots */}
|
|
||||||
<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>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="name" className="block text-xs font-medium text-neutral-700 dark:text-white mb-1.5">
|
|
||||||
Nom complet
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
maxLength="100"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={success || currentUser}
|
|
||||||
className={inputClasses}
|
|
||||||
placeholder="John Doe"
|
|
||||||
autoComplete="name"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-xs font-medium text-neutral-700 dark:text-white mb-1.5">
|
|
||||||
E-mail
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
maxLength="254"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={success || currentUser}
|
|
||||||
className={inputClasses}
|
|
||||||
placeholder="your@email.com"
|
|
||||||
autoComplete="email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-xs font-medium text-neutral-700 dark:text-white mb-1.5">
|
|
||||||
Mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
minLength="8"
|
|
||||||
maxLength="128"
|
|
||||||
value={formData.password}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={success || currentUser}
|
|
||||||
className={inputClasses}
|
|
||||||
placeholder="••••••••"
|
|
||||||
autoComplete="new-password"
|
|
||||||
/>
|
|
||||||
<PasswordStrengthIndicator password={formData.password} showRequirements={true} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="confirmPassword" className="block text-xs font-medium text-neutral-700 dark:text-white mb-1.5">
|
|
||||||
Confirmer le mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="confirmPassword"
|
|
||||||
name="confirmPassword"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
minLength="8"
|
|
||||||
maxLength="128"
|
|
||||||
value={formData.confirmPassword}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={success || currentUser}
|
|
||||||
className={inputClasses}
|
|
||||||
placeholder="••••••••"
|
|
||||||
autoComplete="new-password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading || success || currentUser || !isFormValid()}
|
|
||||||
className="cursor-pointer w-full bg-neutral-900 text-white mt-2 py-2.5 px-4 rounded-lg text-sm font-medium hover:bg-neutral-800 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-neutral-900/20 dark:bg-white dark:text-black dark:hover:bg-neutral-100 dark:focus:ring-white/20"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex items-center justify-center space-x-2">
|
|
||||||
<div className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin dark:border-black/20 dark:border-t-black"></div>
|
|
||||||
<span>Création du compte en cours...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
'Créer un compte'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Login Link */}
|
|
||||||
<div className={`mt-6 text-center transition-opacity duration-200 ${currentUser ? 'opacity-50 pointer-events-none' : ''}`}>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!currentUser) {
|
|
||||||
onNavigate('login');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="group flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">Vous avez déjà un compte ? </span>
|
|
||||||
<span className="text-sm text-neutral-900 group-hover:text-neutral-600 font-medium transition-colors duration-200 dark:text-white dark:group-hover:text-neutral-300">Se connecter</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{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 && (
|
||||||
|
<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 && (
|
||||||
|
<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="John Doe"
|
||||||
|
disabled={!!success || !!currentUser}
|
||||||
|
maxLength="100"
|
||||||
|
autoComplete="name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
label="E-mail"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(value) => setFormData(prev => ({ ...prev, email: value }))}
|
||||||
|
placeholder="your@email.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"
|
||||||
|
loading={isLoading}
|
||||||
|
disabled={!!success || !!currentUser || !isFormValid()}
|
||||||
|
className="w-full mt-2"
|
||||||
|
>
|
||||||
|
Créer un compte
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className={`mt-6 text-center transition-opacity duration-200 ${currentUser ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!currentUser) onNavigate('login');
|
||||||
|
}}
|
||||||
|
className="group flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<span className="text-sm text-neutral-600 dark:text-neutral-400">Vous avez déjà un compte ? </span>
|
||||||
|
<span className="text-sm text-neutral-900 group-hover:text-neutral-600 font-medium transition-colors duration-200 dark:text-white dark:group-hover:text-neutral-300">Se connecter</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +1,38 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset Password Page Component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { PasswordStrengthIndicator } from '@zen/core/shared/components';
|
import { Card, Input, Button, PasswordStrengthIndicator } from '@zen/core/shared/components';
|
||||||
|
|
||||||
export default function ResetPasswordPage({ onSubmit, onNavigate, email, token }) {
|
export default function ResetPasswordPage({ onSubmit, onNavigate, email, token }) {
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({ newPassword: '', confirmPassword: '' });
|
||||||
newPassword: '',
|
|
||||||
confirmPassword: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validation functions
|
|
||||||
const validatePassword = (password) => {
|
const validatePassword = (password) => {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
if (password.length < 8) errors.push('Le mot de passe doit contenir au moins 8 caractères');
|
||||||
if (password.length < 8) {
|
if (password.length > 128) errors.push('Le mot de passe doit contenir 128 caractères ou moins');
|
||||||
errors.push('Le mot de passe doit contenir au moins 8 caractères');
|
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');
|
||||||
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;
|
return errors;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFormValid = () => {
|
const isFormValid = () => {
|
||||||
const passwordErrors = validatePassword(formData.newPassword);
|
return validatePassword(formData.newPassword).length === 0 &&
|
||||||
|
|
||||||
return passwordErrors.length === 0 &&
|
|
||||||
formData.newPassword === formData.confirmPassword &&
|
formData.newPassword === formData.confirmPassword &&
|
||||||
formData.newPassword.length > 0;
|
formData.newPassword.length > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[name]: value
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
setSuccess('');
|
setSuccess('');
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// Frontend validation
|
|
||||||
const passwordErrors = validatePassword(formData.newPassword);
|
const passwordErrors = validatePassword(formData.newPassword);
|
||||||
|
if (passwordErrors.length > 0) { setError(passwordErrors[0]); setIsLoading(false); return; }
|
||||||
if (passwordErrors.length > 0) {
|
|
||||||
setError(passwordErrors[0]); // Show first error
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate password match
|
|
||||||
if (formData.newPassword !== formData.confirmPassword) {
|
if (formData.newPassword !== formData.confirmPassword) {
|
||||||
setError('Les mots de passe ne correspondent pas');
|
setError('Les mots de passe ne correspondent pas');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -89,14 +47,11 @@ export default function ResetPasswordPage({ onSubmit, onNavigate, email, token }
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await onSubmit(submitData);
|
const result = await onSubmit(submitData);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setSuccess(result.message);
|
setSuccess(result.message);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
// Redirect to login after 2 seconds
|
setTimeout(() => onNavigate('login'), 2000);
|
||||||
setTimeout(() => {
|
|
||||||
onNavigate('login');
|
|
||||||
}, 2000);
|
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || 'Échec de la réinitialisation du mot de passe');
|
setError(result.error || 'Échec de la réinitialisation du mot de passe');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -108,115 +63,89 @@ export default function ResetPasswordPage({ onSubmit, onNavigate, email, token }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputClasses = 'w-full px-3 py-2.5 rounded-lg text-sm focus:outline-none transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group bg-white/80 dark:bg-[#0B0B0B] backdrop-blur-sm border border-neutral-200/80 dark:border-neutral-800/50 rounded-2xl px-4 py-6 md:px-6 md:py-8 hover:bg-neutral-50/90 hover:border-neutral-300/80 dark:hover:bg-neutral-900/50 dark:hover:border-neutral-700/50 transition-all duration-300 w-full max-w-md">
|
<Card variant="default" padding="md" spacing="none" className="w-full max-w-md">
|
||||||
{/* Header */}
|
<div className="text-center mb-6">
|
||||||
<div className="text-center mb-6">
|
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
Réinitialiser le mot de passe
|
||||||
Réinitialiser le mot de passe
|
</h1>
|
||||||
</h1>
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
Saisissez votre nouveau mot de passe ci-dessous.
|
||||||
Saisissez votre nouveau mot de passe ci-dessous.
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
{error && (
|
|
||||||
<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 Message */}
|
|
||||||
{success && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Reset Password Form */}
|
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="newPassword" className="block text-xs font-medium text-neutral-700 dark:text-white mb-1.5">
|
|
||||||
Nouveau mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="newPassword"
|
|
||||||
name="newPassword"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
minLength="8"
|
|
||||||
maxLength="128"
|
|
||||||
value={formData.newPassword}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={success}
|
|
||||||
className={inputClasses}
|
|
||||||
placeholder="••••••••"
|
|
||||||
autoComplete="new-password"
|
|
||||||
/>
|
|
||||||
<PasswordStrengthIndicator password={formData.newPassword} showRequirements={true} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="confirmPassword" className="block text-xs font-medium text-neutral-700 dark:text-white mb-1.5">
|
|
||||||
Confirmer le mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="confirmPassword"
|
|
||||||
name="confirmPassword"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
minLength="8"
|
|
||||||
maxLength="128"
|
|
||||||
value={formData.confirmPassword}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={success}
|
|
||||||
className={inputClasses}
|
|
||||||
placeholder="••••••••"
|
|
||||||
autoComplete="new-password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading || success || !isFormValid()}
|
|
||||||
className="cursor-pointer w-full bg-neutral-900 text-white mt-2 py-2.5 px-4 rounded-lg text-sm font-medium hover:bg-neutral-800 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-neutral-900/20 dark:bg-white dark:text-black dark:hover:bg-neutral-100 dark:focus:ring-white/20"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex items-center justify-center space-x-2">
|
|
||||||
<div className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin dark:border-black/20 dark:border-t-black"></div>
|
|
||||||
<span>Réinitialisation...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
'Réinitialiser le mot de passe'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Back to Login Link */}
|
|
||||||
<div className="mt-6 text-center">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onNavigate('login');
|
|
||||||
}}
|
|
||||||
className="text-sm text-neutral-900 hover:text-neutral-600 font-medium transition-colors duration-200 dark:text-white dark:hover:text-neutral-300"
|
|
||||||
>
|
|
||||||
← Retour à la connexion
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<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 && (
|
||||||
|
<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">
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
id="newPassword"
|
||||||
|
name="newPassword"
|
||||||
|
type="password"
|
||||||
|
label="Nouveau mot de passe"
|
||||||
|
value={formData.newPassword}
|
||||||
|
onChange={(value) => setFormData(prev => ({ ...prev, newPassword: value }))}
|
||||||
|
placeholder="••••••••"
|
||||||
|
disabled={!!success}
|
||||||
|
minLength="8"
|
||||||
|
maxLength="128"
|
||||||
|
autoComplete="new-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<PasswordStrengthIndicator password={formData.newPassword} 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}
|
||||||
|
minLength="8"
|
||||||
|
maxLength="128"
|
||||||
|
autoComplete="new-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
loading={isLoading}
|
||||||
|
disabled={!!success || !isFormValid()}
|
||||||
|
className="w-full mt-2"
|
||||||
|
>
|
||||||
|
Réinitialiser le mot de passe
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => { e.preventDefault(); onNavigate('login'); }}
|
||||||
|
className="text-sm text-neutral-900 hover:text-neutral-600 font-medium transition-colors duration-200 dark:text-white dark:hover:text-neutral-300"
|
||||||
|
>
|
||||||
|
← Retour à la connexion
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user