All checks were successful
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>
89 lines
3.2 KiB
TypeScript
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>
|
|
);
|
|
}
|