import { ErrorCode, loginSchema } from "@batch-cooking/shared"; import { type FormEvent, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; import { ApiError } from "../api/client"; import { useAuth } from "../features/auth/AuthContext"; // Shared with SignupPage — see the file for why it's colocated in // features/auth/ rather than duplicated per page. import "../features/auth/auth-form.scss"; import { fieldErrorsFrom } from "../lib/zod-errors"; import { errorMessageService } from "../services/error-message.service"; /** * Login form. Validates client-side first (via the shared `loginSchema`, * same rules the API enforces) for instant feedback with no network round * trip; only calls the API once the payload is locally valid, and * translates any API failure into a localized label via * {@link ErrorMessageService}. All static copy comes from i18next * (`locales/fr/translation.json`, `auth.login` namespace) via * {@link useTranslation}, not hardcoded JSX text. */ export function LoginPage() { const { login } = useAuth(); const navigate = useNavigate(); const { t } = useTranslation(); // Controlled form fields. const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); // Per-field validation messages (client-side, from zod) and a whole-form // error message (from the API), kept separate since they're displayed // in different places and cleared at different times. const [fieldErrors, setFieldErrors] = useState>({}); const [formError, setFormError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); /** Validates, then submits the form; navigates home on success. */ async function handleSubmit(e: FormEvent) { e.preventDefault(); setFormError(null); const result = loginSchema.safeParse({ email, password }); if (!result.success) { setFieldErrors(fieldErrorsFrom(result.error)); return; } setFieldErrors({}); setIsSubmitting(true); try { await login(result.data); navigate("/"); } catch (err) { // ApiError.code is looked up through ErrorMessageService so the // label is centralized and localized — never display err.message // directly, it's the API's developer-facing (English) text. const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; setFormError(errorMessageService.getLabel(code)); } finally { setIsSubmitting(false); } } return (

{t("auth.login.title")}

setEmail(e.target.value)} autoComplete="email" /> {fieldErrors.email &&

{fieldErrors.email}

} setPassword(e.target.value)} autoComplete="current-password" /> {fieldErrors.password &&

{fieldErrors.password}

} {formError &&

{formError}

}

{t("auth.login.noAccount")} {t("auth.login.createProfileLink")}

); }