import { ErrorCode, signupSchema } 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 LoginPage — 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"; /** * Signup form (profile creation). Validates client-side first (via the * shared `signupSchema`, 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 (e.g. email * already taken) into a localized label via {@link ErrorMessageService}. * All static copy comes from i18next (`locales/fr/translation.json`, * `auth.signup` namespace) via {@link useTranslation}, not hardcoded JSX text. */ export function SignupPage() { const { signup } = useAuth(); const navigate = useNavigate(); const { t } = useTranslation(); // Controlled form fields. const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); 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; on success, starts the regime/household/allergens onboarding wizard rather than going straight to the home. */ async function handleSubmit(e: FormEvent) { e.preventDefault(); setFormError(null); const result = signupSchema.safeParse({ firstName, lastName, email, password }); if (!result.success) { setFieldErrors(fieldErrorsFrom(result.error)); return; } setFieldErrors({}); setIsSubmitting(true); try { await signup(result.data); navigate("/onboarding/regime"); } 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.signup.title")}

setFirstName(e.target.value)} autoComplete="given-name" /> {fieldErrors.firstName &&

{fieldErrors.firstName}

} setLastName(e.target.value)} autoComplete="family-name" /> {fieldErrors.lastName &&

{fieldErrors.lastName}

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

{fieldErrors.email}

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

{fieldErrors.password}

} {formError &&

{formError}

}

{t("auth.signup.hasAccount")} {t("auth.signup.loginLink")}

); }