feat(onboarding): réordonne le wizard en compte → préférences → foyer → sources (#129)
All checks were successful
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>
This commit is contained in:
parent
61079b3a2a
commit
2a3fb0434a
10 changed files with 205 additions and 129 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: [] });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/onboarding/allergenes"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<OnboardingAllergensPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/onboarding/foyer"
|
||||
element={
|
||||
|
|
@ -154,14 +164,6 @@ export function App() {
|
|||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/onboarding/allergenes"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<OnboardingAllergensPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/login"
|
||||
element={
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export function SignupPage() {
|
|||
const [formError, setFormError] = useState<string | null>(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);
|
||||
|
|
|
|||
|
|
@ -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<AllergyView[]>([]);
|
||||
const [allergyIds, setAllergyIds] = useState<number[]>([]);
|
||||
const [totalSteps, setTotalSteps] = useState(3);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [formError, setFormError] = useState<string | null>(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() {
|
|||
<main className="onboarding-page">
|
||||
<form className="onboarding-card" onSubmit={handleSubmit} noValidate>
|
||||
<p className="onboarding-step">
|
||||
{t("onboarding.step", { current: totalSteps, total: totalSteps })}
|
||||
{t("onboarding.step", onboardingStepIndicator("allergens"))}
|
||||
</p>
|
||||
<h1>{t("onboarding.allergens.title")}</h1>
|
||||
|
||||
|
|
@ -104,7 +100,7 @@ export function OnboardingAllergensPage() {
|
|||
{formError && <p className="form-error">{formError}</p>}
|
||||
|
||||
<button type="submit" disabled={isSubmitting || isLoading}>
|
||||
{t("onboarding.finish")}
|
||||
{t("onboarding.continue")}
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<main className="onboarding-page">
|
||||
<form className="onboarding-card" onSubmit={handleSubmit} noValidate>
|
||||
<p className="onboarding-step">{t("onboarding.step", { current: 1, total: 3 })}</p>
|
||||
<p className="onboarding-step">{t("onboarding.step", onboardingStepIndicator("regime"))}</p>
|
||||
<h1>{t("onboarding.diet.title")}</h1>
|
||||
|
||||
{isLoading ? (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<main className="onboarding-page">
|
||||
<div className="onboarding-card">
|
||||
<p className="onboarding-step">{t("onboarding.step", { current: 2, total: 3 })}</p>
|
||||
<p className="onboarding-step">
|
||||
{t("onboarding.step", onboardingStepIndicator("household"))}
|
||||
</p>
|
||||
<h1>{t("onboarding.household.title")}</h1>
|
||||
|
||||
{isLoading ? (
|
||||
|
|
@ -70,15 +81,15 @@ export function OnboardingHouseholdPage() {
|
|||
) : house !== null ? (
|
||||
<>
|
||||
<p>{t("onboarding.household.alreadyHasHouse", { name: house.name })}</p>
|
||||
<button type="button" onClick={() => goToNextStep(true)}>
|
||||
{t("onboarding.continue")}
|
||||
<button type="button" onClick={() => goToNextStep(false)}>
|
||||
{t("onboarding.finish")}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="onboarding-subtitle">{t("onboarding.household.subtitle")}</p>
|
||||
<CreateHouseholdForm onDone={() => goToNextStep(true)} />
|
||||
<JoinHouseholdForm onDone={() => goToNextStep(true)} />
|
||||
<JoinHouseholdForm onDone={() => goToNextStep(false)} />
|
||||
<SkipButton onSkip={() => goToNextStep(false)} />
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<main className="onboarding-page">
|
||||
<form className="onboarding-card" onSubmit={handleSubmit} noValidate>
|
||||
<p className="onboarding-step">{t("onboarding.step", { current: 3, total: 4 })}</p>
|
||||
<p className="onboarding-step">
|
||||
{t("onboarding.step", onboardingStepIndicator("sources", { hasSources: true }))}
|
||||
</p>
|
||||
<h1>{t("onboarding.sources.title")}</h1>
|
||||
<p className="onboarding-subtitle">{t("onboarding.sources.subtitle")}</p>
|
||||
|
||||
|
|
@ -93,7 +96,7 @@ export function OnboardingSourcesPage() {
|
|||
{formError && <p className="form-error">{formError}</p>}
|
||||
|
||||
<button type="submit" disabled={isSubmitting || isLoading}>
|
||||
{t("onboarding.continue")}
|
||||
{t("onboarding.finish")}
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
|
|
|
|||
45
apps/web/src/pages/onboarding/onboarding-steps.ts
Normal file
45
apps/web/src/pages/onboarding/onboarding-steps.ts
Normal file
|
|
@ -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<string, readonly OnboardingStep[]>;
|
||||
|
||||
/**
|
||||
* `{ 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 };
|
||||
}
|
||||
|
|
@ -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<br/>(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
|
||||
|
|
|
|||
Loading…
Reference in a new issue