From 2a3fb0434a0b2b6d918472438f9b81c4dddb3561 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 9 Sep 2026 16:34:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(onboarding):=20r=C3=A9ordonne=20le=20wizar?= =?UTF-8?q?d=20en=20compte=20=E2=86=92=20pr=C3=A9f=C3=A9rences=20=E2=86=92?= =?UTF-8?q?=20foyer=20=E2=86=92=20sources=20(#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `` 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 --- apps/web/cypress/e2e/onboarding.feature | 68 +++++++++++-------- .../step_definitions/reference-data.steps.ts | 8 +-- apps/web/src/App.tsx | 26 +++---- apps/web/src/pages/auth/SignupPage.tsx | 2 +- .../onboarding/OnboardingAllergensPage.tsx | 50 +++++++------- .../pages/onboarding/OnboardingDietPage.tsx | 8 ++- .../onboarding/OnboardingHouseholdPage.tsx | 45 +++++++----- .../onboarding/OnboardingSourcesPage.tsx | 33 +++++---- .../src/pages/onboarding/onboarding-steps.ts | 45 ++++++++++++ specs/frontend-architecture.md | 49 +++++++------ 10 files changed, 205 insertions(+), 129 deletions(-) create mode 100644 apps/web/src/pages/onboarding/onboarding-steps.ts diff --git a/apps/web/cypress/e2e/onboarding.feature b/apps/web/cypress/e2e/onboarding.feature index 82efcbe..aecbb99 100644 --- a/apps/web/cypress/e2e/onboarding.feature +++ b/apps/web/cypress/e2e/onboarding.feature @@ -1,41 +1,54 @@ Feature: Onboarding wizard As a newly signed-up user - I want to set my diet, household, and allergens + I want to set my preferences (diet + allergens), then my household So that my profile is ready before I start planning meals + # Order since #129: compte -> régime -> allergènes -> foyer -> sources, + # and the sources step only when the foyer step *created* a household + # (joining one or skipping ends the wizard at the home page). + Background: Given the planning request returns nothing - Scenario: Walks through the wizard, creating a household on the way (which surfaces the sources step), and lands on the home page + Scenario: Walks through every step, creating a household (which surfaces the sources step), and lands on the home page Given the diets reference list has options And selecting the diet will succeed And the household request returns no household - And creating a household will succeed - And the sources reference list is empty And the allergies reference list has options And updating allergies will succeed + And creating a household will succeed + And the sources reference list has options + And the household's enabled sources are empty + And saving the source selection will succeed And I have signed up Then the URL should include "/onboarding/regime" And I should see "Étape 1 sur 3" When I select "Végétarien" from the "diet" field And I click the button "Continuer" Then the diet update request should have been made with diet id 2 - And the URL should include "/onboarding/foyer" - And I should see "Étape 2 sur 3" - When I fill in the "houseName" field with "Chez Alice" - And I click the button "Créer" - Then the household creation request should have been made with name "Chez Alice" And the URL should include "/onboarding/allergenes" - And I should see "Étape 4 sur 4" + And I should see "Étape 2 sur 3" And I should see the section "Allergies" And I should see the section "Intolérances" When I check the checkbox "Arachides" - And I click the button "Terminer" + And I click the button "Continuer" Then the allergies update request should have been made with allergy id 1 + And the URL should include "/onboarding/foyer" + And I should see "Étape 3 sur 3" + When I fill in the "houseName" field with "Chez Alice" + And I click the button "Créer" + Then the household creation request should have been made with name "Chez Alice" + And the URL should include "/onboarding/sources" + And I should see "Étape 4 sur 4" + And I should see the section "Sources disponibles" + And I should see "TheMealDB" + When I check the checkbox "TheMealDB" + And I click the button "Terminer" + Then the source selection update request should have been made with source id 1 And the URL should be the home page And I should see the heading "Planning de la semaine" - Scenario: Lets the regime and allergens steps be skipped without changing anything + Scenario: Lets the preferences steps be skipped without changing anything, then skips the household step too Given the diets reference list is empty And selecting the diet will succeed And the household request returns no household @@ -45,39 +58,32 @@ Feature: Onboarding wizard Then the URL should include "/onboarding/regime" When I click the button "Continuer" Then the diet update request should have been made with no diet id + And the URL should include "/onboarding/allergenes" + When I click the button "Continuer" + Then the allergies update request should have been made with no allergy ids And the URL should include "/onboarding/foyer" When I click the button "Passer cette étape" - Then the URL should include "/onboarding/allergenes" - When I click the button "Terminer" - Then the allergies update request should have been made with no allergy ids - And the URL should be the home page + Then the URL should be the home page - Scenario: Shows and saves the sources step instead of skipping it, when sources are available + Scenario: Creating a household with no sources implemented ends the wizard through the silently-skipped sources step Given the diets reference list is empty And selecting the diet will succeed And the household request returns no household And creating a household will succeed - And the sources reference list has options - And the household's enabled sources are empty - And saving the source selection will succeed + And the sources reference list is empty And the allergies reference list is empty And updating allergies will succeed And I have signed up When I click the button "Continuer" + Then the URL should include "/onboarding/allergenes" + When I click the button "Continuer" Then the URL should include "/onboarding/foyer" When I fill in the "houseName" field with "Chez Alice" And I click the button "Créer" Then the household creation request should have been made with name "Chez Alice" - And the URL should include "/onboarding/sources" - And I should see "Étape 3 sur 4" - And I should see the section "Sources disponibles" - And I should see "TheMealDB" - When I check the checkbox "TheMealDB" - And I click the button "Continuer" - Then the source selection update request should have been made with source id 1 - And the URL should include "/onboarding/allergenes" + And the URL should be the home page - Scenario: Lets the household step be completed by joining an existing household instead of creating one + Scenario: Joining an existing household skips the sources step entirely and ends the wizard Given the diets reference list is empty And selecting the diet will succeed And the household request returns no household @@ -87,8 +93,10 @@ Feature: Onboarding wizard And updating allergies will succeed And I have signed up When I click the button "Continuer" + Then the URL should include "/onboarding/allergenes" + When I click the button "Continuer" Then the URL should include "/onboarding/foyer" When I fill in the "inviteCode" field with "abcd2345" And I click the button "Rejoindre" Then the household join request should have been made with invite code "ABCD2345" - And the URL should include "/onboarding/allergenes" + And the URL should be the home page diff --git a/apps/web/cypress/support/step_definitions/reference-data.steps.ts b/apps/web/cypress/support/step_definitions/reference-data.steps.ts index a457b4a..4788346 100644 --- a/apps/web/cypress/support/step_definitions/reference-data.steps.ts +++ b/apps/web/cypress/support/step_definitions/reference-data.steps.ts @@ -32,10 +32,10 @@ Given("the allergies reference list is empty", () => { cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [] }); }); -// Every onboarding scenario that reaches the household step also reaches -// `/onboarding/sources` right after (when a household got created/joined — -// see `OnboardingHouseholdPage`'s `goToNextStep`), which reads this before -// self-skipping to `/onboarding/allergenes` if it comes back empty. +// An onboarding scenario whose household step *creates* a household then +// reaches `/onboarding/sources` (see `OnboardingHouseholdPage`'s +// `goToNextStep`), which reads this before self-skipping straight to the +// home page if it comes back empty. Given("the sources reference list is empty", () => { cy.intercept("GET", "**/reference/sources", { statusCode: 200, body: [] }); }); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 0390df8..94400c8 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -51,14 +51,16 @@ import { ShoppingListPage } from "./pages/shopping-list/ShoppingListPage"; * just redirects to `/parametres/foyer` so an existing bookmark/link keeps * working. * - * `/onboarding/*` (regime/foyer/sources/allergens, in that order) is also + * `/onboarding/*` (regime/allergenes/foyer/sources, in that order) is also * `RequireAuth`-gated — reached right after signup, once a session already * exists — but deliberately its own top-level route group, *not* nested * under `AppLayout`: a focused, distraction-free wizard with no sidebar, * same full-page-card language as `/login`/`/signup` (see `onboarding.scss`). - * `/onboarding/sources` is conditional — only reached when the `foyer` step - * created/joined a household (see `OnboardingHouseholdPage`'s `goToNextStep`); - * skipped otherwise, straight to `/onboarding/allergenes`. + * `regime` + `allergenes` are the two "préférences" screens; `/onboarding/sources` + * is conditional — only reached when the `foyer` step *created* a household + * (see `OnboardingHouseholdPage`'s `goToNextStep`), every other path ending + * the wizard straight to `/`. Step order and the "sur 3" / "4 sur 4" + * indicators live in `pages/onboarding/onboarding-steps.ts`. * * `/telecharger` is the mobile-app-redirect chantier's fallback landing spot * (see `lib/mobile-app-redirect.ts`'s own doc comment) — public like @@ -138,6 +140,14 @@ export function App() { } /> + + + + } + /> } /> - - - - } - /> (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. */ + /** Validates, then submits the form; on success, starts the régime → allergènes → foyer onboarding wizard rather than going straight to the home. */ async function handleSubmit(e: FormEvent) { e.preventDefault(); setFormError(null); diff --git a/apps/web/src/pages/onboarding/OnboardingAllergensPage.tsx b/apps/web/src/pages/onboarding/OnboardingAllergensPage.tsx index 6052d9f..81fe281 100644 --- a/apps/web/src/pages/onboarding/OnboardingAllergensPage.tsx +++ b/apps/web/src/pages/onboarding/OnboardingAllergensPage.tsx @@ -6,22 +6,23 @@ 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 { onboardingStepIndicator } from "./onboarding-steps"; 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 + * Second step of the post-signup onboarding wizard, routed at + * `/onboarding/allergenes` — the allergens half of "préférences" (régime + * is the first half), now ahead of the household step rather than the + * wizard's final screen (see #129). 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 `/parametres/preferences` settings page is the * always-fetch source of truth for editing an existing selection later). * - * Also fetches the current household on mount, only to know whether the - * `/onboarding/sources` step ran before this one (household step created - * one and it had a source to configure) — the step count shown reflects - * whichever path the visitor actually took, "3 sur 3" or "4 sur 4" - * (there's no shared wizard state to read this from — see - * `OnboardingHouseholdPage`'s doc comment on why each step is independent). + * No household fetch anymore: this step no longer needs to retroactively + * work out whether a sources step ran, since it now comes before the + * household step entirely — the "sur 3" vs "4 sur 4" split lives in + * {@link onboardingStepIndicator}. */ export function OnboardingAllergensPage() { const { t } = useTranslation(); @@ -29,27 +30,22 @@ export function OnboardingAllergensPage() { const [allergies, setAllergies] = useState([]); const [allergyIds, setAllergyIds] = useState([]); - const [totalSteps, setTotalSteps] = useState(3); const [isLoading, setIsLoading] = useState(true); const [formError, setFormError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { let cancelled = false; - Promise.all([apiClient.getAllergies(), apiClient.getCurrentHouse()]) - .then(([allergiesResult, house]) => { - if (cancelled) return; - setAllergies(allergiesResult); - setTotalSteps(house !== null ? 4 : 3); - }) - .catch(() => { - // Neither piece is essential for this last step to render — the - // allergy list just stays empty (nothing to pick from) and the - // step count keeps its 3-of-3 default, same "don't strand the - // visitor over a transient failure" reasoning as - // OnboardingSourcesPage's own catch, just with nothing to - // navigate away to since this already is the last step. + apiClient + .getAllergies() + .then((allergiesResult) => { + if (!cancelled) setAllergies(allergiesResult); }) + // Not essential for this step to render — the allergy list just + // stays empty (nothing to pick from) rather than stranding the + // visitor over a transient failure, same reasoning as + // OnboardingSourcesPage's own catch. + .catch(() => {}) .finally(() => { if (!cancelled) setIsLoading(false); }); @@ -58,14 +54,14 @@ export function OnboardingAllergensPage() { }; }, []); - /** Finishes the wizard — the profile journey is complete, back to the app itself. */ + /** Moves on to the household step — preferences (régime + allergènes) are done. */ async function handleSubmit(e: FormEvent) { e.preventDefault(); setFormError(null); setIsSubmitting(true); try { await apiClient.updateAllergyIds(allergyIds); - void navigate("/"); + void navigate("/onboarding/foyer"); } catch (err) { const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; setFormError(errorMessageService.getLabel(code)); @@ -78,7 +74,7 @@ export function OnboardingAllergensPage() {

- {t("onboarding.step", { current: totalSteps, total: totalSteps })} + {t("onboarding.step", onboardingStepIndicator("allergens"))}

{t("onboarding.allergens.title")}

@@ -104,7 +100,7 @@ export function OnboardingAllergensPage() { {formError &&

{formError}

}
diff --git a/apps/web/src/pages/onboarding/OnboardingDietPage.tsx b/apps/web/src/pages/onboarding/OnboardingDietPage.tsx index e4ca7b8..18c9c9c 100644 --- a/apps/web/src/pages/onboarding/OnboardingDietPage.tsx +++ b/apps/web/src/pages/onboarding/OnboardingDietPage.tsx @@ -7,6 +7,7 @@ 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"; /** @@ -15,6 +16,9 @@ import "./onboarding.scss"; * 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(); @@ -53,7 +57,7 @@ export function OnboardingDietPage() { setIsSubmitting(true); try { await apiClient.updateDiet(dietId); - void navigate("/onboarding/foyer"); + void navigate("/onboarding/allergenes"); } catch (err) { const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; setFormError(errorMessageService.getLabel(code)); @@ -65,7 +69,7 @@ export function OnboardingDietPage() { return (
-

{t("onboarding.step", { current: 1, total: 3 })}

+

{t("onboarding.step", onboardingStepIndicator("regime"))}

{t("onboarding.diet.title")}

{isLoading ? ( diff --git a/apps/web/src/pages/onboarding/OnboardingHouseholdPage.tsx b/apps/web/src/pages/onboarding/OnboardingHouseholdPage.tsx index a332fdf..a8fd8d1 100644 --- a/apps/web/src/pages/onboarding/OnboardingHouseholdPage.tsx +++ b/apps/web/src/pages/onboarding/OnboardingHouseholdPage.tsx @@ -6,16 +6,24 @@ import { ApiError, apiClient } from "../../api/client"; import { HouseNameField } from "../../features/profile/HouseNameField"; import { fieldErrorsFrom } from "../../lib/zod-errors"; import { errorMessageService } from "../../services/error-message.service"; +import { onboardingStepIndicator } from "./onboarding-steps"; import "./onboarding.scss"; /** - * Second step of the post-signup onboarding wizard, routed at - * `/onboarding/foyer` — unlike the regime/allergens steps, this one is - * genuinely optional rather than just "skippable via an unedited default": - * no household is created at signup anymore (see the API's - * `auth.service.ts`), so there's nothing to prefill or implicitly keep by - * pressing "Continuer" — the visitor explicitly creates one, joins one by - * invite code, or skips the step outright via {@link SkipButton}. + * Third step of the post-signup onboarding wizard, routed at + * `/onboarding/foyer` — after the régime + allergènes "préférences" steps + * (see #129). Unlike those, this one is genuinely optional rather than + * just "skippable via an unedited default": no household is created at + * signup anymore (see the API's `auth.service.ts`), so there's nothing to + * prefill or implicitly keep by pressing "Continuer" — the visitor + * explicitly creates one, joins one by invite code, or skips the step + * outright via {@link SkipButton}. + * + * This step's outcome decides whether the wizard has a 4th step: only + * *creating* a household leads on to `/onboarding/sources` (a per-house + * preference to seed while the visitor is here); joining one or skipping + * ends the wizard straight to `/` (a joined household's sources were set + * by whoever created it; a skipped step has no household to attach any to). * * Still checks `getCurrentHouse()` on mount and skips straight to the * "already have one" state if it finds one — defensive against revisiting @@ -50,19 +58,22 @@ export function OnboardingHouseholdPage() { }, []); /** - * A household is what the next step (`/onboarding/sources`) needs to - * attach a preference to — created/joined/already-there goes on to it, - * skipped goes straight to `/onboarding/allergenes` instead (nothing to - * configure sources for without a household). + * `/onboarding/sources` only makes sense for a household the visitor just + * *created* — that's the sole caller passing `true`. Joining one, skipping + * the step, or finding one already there (browser back — can't tell + * created from joined, and the sources step self-skips when there's + * nothing to configure anyway) all end the wizard at `/`. */ - function goToNextStep(hasHousehold: boolean) { - void navigate(hasHousehold ? "/onboarding/sources" : "/onboarding/allergenes"); + function goToNextStep(createdHousehold: boolean) { + void navigate(createdHousehold ? "/onboarding/sources" : "/"); } return (
-

{t("onboarding.step", { current: 2, total: 3 })}

+

+ {t("onboarding.step", onboardingStepIndicator("household"))} +

{t("onboarding.household.title")}

{isLoading ? ( @@ -70,15 +81,15 @@ export function OnboardingHouseholdPage() { ) : house !== null ? ( <>

{t("onboarding.household.alreadyHasHouse", { name: house.name })}

- ) : ( <>

{t("onboarding.household.subtitle")}

goToNextStep(true)} /> - goToNextStep(true)} /> + goToNextStep(false)} /> goToNextStep(false)} /> )} diff --git a/apps/web/src/pages/onboarding/OnboardingSourcesPage.tsx b/apps/web/src/pages/onboarding/OnboardingSourcesPage.tsx index 973a4f8..c365ad2 100644 --- a/apps/web/src/pages/onboarding/OnboardingSourcesPage.tsx +++ b/apps/web/src/pages/onboarding/OnboardingSourcesPage.tsx @@ -6,22 +6,23 @@ import { useNavigate } from "react-router-dom"; import { ApiError, apiClient } from "../../api/client"; import { SourceSelect } from "../../features/house/SourceSelect"; import { errorMessageService } from "../../services/error-message.service"; +import { onboardingStepIndicator } from "./onboarding-steps"; import "./onboarding.scss"; /** - * Third step of the post-signup onboarding wizard, routed at - * `/onboarding/sources` — only reached when a household now exists (see - * `OnboardingHouseholdPage`'s `goToStep`, which skips straight to - * `/onboarding/allergenes` when the previous step was skipped): enabled - * sources are a per-household preference (`HouseSource`, schema.prisma), - * so there's nothing to attach a selection to without one. + * Final step of the post-signup onboarding wizard, routed at + * `/onboarding/sources` — reached only when the previous step *created* a + * household (see `OnboardingHouseholdPage`'s `goToNextStep`, which ends the + * wizard at `/` on every other path): enabled sources are a per-household + * preference (`HouseSource`, schema.prisma), so there's nothing to attach + * a selection to otherwise. * - * Starts from an empty selection — a newly created/joined household has - * nothing enabled yet by design (opt-in, see `house.service.ts`'s + * Starts from an empty selection — a newly created household has nothing + * enabled yet by design (opt-in, see `house.service.ts`'s * `getHouseSourceIds`). If no source is implemented yet (`getSources()` * comes back empty — currently always the case), there's nothing to pick - * from either, so this step silently steps itself over rather than showing - * an empty screen. + * from either, so this step silently finishes the wizard (straight to `/`) + * rather than showing an empty screen. */ export function OnboardingSourcesPage() { const { t } = useTranslation(); @@ -40,7 +41,7 @@ export function OnboardingSourcesPage() { .then((result) => { if (cancelled) return; if (result.length === 0) { - void navigate("/onboarding/allergenes", { replace: true }); + void navigate("/", { replace: true }); return; } setSources(result); @@ -50,7 +51,7 @@ export function OnboardingSourcesPage() { // Nothing to configure sources for if we can't even list them — the // wizard shouldn't strand the visitor here over a transient failure // fetching an optional step's own data. - if (!cancelled) void navigate("/onboarding/allergenes", { replace: true }); + if (!cancelled) void navigate("/", { replace: true }); }); return () => { cancelled = true; @@ -63,7 +64,7 @@ export function OnboardingSourcesPage() { setIsSubmitting(true); try { await apiClient.updateHouseSourceIds(sourceIds); - void navigate("/onboarding/allergenes"); + void navigate("/"); } catch (err) { const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR; setFormError(errorMessageService.getLabel(code)); @@ -75,7 +76,9 @@ export function OnboardingSourcesPage() { return (
-

{t("onboarding.step", { current: 3, total: 4 })}

+

+ {t("onboarding.step", onboardingStepIndicator("sources", { hasSources: true }))} +

{t("onboarding.sources.title")}

{t("onboarding.sources.subtitle")}

@@ -93,7 +96,7 @@ export function OnboardingSourcesPage() { {formError &&

{formError}

}
diff --git a/apps/web/src/pages/onboarding/onboarding-steps.ts b/apps/web/src/pages/onboarding/onboarding-steps.ts new file mode 100644 index 0000000..8908be4 --- /dev/null +++ b/apps/web/src/pages/onboarding/onboarding-steps.ts @@ -0,0 +1,45 @@ +/** + * The post-signup onboarding wizard's step order, in one place so the + * per-page "Étape N sur M" indicators can't drift out of sync when the + * sequence changes. + * + * Order: régime → allergènes → foyer → [sources]. `"sources"` is a + * conditional 4th step, reached only when the visitor *creates* a + * household on the foyer step — joining one or skipping the step ends the + * wizard at foyer (a joined household's sources were already configured by + * whoever created it, and there's nothing to attach a source selection to + * without a household at all). + * + * Each page renders its own indicator with no shared runtime wizard state + * to read from (see {@link OnboardingHouseholdPage}'s doc on why the steps + * are independent), so régime/allergènes/foyer — all upstream of the + * "create vs join/skip" decision — can't yet know whether a 4th step is + * coming and show "sur 3"; only the `"sources"` step, which by definition + * is only ever reached on the 4-step path, shows "4 sur 4". This mirrors + * the pre-#129 flow, whose last step likewise only learned the real total + * after the fact. + */ +export type OnboardingStep = "regime" | "allergens" | "household" | "sources"; + +/** The wizard's two possible step sequences, keyed by whether the sources step runs. */ +export const ONBOARDING_STEP_SEQUENCE = { + /** régime → allergènes → foyer: household joined or skipped (or still upstream of that choice). */ + withoutSources: ["regime", "allergens", "household"], + /** régime → allergènes → foyer → sources: the visitor created a household. */ + withSources: ["regime", "allergens", "household", "sources"], +} as const satisfies Record; + +/** + * `{ current, total }` to feed `t("onboarding.step", …)` for a given step. + * Defaults to the 3-step sequence; the `"sources"` step passes + * `hasSources: true` for its "4 sur 4". + */ +export function onboardingStepIndicator( + step: OnboardingStep, + { hasSources = false }: { hasSources?: boolean } = {}, +): { current: number; total: number } { + const sequence: readonly OnboardingStep[] = hasSources + ? ONBOARDING_STEP_SEQUENCE.withSources + : ONBOARDING_STEP_SEQUENCE.withoutSources; + return { current: sequence.indexOf(step) + 1, total: sequence.length }; +} diff --git a/specs/frontend-architecture.md b/specs/frontend-architecture.md index f892440..dc97f85 100644 --- a/specs/frontend-architecture.md +++ b/specs/frontend-architecture.md @@ -71,7 +71,7 @@ apps/web/src/ │ │ ├── AccountSettingsPage.tsx / HouseholdSettingsPage.tsx / PreferencesPage.tsx │ │ ├── UserPreferencesPage.tsx / CreditsPage.tsx │ │ └── settings-pages.scss -│ └── onboarding/ # wizard d'inscription (régime → foyer → [sources] → allergènes), voir plus bas +│ └── onboarding/ # wizard d'inscription (régime → allergènes → foyer → [sources]), voir plus bas │ ├── OnboardingDietPage.tsx / OnboardingHouseholdPage.tsx / OnboardingSourcesPage.tsx / OnboardingAllergensPage.tsx │ └── onboarding.scss ├── styles/ @@ -156,9 +156,11 @@ flowchart TB l'ancienne page combinée) redirige vers `/parametres/foyer` pour ne pas casser un lien existant. - **`/onboarding/*`** reste son propre groupe de routes top-level (pas nichées - sous `AppLayout` — wizard plein écran sans sidebar) : `regime` → `foyer` → - `sources` (conditionnelle — seulement si l'étape `foyer` a créé/rejoint un - foyer, sinon on saute directement à l'étape suivante) → `allergenes`. + sous `AppLayout` — wizard plein écran sans sidebar) : `regime` → `allergenes` + (les deux « préférences ») → `foyer` → `sources` (conditionnelle — seulement + si l'étape `foyer` a **créé** un foyer ; rejoindre/passer termine le wizard + vers `/`). L'ordre et les compteurs « sur 3 » / « 4 sur 4 » vivent dans + `pages/onboarding/onboarding-steps.ts`. --- @@ -226,11 +228,11 @@ Deux surfaces partagent les mêmes composants de champ (`features/profile/` + ```mermaid flowchart LR SIGNUP["SignupPage
(POST /auth/signup)"] --> OB1["/onboarding/regime"] - OB1 --> OB2["/onboarding/foyer"] - OB2 -->|"foyer créé/rejoint"| OB3["/onboarding/sources"] - OB2 -->|"pas de foyer"| OB4["/onboarding/allergenes"] - OB3 --> OB4 - OB4 --> HOME["/ (PlanningPage)"] + OB1 --> OB2["/onboarding/allergenes"] + OB2 --> OB3["/onboarding/foyer"] + OB3 -->|"foyer créé"| OB4["/onboarding/sources"] + OB3 -->|"rejoint / passé"| HOME["/ (PlanningPage)"] + OB4 --> HOME SETTINGSMENU["Sidebar : Paramètres"] --> S1["/parametres/compte"] SETTINGSMENU --> S2["/parametres/preferences"] @@ -240,18 +242,23 @@ flowchart LR SETTINGSMENU --> S6["/parametres/credits"] ``` -- **`pages/onboarding/`** — wizard de **4 écrans** (régime → foyer → sources → - allergènes), lancé une seule fois juste après l'inscription. Routes - top-level `RequireAuth`, **pas** nichées sous `AppLayout` : wizard plein - écran sans sidebar (`onboarding.scss`, même langage visuel que - `/login`/`/signup`). Chaque étape a un unique bouton "Continuer" qui envoie - la valeur courante — pas de bouton "Passer" séparé, une valeur - "aucune"/vide *est* le skip. `/onboarding/sources` est **conditionnelle** : - seulement atteinte si l'étape `foyer` vient de créer/rejoindre un foyer - (`OnboardingHouseholdPage`'s `goToNextStep`) — sautée sinon, directement vers - `/onboarding/allergenes`. `OnboardingSourcesPage` elle-même redirige en - silence vers l'étape suivante si `GET /reference/sources` revient vide ou en - erreur, plutôt que d'afficher une étape sans rien à choisir. +- **`pages/onboarding/`** — wizard de **3 écrans** (régime → allergènes → + foyer), + un **4ᵉ conditionnel** (sources), lancé une seule fois juste après + l'inscription. Routes top-level `RequireAuth`, **pas** nichées sous + `AppLayout` : wizard plein écran sans sidebar (`onboarding.scss`, même + langage visuel que `/login`/`/signup`). Régime et allergènes forment les + deux « préférences » ; chacun a un unique bouton "Continuer" qui envoie la + valeur courante — une valeur "aucune"/vide *est* le skip. L'étape `foyer`, + elle, est explicitement optionnelle (créer / rejoindre / "Passer cette + étape"). `/onboarding/sources` est **conditionnelle** : seulement atteinte + si l'étape `foyer` vient de **créer** un foyer + (`OnboardingHouseholdPage`'s `goToNextStep`) — rejoindre ou passer termine + le wizard vers `/`. `OnboardingSourcesPage` elle-même redirige en silence + vers `/` si `GET /reference/sources` revient vide ou en erreur, plutôt que + d'afficher une étape sans rien à choisir. L'ordre des étapes et les + compteurs affichés (« Étape 1 sur 3 » … « 4 sur 4 » pour le chemin avec + sources) sont centralisés dans `pages/onboarding/onboarding-steps.ts` — + chaque page les lisait auparavant en dur, ce qu'un réordonnancement cassait. - **Pages `/parametres/*`** (`pages/settings/`, dans `AppLayout`) — l'ancienne `HouseholdPage` combinée a été **éclatée en 5 pages** dédiées (voir la section suivante pour le détail de chacune), toutes en **hot saving** (pas