Retour fonctionnel : allergies et intolérances doivent être distinguées dans l'UI, et /foyer doit sauvegarder à la volée plutôt que via des boutons "Enregistrer". - AllergySelect prend un `legend` en prop au lieu d'un libellé interne fixe — le même composant est rendu deux fois par chaque page consommatrice (HouseholdPage, OnboardingAllergensPage), une fois par `kind` (ALLERGY / INTOLERANCE), la sélection restant une seule liste d'IDs partagée. - HouseholdPage : suppression des boutons "Enregistrer", autosave déclenché depuis le handler onChange de chaque champ (jamais un useEffect générique sur la valeur — se déclencherait aussi au chargement initial, sans distinction propre "chargé" vs "modifié"). Nom du foyer et allergènes/intolérances debouncés (600ms/500ms), régime sauvegardé immédiatement (sélection discrète). Validation client (nom vide) empêche l'autosave plutôt que de déclencher un aller-retour API voué à l'échec. - i18n : household.form.allergiesLabel devient "Allergies" (au lieu de "Allergies & intolérances"), nouvelle clé intolerancesLabel, save/ saved remplacés par saving/saved (plus de bouton à libeller). - Cypress (household.cy.ts réécrit, onboarding.cy.ts mis à jour) + specs/frontend-architecture.md + README.md. Vérifié dans le navigateur : wizard d'inscription affiche bien les deux groupes (12 allergies / 2 intolérances) ; /foyer sans aucun bouton, chaque section sauvegarde automatiquement (vérifié en base après édition du nom du foyer et du régime) ; compte de test nettoyé. Clôt le retour fonctionnel sur la feature profil/foyer/régime/ allergènes (8 commits au total sur cette PR).
93 lines
3.2 KiB
TypeScript
93 lines
3.2 KiB
TypeScript
import type { AllergyView } from "@batch-cooking/shared";
|
|
import { ErrorCode } from "@batch-cooking/shared";
|
|
import { type FormEvent, useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { ApiError, apiClient } from "../../api/client";
|
|
import { AllergySelect } from "../../features/profile/AllergySelect";
|
|
import { errorMessageService } from "../../services/error-message.service";
|
|
import "./onboarding.scss";
|
|
|
|
/**
|
|
* Last step of the post-signup onboarding wizard, routed at
|
|
* `/onboarding/allergenes`. Starts from an empty selection — a freshly
|
|
* signed-up profile has none yet, so there's no need for the extra
|
|
* `GET /profile/allergies` round trip a "resume where I left off" flow
|
|
* would require (the `/foyer` settings page, task 10, is the always-fetch
|
|
* source of truth for editing an existing selection later).
|
|
*/
|
|
export function OnboardingAllergensPage() {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
|
|
const [allergies, setAllergies] = useState<AllergyView[]>([]);
|
|
const [allergyIds, setAllergyIds] = useState<number[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [formError, setFormError] = useState<string | null>(null);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
apiClient
|
|
.getAllergies()
|
|
.then((result) => {
|
|
if (!cancelled) setAllergies(result);
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setIsLoading(false);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
/** Finishes the wizard — the profile journey is complete, back to the app itself. */
|
|
async function handleSubmit(e: FormEvent) {
|
|
e.preventDefault();
|
|
setFormError(null);
|
|
setIsSubmitting(true);
|
|
try {
|
|
await apiClient.updateAllergyIds(allergyIds);
|
|
navigate("/");
|
|
} catch (err) {
|
|
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
|
|
setFormError(errorMessageService.getLabel(code));
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="onboarding-page">
|
|
<form className="onboarding-card" onSubmit={handleSubmit} noValidate>
|
|
<p className="onboarding-step">{t("onboarding.step", { current: 3, total: 3 })}</p>
|
|
<h1>{t("onboarding.allergens.title")}</h1>
|
|
|
|
{isLoading ? (
|
|
<p>{t("onboarding.loading")}</p>
|
|
) : (
|
|
<>
|
|
<AllergySelect
|
|
legend={t("household.form.allergiesLabel")}
|
|
allergies={allergies.filter((allergy) => allergy.kind === "ALLERGY")}
|
|
value={allergyIds}
|
|
onChange={setAllergyIds}
|
|
/>
|
|
<AllergySelect
|
|
legend={t("household.form.intolerancesLabel")}
|
|
allergies={allergies.filter((allergy) => allergy.kind === "INTOLERANCE")}
|
|
value={allergyIds}
|
|
onChange={setAllergyIds}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{formError && <p className="form-error">{formError}</p>}
|
|
|
|
<button type="submit" disabled={isSubmitting || isLoading}>
|
|
{t("onboarding.finish")}
|
|
</button>
|
|
</form>
|
|
</main>
|
|
);
|
|
}
|