This commit is contained in:
2026-08-10 22:06:49 +02:00
parent 2a5d9bb0fd
commit 8bff7982ff
83 changed files with 17975 additions and 4824 deletions
+136 -39
View File
@@ -1,21 +1,63 @@
import { useState } from 'react'
import { motion } from 'framer-motion'
import { Loader2 } from 'lucide-react'
import { ArrowLeft, Mail } from 'lucide-react'
import { supa } from '../lib/supabase'
import { useFeedback } from '../lib/feedback'
import { Button, Field, Input } from '../components/ui'
import Logo from '../components/Logo'
/** Traduit les messages d'erreur de Supabase Auth. */
function translate(msg: string): string {
if (/invalid login/i.test(msg)) return 'Email ou mot de passe incorrect.'
if (/email not confirmed/i.test(msg)) return "Cette adresse email n'a pas encore été confirmée."
if (/rate limit|too many/i.test(msg)) return 'Trop de tentatives. Réessaie dans une minute.'
if (/failed to fetch|network/i.test(msg)) {
return 'Impossible de joindre le serveur. Vérifie ta connexion internet.'
}
return msg
}
export default function Login(): JSX.Element {
const { toast } = useFeedback()
const [mode, setMode] = useState<'signin' | 'reset'>('signin')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [busy, setBusy] = useState(false)
const [error, setError] = useState('')
const [sent, setSent] = useState(false)
const submit = async (): Promise<void> => {
const signIn = async (): Promise<void> => {
setError('')
if (!email.trim() || !password) {
setError('Renseigne ton email et ton mot de passe.')
return
}
setBusy(true)
try {
const { error } = await supa().auth.signInWithPassword({ email, password })
if (error) throw error
const { error: e } = await supa().auth.signInWithPassword({
email: email.trim(),
password
})
if (e) throw e
// La bascule vers l'application est pilotée par onAuthStateChange.
} catch (e) {
setError(translate((e as Error).message))
setBusy(false)
}
}
const sendReset = async (): Promise<void> => {
setError('')
if (!email.trim()) {
setError('Indique ton adresse email.')
return
}
setBusy(true)
try {
const { error: e } = await supa().auth.resetPasswordForEmail(email.trim())
if (e) throw e
setSent(true)
toast('Email de réinitialisation envoyé.', 'success')
} catch (e) {
setError(translate((e as Error).message))
} finally {
@@ -26,52 +68,107 @@ export default function Login(): JSX.Element {
return (
<div className="flex h-full items-center justify-center p-6">
<motion.div
initial={{ opacity: 0, y: 16, scale: 0.97 }}
initial={{ opacity: 0, y: 14, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ type: 'spring', stiffness: 260, damping: 24 }}
transition={{ type: 'spring', stiffness: 280, damping: 26 }}
className="card w-full max-w-sm"
>
<div className="mb-6 flex flex-col items-center gap-2 text-center">
<Logo size={56} className="rounded-xl shadow-sm" />
<Logo size={52} className="rounded-xl shadow-card" />
<h1 className="text-xl font-semibold text-ink">GameDev Tracker</h1>
<p className="text-sm text-muted">Connecte-toi à ton équipe</p>
<p className="text-sm text-muted">
{mode === 'signin' ? 'Connecte-toi à ton équipe' : 'Réinitialiser le mot de passe'}
</p>
</div>
<label className="label">Email</label>
<input
className="input mb-3"
type="email"
placeholder="toi@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label className="label">Mot de passe</label>
<input
className="input mb-4"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && submit()}
/>
{/* Un vrai <form> : les gestionnaires de mots de passe fonctionnent,
l'autocomplétion aussi, et Entrée valide naturellement. */}
<form
onSubmit={(e) => {
e.preventDefault()
void (mode === 'signin' ? signIn() : sendReset())
}}
className="space-y-3"
>
<Field label="Email">
{({ id }) => (
<Input
id={id}
type="email"
name="email"
autoComplete="username"
autoFocus
placeholder="toi@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
)}
</Field>
{error && <p className="mb-3 text-sm text-rose-600">{error}</p>}
{mode === 'signin' && (
<Field label="Mot de passe">
{({ id }) => (
<Input
id={id}
type="password"
name="password"
autoComplete="current-password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
)}
</Field>
)}
<button className="btn-primary w-full" onClick={submit} disabled={busy}>
{busy && <Loader2 className="animate-spin" size={16} />}
Se connecter
</button>
{error && (
<p role="alert" className="rounded-lg bg-rose-500/10 px-3 py-2 text-sm text-rose-600 dark:text-rose-300">
{error}
</p>
)}
<p className="mt-4 text-center text-xs text-subtle">
Pas de compte ? Demande à ton administrateur de t'en créer un.
</p>
{sent && mode === 'reset' && (
<p className="flex items-start gap-2 rounded-lg bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700 dark:text-emerald-300">
<Mail size={15} className="mt-0.5 shrink-0" />
Si un compte existe pour cette adresse, un lien de réinitialisation vient d'être envoyé.
</p>
)}
<Button type="submit" variant="primary" full loading={busy}>
{mode === 'signin' ? 'Se connecter' : 'Envoyer le lien'}
</Button>
</form>
{mode === 'signin' ? (
<div className="mt-4 space-y-1.5 text-center">
<button
type="button"
onClick={() => {
setMode('reset')
setError('')
}}
className="text-xs text-accent hover:underline"
>
Mot de passe oublié ?
</button>
<p className="text-xs text-subtle">
Pas de compte ? Demande à ton administrateur de t'en créer un.
</p>
</div>
) : (
<button
type="button"
onClick={() => {
setMode('signin')
setError('')
setSent(false)
}}
className="mx-auto mt-4 flex items-center gap-1 text-xs text-accent hover:underline"
>
<ArrowLeft size={12} /> Revenir à la connexion
</button>
)}
</motion.div>
</div>
)
}
function translate(msg: string): string {
if (msg.includes('Invalid login')) return 'Email ou mot de passe incorrect.'
if (msg.includes('Email not confirmed')) return "L'email n'a pas encore é confirmé."
return msg
}