batchCooking/apps/web/src/pages/onboarding/OnboardingDietPage.tsx
Nicolas 2a3fb0434a
All checks were successful
CI / lint (push) Successful in 3m54s
CI / build (push) Successful in 4m10s
CI / intent-service-test (push) Successful in 5m15s
CI / e2e (push) Successful in 8m32s
CI / test (push) Successful in 23m8s
CI / build-android (push) Has been skipped
feat(onboarding): réordonne le wizard en compte → préférences → foyer → sources (#129)
Nouvel ordre : régime → allergènes (les deux « préférences ») → foyer →
sources. L'étape sources devient la dernière et n'est atteinte que si
l'étape foyer vient de **créer** un foyer — rejoindre ou passer termine le
wizard vers `/` (un foyer rejoint a déjà ses sources ; un foyer passé n'a
rien à quoi les rattacher).

- `onboarding-steps.ts` (nouveau) : séquence des étapes + `onboardingStepIndicator()`
  en un seul endroit. Les compteurs « Étape N sur M » étaient codés en dur
  page par page, ce qu'un réordonnancement cassait. régime/allergènes/foyer
  sont en amont du choix « créer vs rejoindre/passer » donc affichent « sur 3 » ;
  seule l'étape sources, jamais atteinte hors du chemin à 4 étapes, affiche « 4 sur 4 ».
- `OnboardingDietPage` → enchaîne sur `/onboarding/allergenes`.
- `OnboardingAllergensPage` → enchaîne sur `/onboarding/foyer` ; bouton « Continuer »
  (plus « Terminer ») ; suppression du `getCurrentHouse()` + `totalSteps` dynamique,
  devenus inutiles maintenant que cette étape précède le foyer.
- `OnboardingHouseholdPage` → `goToNextStep(createdHousehold)` : `/onboarding/sources`
  seulement si créé, sinon `/`. `JoinHouseholdForm`/`SkipButton`/branche défensive
  « déjà un foyer » terminent vers `/`.
- `OnboardingSourcesPage` → dernière étape : submit, court-circuit « aucune source »
  et catch vont tous vers `/` ; bouton « Terminer ».
- `App.tsx` : blocs `<Route>` réordonnés + commentaire de doc du groupe.
- `specs/frontend-architecture.md` : ordre, diagramme mermaid, condition sources.

Tests : `onboarding.feature` réécrit — 4 parcours (walk-through complet avec
foyer créé + sources ; préférences + foyer passés ; foyer créé sans source
implémentée → étape sources auto-sautée ; foyer rejoint → pas de sources),
compteurs vérifiés sur chaque chemin. `pnpm lint` / `pnpm build` verts ;
Cypress local : onboarding 4/4, régression auth/preferences/household-settings 16/16.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 16:34:49 +02:00

89 lines
3.2 KiB
TypeScript

import type { DietView } 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 { useAuth } from "../../features/auth/AuthContext";
import { DietSelect } from "../../features/profile/DietSelect";
import { errorMessageService } from "../../services/error-message.service";
import { onboardingStepIndicator } from "./onboarding-steps";
import "./onboarding.scss";
/**
* First step of the post-signup onboarding wizard, routed at
* `/onboarding/regime`. Initial selection comes from `useAuth()`'s
* already-loaded profile (`user.dietId`) — freshly signed-up, this is
* `null` — no extra fetch needed just to know the starting value, unlike
* the household step which has no such shortcut for the current name.
*
* Followed by the allergens step (both make up "préférences" — see #129);
* the household step comes after those, not between them.
*/
export function OnboardingDietPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const { user } = useAuth();
const [diets, setDiets] = useState<DietView[]>([]);
const [dietId, setDietId] = useState<number | null>(user?.dietId ?? null);
const [isLoading, setIsLoading] = useState(true);
const [formError, setFormError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
let cancelled = false;
apiClient
.getDiets()
.then((result) => {
if (!cancelled) setDiets(result);
})
// Not essential for this step to render — the diet list just stays
// empty (nothing to pick, "Aucun régime particulier" still works)
// rather than stranding the visitor over a transient failure, same
// reasoning as OnboardingSourcesPage's own catch.
.catch(() => {})
.finally(() => {
if (!cancelled) setIsLoading(false);
});
return () => {
cancelled = true;
};
}, []);
async function handleSubmit(e: FormEvent) {
e.preventDefault();
setFormError(null);
setIsSubmitting(true);
try {
await apiClient.updateDiet(dietId);
void navigate("/onboarding/allergenes");
} 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", onboardingStepIndicator("regime"))}</p>
<h1>{t("onboarding.diet.title")}</h1>
{isLoading ? (
<p>{t("onboarding.loading")}</p>
) : (
<DietSelect diets={diets} value={dietId} onChange={setDietId} />
)}
{formError && <p className="form-error">{formError}</p>}
<button type="submit" disabled={isSubmitting || isLoading}>
{t("onboarding.continue")}
</button>
</form>
</main>
);
}