175 lines
5.7 KiB
TypeScript
175 lines
5.7 KiB
TypeScript
import { useState } from 'react'
|
|
import { motion } from 'framer-motion'
|
|
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 signIn = async (): Promise<void> => {
|
|
setError('')
|
|
if (!email.trim() || !password) {
|
|
setError('Renseigne ton email et ton mot de passe.')
|
|
return
|
|
}
|
|
setBusy(true)
|
|
try {
|
|
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 {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full items-center justify-center p-6">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 14, scale: 0.98 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
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={52} className="rounded-xl shadow-card" />
|
|
<h1 className="text-xl font-semibold text-ink">GameDev Tracker</h1>
|
|
<p className="text-sm text-muted">
|
|
{mode === 'signin' ? 'Connecte-toi à ton équipe' : 'Réinitialiser le mot de passe'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* 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>
|
|
|
|
{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>
|
|
)}
|
|
|
|
{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>
|
|
)}
|
|
|
|
{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>
|
|
)
|
|
}
|