refactor: reorganize feature modules with consistent naming conventions and flattened structure

This commit is contained in:
2026-04-22 14:12:15 -04:00
parent 256df9102c
commit 61388f04a6
41 changed files with 0 additions and 824 deletions
+228
View File
@@ -0,0 +1,228 @@
'use client';
/**
* Login Page Component
*/
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
export default function LoginPage({ onSubmit, onNavigate, onSetSessionCookie, redirectAfterLogin = '/', currentUser = null }) {
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [honeypot, setHoneypot] = useState('');
const [formLoadedAt, setFormLoadedAt] = useState(0);
const router = useRouter();
useEffect(() => {
setFormLoadedAt(Date.now());
}, []);
// If already logged in, redirect to redirectAfterLogin
useEffect(() => {
if (currentUser) {
router.replace(redirectAfterLogin);
}
}, [currentUser, redirectAfterLogin, router]);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !isLoading && !success) {
handleSubmit();
}
};
const handleSubmit = async () => {
setError('');
setSuccess('');
setIsLoading(true);
const submitData = new FormData();
submitData.append('email', formData.email);
submitData.append('password', formData.password);
submitData.append('_hp', honeypot);
submitData.append('_t', String(formLoadedAt));
try {
const result = await onSubmit(submitData);
if (result.success) {
const successMsg = result.message || 'Connexion réussie ! Redirection...';
// Display success message immediately (no page refresh because we didn't set cookie yet)
setSuccess(successMsg);
setIsLoading(false);
// Wait for user to see the success message
setTimeout(async () => {
// Now set the session cookie (this might cause a refresh, but we're redirecting anyway)
if (result.sessionToken && onSetSessionCookie) {
await onSetSessionCookie(result.sessionToken);
}
// Then navigate
router.push(redirectAfterLogin);
}, 1500);
} else {
setError(result.error || 'Échec de la connexion');
setIsLoading(false);
}
} catch (err) {
console.error('Login error:', err);
setError('Une erreur inattendue s\'est produite');
setIsLoading(false);
}
};
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 (
<div className="bg-white dark:bg-neutral-900/40 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">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
Connexion
</h1>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Veuillez vous connecter pour continuer.
</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>
);
}