This commit is contained in:
2026-06-30 17:09:52 +02:00
commit 0806be135d
54 changed files with 10482 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
import { useState } from 'react'
import { motion } from 'framer-motion'
import { Gamepad2, Loader2 } from 'lucide-react'
import { supa } from '../lib/supabase'
import { Select } from '../components/ui'
import { MemberRole, ROLE_LABELS } from '../lib/types'
export default function Login(): JSX.Element {
const [mode, setMode] = useState<'signin' | 'signup'>('signin')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [fullName, setFullName] = useState('')
const [role, setRole] = useState<MemberRole>('programmer')
const [busy, setBusy] = useState(false)
const [error, setError] = useState('')
const [info, setInfo] = useState('')
const submit = async (): Promise<void> => {
setError('')
setInfo('')
setBusy(true)
try {
if (mode === 'signup') {
const { error } = await supa().auth.signUp({
email,
password,
options: { data: { full_name: fullName || email.split('@')[0], role } }
})
if (error) throw error
setInfo('Compte créé ! Tu peux maintenant te connecter.')
setMode('signin')
} else {
const { error } = await supa().auth.signInWithPassword({ email, password })
if (error) throw error
}
} 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: 16, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ type: 'spring', stiffness: 260, damping: 24 }}
className="card w-full max-w-sm"
>
<div className="mb-6 flex flex-col items-center gap-2 text-center">
<div className="rounded-xl bg-accent/20 p-3 text-accent">
<Gamepad2 size={28} />
</div>
<h1 className="text-xl font-semibold text-white">GameDev Tracker</h1>
<p className="text-sm text-gray-400">
{mode === 'signin' ? 'Connecte-toi à ton équipe' : 'Crée ton compte membre'}
</p>
</div>
{mode === 'signup' && (
<>
<label className="label">Nom complet</label>
<input
className="input mb-3"
placeholder="Alex Martin"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
/>
<label className="label">Rôle dans l'équipe</label>
<Select
className="mb-3"
value={role}
onChange={(e) => setRole(e.target.value as MemberRole)}
>
{Object.entries(ROLE_LABELS).map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</Select>
</>
)}
<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()}
/>
{error && <p className="mb-3 text-sm text-rose-400">{error}</p>}
{info && <p className="mb-3 text-sm text-accent2">{info}</p>}
<button className="btn-primary w-full" onClick={submit} disabled={busy}>
{busy && <Loader2 className="animate-spin" size={16} />}
{mode === 'signin' ? 'Se connecter' : "Créer le compte"}
</button>
<button
className="mt-4 w-full text-center text-sm text-gray-400 hover:text-white"
onClick={() => {
setMode(mode === 'signin' ? 'signup' : 'signin')
setError('')
setInfo('')
}}
>
{mode === 'signin' ? "Pas encore de compte ? S'inscrire" : 'Déjà un compte ? Se connecter'}
</button>
</motion.div>
</div>
)
}
function translate(msg: string): string {
if (msg.includes('Invalid login')) return 'Email ou mot de passe incorrect.'
if (msg.includes('already registered')) return 'Cet email est déjà utilisé.'
if (msg.includes('at least 6')) return 'Le mot de passe doit faire au moins 6 caractères.'
return msg
}