V0.3
This commit is contained in:
+136
-39
@@ -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 été confirmé."
|
||||
return msg
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user