Files
budget-tracker/frontend/src/pages/RegisterPage.tsx
T

188 lines
6.2 KiB
TypeScript

import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { registerUser, loginUser, getMe, setClientTokens } from "../api/client";
import { useAuthStore } from "../stores/auth";
interface FormState {
full_name: string;
email: string;
password: string;
}
interface Errors {
full_name?: string;
email?: string;
password?: string;
global?: string;
}
export default function RegisterPage() {
const navigate = useNavigate();
const login = useAuthStore((s) => s.login);
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const [form, setForm] = useState<FormState>({
full_name: "",
email: "",
password: "",
});
const [errors, setErrors] = useState<Errors>({});
const [isLoading, setIsLoading] = useState(false);
if (isAuthenticated) {
navigate("/", { replace: true });
return null;
}
function validate(): boolean {
const errs: Errors = {};
if (!form.full_name.trim() || form.full_name.trim().length < 2)
errs.full_name = "Nom complet requis (2 caractères minimum)";
if (!form.email) errs.email = "Email requis";
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email))
errs.email = "Email invalide";
if (!form.password || form.password.length < 8)
errs.password = "Mot de passe requis (8 caractères minimum)";
setErrors(errs);
return Object.keys(errs).length === 0;
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!validate()) return;
setIsLoading(true);
setErrors({});
try {
await registerUser(form.email, form.password, form.full_name.trim());
// Auto-login after registration
const tokens = await loginUser(form.email, form.password);
setClientTokens(tokens.access_token, tokens.refresh_token);
const user = await getMe();
login(user, tokens.access_token, tokens.refresh_token);
navigate("/", { replace: true });
} catch (err) {
const isConflict =
err instanceof Error && err.message.toLowerCase().includes("409");
setErrors({
global: isConflict
? "Cet email est déjà utilisé"
: "Erreur lors de l'inscription, veuillez réessayer",
});
} finally {
setIsLoading(false);
}
}
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
<div className="w-full max-w-sm">
<div className="mb-8 text-center">
<h1 className="text-2xl font-bold text-slate-800">Budget Tracker</h1>
<p className="mt-1 text-sm text-slate-500">Créez votre compte</p>
</div>
<div className="rounded-xl bg-white p-6 shadow-sm ring-1 ring-slate-200">
{errors.global && (
<div className="mb-4 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600">
{errors.global}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4" noValidate>
<div>
<label
htmlFor="full_name"
className="mb-1.5 block text-sm font-medium text-slate-700"
>
Nom complet
</label>
<input
id="full_name"
type="text"
autoComplete="name"
value={form.full_name}
onChange={(e) =>
setForm((f) => ({ ...f, full_name: e.target.value }))
}
className={`w-full rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/30 ${
errors.full_name ? "border-red-400" : "border-slate-200"
}`}
placeholder="Jean Dupont"
/>
{errors.full_name && (
<p className="mt-1 text-xs text-red-500">{errors.full_name}</p>
)}
</div>
<div>
<label
htmlFor="email"
className="mb-1.5 block text-sm font-medium text-slate-700"
>
Email
</label>
<input
id="email"
type="email"
autoComplete="email"
value={form.email}
onChange={(e) =>
setForm((f) => ({ ...f, email: e.target.value }))
}
className={`w-full rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/30 ${
errors.email ? "border-red-400" : "border-slate-200"
}`}
placeholder="vous@exemple.com"
/>
{errors.email && (
<p className="mt-1 text-xs text-red-500">{errors.email}</p>
)}
</div>
<div>
<label
htmlFor="password"
className="mb-1.5 block text-sm font-medium text-slate-700"
>
Mot de passe
</label>
<input
id="password"
type="password"
autoComplete="new-password"
value={form.password}
onChange={(e) =>
setForm((f) => ({ ...f, password: e.target.value }))
}
className={`w-full rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary/30 ${
errors.password ? "border-red-400" : "border-slate-200"
}`}
placeholder="8 caractères minimum"
/>
{errors.password && (
<p className="mt-1 text-xs text-red-500">{errors.password}</p>
)}
</div>
<button
type="submit"
disabled={isLoading}
className="w-full rounded-lg bg-primary py-2 text-sm font-medium text-white hover:bg-primary/90 disabled:opacity-50"
>
{isLoading ? "Inscription…" : "Créer mon compte"}
</button>
</form>
</div>
<p className="mt-4 text-center text-sm text-slate-500">
Déjà un compte ?{" "}
<Link to="/login" className="font-medium text-primary hover:underline">
Se connecter
</Link>
</p>
</div>
</div>
);
}