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
|
Feature: Onboarding wizard
|
||||||
As a newly signed-up user
|
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
|
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:
|
Background:
|
||||||
Given the planning request returns nothing
|
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
|
Given the diets reference list has options
|
||||||
And selecting the diet will succeed
|
And selecting the diet will succeed
|
||||||
And the household request returns no household
|
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 the allergies reference list has options
|
||||||
And updating allergies will succeed
|
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
|
And I have signed up
|
||||||
Then the URL should include "/onboarding/regime"
|
Then the URL should include "/onboarding/regime"
|
||||||
And I should see "Étape 1 sur 3"
|
And I should see "Étape 1 sur 3"
|
||||||
When I select "Végétarien" from the "diet" field
|
When I select "Végétarien" from the "diet" field
|
||||||
And I click the button "Continuer"
|
And I click the button "Continuer"
|
||||||
Then the diet update request should have been made with diet id 2
|
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 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 "Allergies"
|
||||||
And I should see the section "Intolérances"
|
And I should see the section "Intolérances"
|
||||||
When I check the checkbox "Arachides"
|
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
|
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 the URL should be the home page
|
||||||
And I should see the heading "Planning de la semaine"
|
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
|
Given the diets reference list is empty
|
||||||
And selecting the diet will succeed
|
And selecting the diet will succeed
|
||||||
And the household request returns no household
|
And the household request returns no household
|
||||||
|
|
@ -45,39 +58,32 @@ Feature: Onboarding wizard
|
||||||
Then the URL should include "/onboarding/regime"
|
Then the URL should include "/onboarding/regime"
|
||||||
When I click the button "Continuer"
|
When I click the button "Continuer"
|
||||||
Then the diet update request should have been made with no diet id
|
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"
|
And the URL should include "/onboarding/foyer"
|
||||||
When I click the button "Passer cette étape"
|
When I click the button "Passer cette étape"
|
||||||
Then the URL should include "/onboarding/allergenes"
|
Then the URL should be the home page
|
||||||
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
|
|
||||||
|
|
||||||
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
|
Given the diets reference list is empty
|
||||||
And selecting the diet will succeed
|
And selecting the diet will succeed
|
||||||
And the household request returns no household
|
And the household request returns no household
|
||||||
And creating a household will succeed
|
And creating a household will succeed
|
||||||
And the sources reference list has options
|
And the sources reference list is empty
|
||||||
And the household's enabled sources are empty
|
|
||||||
And saving the source selection will succeed
|
|
||||||
And the allergies reference list is empty
|
And the allergies reference list is empty
|
||||||
And updating allergies will succeed
|
And updating allergies will succeed
|
||||||
And I have signed up
|
And I have signed up
|
||||||
When I click the button "Continuer"
|
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"
|
Then the URL should include "/onboarding/foyer"
|
||||||
When I fill in the "houseName" field with "Chez Alice"
|
When I fill in the "houseName" field with "Chez Alice"
|
||||||
And I click the button "Créer"
|
And I click the button "Créer"
|
||||||
Then the household creation request should have been made with name "Chez Alice"
|
Then the household creation request should have been made with name "Chez Alice"
|
||||||
And the URL should include "/onboarding/sources"
|
And the URL should be the home page
|
||||||
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"
|
|
||||||
|
|
||||||
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
|
Given the diets reference list is empty
|
||||||
And selecting the diet will succeed
|
And selecting the diet will succeed
|
||||||
And the household request returns no household
|
And the household request returns no household
|
||||||
|
|
@ -87,8 +93,10 @@ Feature: Onboarding wizard
|
||||||
And updating allergies will succeed
|
And updating allergies will succeed
|
||||||
And I have signed up
|
And I have signed up
|
||||||
When I click the button "Continuer"
|
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"
|
Then the URL should include "/onboarding/foyer"
|
||||||
When I fill in the "inviteCode" field with "abcd2345"
|
When I fill in the "inviteCode" field with "abcd2345"
|
||||||
And I click the button "Rejoindre"
|
And I click the button "Rejoindre"
|
||||||
Then the household join request should have been made with invite code "ABCD2345"
|
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: [] });
|
cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Every onboarding scenario that reaches the household step also reaches
|
// An onboarding scenario whose household step *creates* a household then
|
||||||
// `/onboarding/sources` right after (when a household got created/joined —
|
// reaches `/onboarding/sources` (see `OnboardingHouseholdPage`'s
|
||||||
// see `OnboardingHouseholdPage`'s `goToNextStep`), which reads this before
|
// `goToNextStep`), which reads this before self-skipping straight to the
|
||||||
// self-skipping to `/onboarding/allergenes` if it comes back empty.
|
// home page if it comes back empty.
|
||||||
Given("the sources reference list is empty", () => {
|
Given("the sources reference list is empty", () => {
|
||||||
cy.intercept("GET", "**/reference/sources", { statusCode: 200, body: [] });
|
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
|
* just redirects to `/parametres/foyer` so an existing bookmark/link keeps
|
||||||
* working.
|
* 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
|
* `RequireAuth`-gated — reached right after signup, once a session already
|
||||||
* exists — but deliberately its own top-level route group, *not* nested
|
* exists — but deliberately its own top-level route group, *not* nested
|
||||||
* under `AppLayout`: a focused, distraction-free wizard with no sidebar,
|
* under `AppLayout`: a focused, distraction-free wizard with no sidebar,
|
||||||
* same full-page-card language as `/login`/`/signup` (see `onboarding.scss`).
|
* same full-page-card language as `/login`/`/signup` (see `onboarding.scss`).
|
||||||
* `/onboarding/sources` is conditional — only reached when the `foyer` step
|
* `regime` + `allergenes` are the two "préférences" screens; `/onboarding/sources`
|
||||||
* created/joined a household (see `OnboardingHouseholdPage`'s `goToNextStep`);
|
* is conditional — only reached when the `foyer` step *created* a household
|
||||||
* skipped otherwise, straight to `/onboarding/allergenes`.
|
* (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
|
* `/telecharger` is the mobile-app-redirect chantier's fallback landing spot
|
||||||
* (see `lib/mobile-app-redirect.ts`'s own doc comment) — public like
|
* (see `lib/mobile-app-redirect.ts`'s own doc comment) — public like
|
||||||
|
|
@ -138,6 +140,14 @@ export function App() {
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/onboarding/allergenes"
|
||||||
|
element={
|
||||||
|
<RequireAuth>
|
||||||
|
<OnboardingAllergensPage />
|
||||||
|
</RequireAuth>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/onboarding/foyer"
|
path="/onboarding/foyer"
|
||||||
element={
|
element={
|
||||||
|
|
@ -154,14 +164,6 @@ export function App() {
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path="/onboarding/allergenes"
|
|
||||||
element={
|
|
||||||
<RequireAuth>
|
|
||||||
<OnboardingAllergensPage />
|
|
||||||
</RequireAuth>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/login"
|
path="/login"
|
||||||
element={
|
element={
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export function SignupPage() {
|
||||||
const [formError, setFormError] = useState<string | null>(null);
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
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) {
|
async function handleSubmit(e: FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
|
|
|
||||||
|
|
@ -6,22 +6,23 @@ import { useNavigate } from "react-router-dom";
|
||||||
import { ApiError, apiClient } from "../../api/client";
|
import { ApiError, apiClient } from "../../api/client";
|
||||||
import { AllergySelect } from "../../features/profile/AllergySelect";
|
import { AllergySelect } from "../../features/profile/AllergySelect";
|
||||||
import { errorMessageService } from "../../services/error-message.service";
|
import { errorMessageService } from "../../services/error-message.service";
|
||||||
|
import { onboardingStepIndicator } from "./onboarding-steps";
|
||||||
import "./onboarding.scss";
|
import "./onboarding.scss";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last step of the post-signup onboarding wizard, routed at
|
* Second step of the post-signup onboarding wizard, routed at
|
||||||
* `/onboarding/allergenes`. Starts from an empty selection — a freshly
|
* `/onboarding/allergenes` — the allergens half of "préférences" (régime
|
||||||
* signed-up profile has none yet, so there's no need for the extra
|
* 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
|
* `GET /profile/allergies` round trip a "resume where I left off" flow
|
||||||
* would require (the `/parametres/preferences` settings page is the
|
* would require (the `/parametres/preferences` settings page is the
|
||||||
* always-fetch source of truth for editing an existing selection later).
|
* always-fetch source of truth for editing an existing selection later).
|
||||||
*
|
*
|
||||||
* Also fetches the current household on mount, only to know whether the
|
* No household fetch anymore: this step no longer needs to retroactively
|
||||||
* `/onboarding/sources` step ran before this one (household step created
|
* work out whether a sources step ran, since it now comes before the
|
||||||
* one and it had a source to configure) — the step count shown reflects
|
* household step entirely — the "sur 3" vs "4 sur 4" split lives in
|
||||||
* whichever path the visitor actually took, "3 sur 3" or "4 sur 4"
|
* {@link onboardingStepIndicator}.
|
||||||
* (there's no shared wizard state to read this from — see
|
|
||||||
* `OnboardingHouseholdPage`'s doc comment on why each step is independent).
|
|
||||||
*/
|
*/
|
||||||
export function OnboardingAllergensPage() {
|
export function OnboardingAllergensPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -29,27 +30,22 @@ export function OnboardingAllergensPage() {
|
||||||
|
|
||||||
const [allergies, setAllergies] = useState<AllergyView[]>([]);
|
const [allergies, setAllergies] = useState<AllergyView[]>([]);
|
||||||
const [allergyIds, setAllergyIds] = useState<number[]>([]);
|
const [allergyIds, setAllergyIds] = useState<number[]>([]);
|
||||||
const [totalSteps, setTotalSteps] = useState(3);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [formError, setFormError] = useState<string | null>(null);
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
Promise.all([apiClient.getAllergies(), apiClient.getCurrentHouse()])
|
apiClient
|
||||||
.then(([allergiesResult, house]) => {
|
.getAllergies()
|
||||||
if (cancelled) return;
|
.then((allergiesResult) => {
|
||||||
setAllergies(allergiesResult);
|
if (!cancelled) 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.
|
|
||||||
})
|
})
|
||||||
|
// 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(() => {
|
.finally(() => {
|
||||||
if (!cancelled) setIsLoading(false);
|
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) {
|
async function handleSubmit(e: FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await apiClient.updateAllergyIds(allergyIds);
|
await apiClient.updateAllergyIds(allergyIds);
|
||||||
void navigate("/");
|
void navigate("/onboarding/foyer");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
|
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
|
||||||
setFormError(errorMessageService.getLabel(code));
|
setFormError(errorMessageService.getLabel(code));
|
||||||
|
|
@ -78,7 +74,7 @@ export function OnboardingAllergensPage() {
|
||||||
<main className="onboarding-page">
|
<main className="onboarding-page">
|
||||||
<form className="onboarding-card" onSubmit={handleSubmit} noValidate>
|
<form className="onboarding-card" onSubmit={handleSubmit} noValidate>
|
||||||
<p className="onboarding-step">
|
<p className="onboarding-step">
|
||||||
{t("onboarding.step", { current: totalSteps, total: totalSteps })}
|
{t("onboarding.step", onboardingStepIndicator("allergens"))}
|
||||||
</p>
|
</p>
|
||||||
<h1>{t("onboarding.allergens.title")}</h1>
|
<h1>{t("onboarding.allergens.title")}</h1>
|
||||||
|
|
||||||
|
|
@ -104,7 +100,7 @@ export function OnboardingAllergensPage() {
|
||||||
{formError && <p className="form-error">{formError}</p>}
|
{formError && <p className="form-error">{formError}</p>}
|
||||||
|
|
||||||
<button type="submit" disabled={isSubmitting || isLoading}>
|
<button type="submit" disabled={isSubmitting || isLoading}>
|
||||||
{t("onboarding.finish")}
|
{t("onboarding.continue")}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { ApiError, apiClient } from "../../api/client";
|
||||||
import { useAuth } from "../../features/auth/AuthContext";
|
import { useAuth } from "../../features/auth/AuthContext";
|
||||||
import { DietSelect } from "../../features/profile/DietSelect";
|
import { DietSelect } from "../../features/profile/DietSelect";
|
||||||
import { errorMessageService } from "../../services/error-message.service";
|
import { errorMessageService } from "../../services/error-message.service";
|
||||||
|
import { onboardingStepIndicator } from "./onboarding-steps";
|
||||||
import "./onboarding.scss";
|
import "./onboarding.scss";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -15,6 +16,9 @@ import "./onboarding.scss";
|
||||||
* already-loaded profile (`user.dietId`) — freshly signed-up, this is
|
* already-loaded profile (`user.dietId`) — freshly signed-up, this is
|
||||||
* `null` — no extra fetch needed just to know the starting value, unlike
|
* `null` — no extra fetch needed just to know the starting value, unlike
|
||||||
* the household step which has no such shortcut for the current name.
|
* 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() {
|
export function OnboardingDietPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -53,7 +57,7 @@ export function OnboardingDietPage() {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await apiClient.updateDiet(dietId);
|
await apiClient.updateDiet(dietId);
|
||||||
void navigate("/onboarding/foyer");
|
void navigate("/onboarding/allergenes");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
|
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
|
||||||
setFormError(errorMessageService.getLabel(code));
|
setFormError(errorMessageService.getLabel(code));
|
||||||
|
|
@ -65,7 +69,7 @@ export function OnboardingDietPage() {
|
||||||
return (
|
return (
|
||||||
<main className="onboarding-page">
|
<main className="onboarding-page">
|
||||||
<form className="onboarding-card" onSubmit={handleSubmit} noValidate>
|
<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>
|
<h1>{t("onboarding.diet.title")}</h1>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,24 @@ import { ApiError, apiClient } from "../../api/client";
|
||||||
import { HouseNameField } from "../../features/profile/HouseNameField";
|
import { HouseNameField } from "../../features/profile/HouseNameField";
|
||||||
import { fieldErrorsFrom } from "../../lib/zod-errors";
|
import { fieldErrorsFrom } from "../../lib/zod-errors";
|
||||||
import { errorMessageService } from "../../services/error-message.service";
|
import { errorMessageService } from "../../services/error-message.service";
|
||||||
|
import { onboardingStepIndicator } from "./onboarding-steps";
|
||||||
import "./onboarding.scss";
|
import "./onboarding.scss";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Second step of the post-signup onboarding wizard, routed at
|
* Third step of the post-signup onboarding wizard, routed at
|
||||||
* `/onboarding/foyer` — unlike the regime/allergens steps, this one is
|
* `/onboarding/foyer` — after the régime + allergènes "préférences" steps
|
||||||
* genuinely optional rather than just "skippable via an unedited default":
|
* (see #129). Unlike those, this one is genuinely optional rather than
|
||||||
* no household is created at signup anymore (see the API's
|
* just "skippable via an unedited default": no household is created at
|
||||||
* `auth.service.ts`), so there's nothing to prefill or implicitly keep by
|
* signup anymore (see the API's `auth.service.ts`), so there's nothing to
|
||||||
* pressing "Continuer" — the visitor explicitly creates one, joins one by
|
* prefill or implicitly keep by pressing "Continuer" — the visitor
|
||||||
* invite code, or skips the step outright via {@link SkipButton}.
|
* 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
|
* Still checks `getCurrentHouse()` on mount and skips straight to the
|
||||||
* "already have one" state if it finds one — defensive against revisiting
|
* "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
|
* `/onboarding/sources` only makes sense for a household the visitor just
|
||||||
* attach a preference to — created/joined/already-there goes on to it,
|
* *created* — that's the sole caller passing `true`. Joining one, skipping
|
||||||
* skipped goes straight to `/onboarding/allergenes` instead (nothing to
|
* the step, or finding one already there (browser back — can't tell
|
||||||
* configure sources for without a household).
|
* 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) {
|
function goToNextStep(createdHousehold: boolean) {
|
||||||
void navigate(hasHousehold ? "/onboarding/sources" : "/onboarding/allergenes");
|
void navigate(createdHousehold ? "/onboarding/sources" : "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="onboarding-page">
|
<main className="onboarding-page">
|
||||||
<div className="onboarding-card">
|
<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>
|
<h1>{t("onboarding.household.title")}</h1>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|
@ -70,15 +81,15 @@ export function OnboardingHouseholdPage() {
|
||||||
) : house !== null ? (
|
) : house !== null ? (
|
||||||
<>
|
<>
|
||||||
<p>{t("onboarding.household.alreadyHasHouse", { name: house.name })}</p>
|
<p>{t("onboarding.household.alreadyHasHouse", { name: house.name })}</p>
|
||||||
<button type="button" onClick={() => goToNextStep(true)}>
|
<button type="button" onClick={() => goToNextStep(false)}>
|
||||||
{t("onboarding.continue")}
|
{t("onboarding.finish")}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="onboarding-subtitle">{t("onboarding.household.subtitle")}</p>
|
<p className="onboarding-subtitle">{t("onboarding.household.subtitle")}</p>
|
||||||
<CreateHouseholdForm onDone={() => goToNextStep(true)} />
|
<CreateHouseholdForm onDone={() => goToNextStep(true)} />
|
||||||
<JoinHouseholdForm onDone={() => goToNextStep(true)} />
|
<JoinHouseholdForm onDone={() => goToNextStep(false)} />
|
||||||
<SkipButton onSkip={() => goToNextStep(false)} />
|
<SkipButton onSkip={() => goToNextStep(false)} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -6,22 +6,23 @@ import { useNavigate } from "react-router-dom";
|
||||||
import { ApiError, apiClient } from "../../api/client";
|
import { ApiError, apiClient } from "../../api/client";
|
||||||
import { SourceSelect } from "../../features/house/SourceSelect";
|
import { SourceSelect } from "../../features/house/SourceSelect";
|
||||||
import { errorMessageService } from "../../services/error-message.service";
|
import { errorMessageService } from "../../services/error-message.service";
|
||||||
|
import { onboardingStepIndicator } from "./onboarding-steps";
|
||||||
import "./onboarding.scss";
|
import "./onboarding.scss";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Third step of the post-signup onboarding wizard, routed at
|
* Final step of the post-signup onboarding wizard, routed at
|
||||||
* `/onboarding/sources` — only reached when a household now exists (see
|
* `/onboarding/sources` — reached only when the previous step *created* a
|
||||||
* `OnboardingHouseholdPage`'s `goToStep`, which skips straight to
|
* household (see `OnboardingHouseholdPage`'s `goToNextStep`, which ends the
|
||||||
* `/onboarding/allergenes` when the previous step was skipped): enabled
|
* wizard at `/` on every other path): enabled sources are a per-household
|
||||||
* sources are a per-household preference (`HouseSource`, schema.prisma),
|
* preference (`HouseSource`, schema.prisma), so there's nothing to attach
|
||||||
* so there's nothing to attach a selection to without one.
|
* a selection to otherwise.
|
||||||
*
|
*
|
||||||
* Starts from an empty selection — a newly created/joined household has
|
* Starts from an empty selection — a newly created household has nothing
|
||||||
* nothing enabled yet by design (opt-in, see `house.service.ts`'s
|
* enabled yet by design (opt-in, see `house.service.ts`'s
|
||||||
* `getHouseSourceIds`). If no source is implemented yet (`getSources()`
|
* `getHouseSourceIds`). If no source is implemented yet (`getSources()`
|
||||||
* comes back empty — currently always the case), there's nothing to pick
|
* comes back empty — currently always the case), there's nothing to pick
|
||||||
* from either, so this step silently steps itself over rather than showing
|
* from either, so this step silently finishes the wizard (straight to `/`)
|
||||||
* an empty screen.
|
* rather than showing an empty screen.
|
||||||
*/
|
*/
|
||||||
export function OnboardingSourcesPage() {
|
export function OnboardingSourcesPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -40,7 +41,7 @@ export function OnboardingSourcesPage() {
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
if (result.length === 0) {
|
if (result.length === 0) {
|
||||||
void navigate("/onboarding/allergenes", { replace: true });
|
void navigate("/", { replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSources(result);
|
setSources(result);
|
||||||
|
|
@ -50,7 +51,7 @@ export function OnboardingSourcesPage() {
|
||||||
// Nothing to configure sources for if we can't even list them — the
|
// Nothing to configure sources for if we can't even list them — the
|
||||||
// wizard shouldn't strand the visitor here over a transient failure
|
// wizard shouldn't strand the visitor here over a transient failure
|
||||||
// fetching an optional step's own data.
|
// fetching an optional step's own data.
|
||||||
if (!cancelled) void navigate("/onboarding/allergenes", { replace: true });
|
if (!cancelled) void navigate("/", { replace: true });
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
|
@ -63,7 +64,7 @@ export function OnboardingSourcesPage() {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await apiClient.updateHouseSourceIds(sourceIds);
|
await apiClient.updateHouseSourceIds(sourceIds);
|
||||||
void navigate("/onboarding/allergenes");
|
void navigate("/");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
|
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
|
||||||
setFormError(errorMessageService.getLabel(code));
|
setFormError(errorMessageService.getLabel(code));
|
||||||
|
|
@ -75,7 +76,9 @@ export function OnboardingSourcesPage() {
|
||||||
return (
|
return (
|
||||||
<main className="onboarding-page">
|
<main className="onboarding-page">
|
||||||
<form className="onboarding-card" onSubmit={handleSubmit} noValidate>
|
<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>
|
<h1>{t("onboarding.sources.title")}</h1>
|
||||||
<p className="onboarding-subtitle">{t("onboarding.sources.subtitle")}</p>
|
<p className="onboarding-subtitle">{t("onboarding.sources.subtitle")}</p>
|
||||||
|
|
||||||
|
|
@ -93,7 +96,7 @@ export function OnboardingSourcesPage() {
|
||||||
{formError && <p className="form-error">{formError}</p>}
|
{formError && <p className="form-error">{formError}</p>}
|
||||||
|
|
||||||
<button type="submit" disabled={isSubmitting || isLoading}>
|
<button type="submit" disabled={isSubmitting || isLoading}>
|
||||||
{t("onboarding.continue")}
|
{t("onboarding.finish")}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</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
|
│ │ ├── AccountSettingsPage.tsx / HouseholdSettingsPage.tsx / PreferencesPage.tsx
|
||||||
│ │ ├── UserPreferencesPage.tsx / CreditsPage.tsx
|
│ │ ├── UserPreferencesPage.tsx / CreditsPage.tsx
|
||||||
│ │ └── settings-pages.scss
|
│ │ └── 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
|
│ ├── OnboardingDietPage.tsx / OnboardingHouseholdPage.tsx / OnboardingSourcesPage.tsx / OnboardingAllergensPage.tsx
|
||||||
│ └── onboarding.scss
|
│ └── onboarding.scss
|
||||||
├── styles/
|
├── styles/
|
||||||
|
|
@ -156,9 +156,11 @@ flowchart TB
|
||||||
l'ancienne page combinée) redirige vers `/parametres/foyer` pour ne pas casser
|
l'ancienne page combinée) redirige vers `/parametres/foyer` pour ne pas casser
|
||||||
un lien existant.
|
un lien existant.
|
||||||
- **`/onboarding/*`** reste son propre groupe de routes top-level (pas nichées
|
- **`/onboarding/*`** reste son propre groupe de routes top-level (pas nichées
|
||||||
sous `AppLayout` — wizard plein écran sans sidebar) : `regime` → `foyer` →
|
sous `AppLayout` — wizard plein écran sans sidebar) : `regime` → `allergenes`
|
||||||
`sources` (conditionnelle — seulement si l'étape `foyer` a créé/rejoint un
|
(les deux « préférences ») → `foyer` → `sources` (conditionnelle — seulement
|
||||||
foyer, sinon on saute directement à l'étape suivante) → `allergenes`.
|
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
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
SIGNUP["SignupPage<br/>(POST /auth/signup)"] --> OB1["/onboarding/regime"]
|
SIGNUP["SignupPage<br/>(POST /auth/signup)"] --> OB1["/onboarding/regime"]
|
||||||
OB1 --> OB2["/onboarding/foyer"]
|
OB1 --> OB2["/onboarding/allergenes"]
|
||||||
OB2 -->|"foyer créé/rejoint"| OB3["/onboarding/sources"]
|
OB2 --> OB3["/onboarding/foyer"]
|
||||||
OB2 -->|"pas de foyer"| OB4["/onboarding/allergenes"]
|
OB3 -->|"foyer créé"| OB4["/onboarding/sources"]
|
||||||
OB3 --> OB4
|
OB3 -->|"rejoint / passé"| HOME["/ (PlanningPage)"]
|
||||||
OB4 --> HOME["/ (PlanningPage)"]
|
OB4 --> HOME
|
||||||
|
|
||||||
SETTINGSMENU["Sidebar : Paramètres"] --> S1["/parametres/compte"]
|
SETTINGSMENU["Sidebar : Paramètres"] --> S1["/parametres/compte"]
|
||||||
SETTINGSMENU --> S2["/parametres/preferences"]
|
SETTINGSMENU --> S2["/parametres/preferences"]
|
||||||
|
|
@ -240,18 +242,23 @@ flowchart LR
|
||||||
SETTINGSMENU --> S6["/parametres/credits"]
|
SETTINGSMENU --> S6["/parametres/credits"]
|
||||||
```
|
```
|
||||||
|
|
||||||
- **`pages/onboarding/`** — wizard de **4 écrans** (régime → foyer → sources →
|
- **`pages/onboarding/`** — wizard de **3 écrans** (régime → allergènes →
|
||||||
allergènes), lancé une seule fois juste après l'inscription. Routes
|
foyer), + un **4ᵉ conditionnel** (sources), lancé une seule fois juste après
|
||||||
top-level `RequireAuth`, **pas** nichées sous `AppLayout` : wizard plein
|
l'inscription. Routes top-level `RequireAuth`, **pas** nichées sous
|
||||||
écran sans sidebar (`onboarding.scss`, même langage visuel que
|
`AppLayout` : wizard plein écran sans sidebar (`onboarding.scss`, même
|
||||||
`/login`/`/signup`). Chaque étape a un unique bouton "Continuer" qui envoie
|
langage visuel que `/login`/`/signup`). Régime et allergènes forment les
|
||||||
la valeur courante — pas de bouton "Passer" séparé, une valeur
|
deux « préférences » ; chacun a un unique bouton "Continuer" qui envoie la
|
||||||
"aucune"/vide *est* le skip. `/onboarding/sources` est **conditionnelle** :
|
valeur courante — une valeur "aucune"/vide *est* le skip. L'étape `foyer`,
|
||||||
seulement atteinte si l'étape `foyer` vient de créer/rejoindre un foyer
|
elle, est explicitement optionnelle (créer / rejoindre / "Passer cette
|
||||||
(`OnboardingHouseholdPage`'s `goToNextStep`) — sautée sinon, directement vers
|
étape"). `/onboarding/sources` est **conditionnelle** : seulement atteinte
|
||||||
`/onboarding/allergenes`. `OnboardingSourcesPage` elle-même redirige en
|
si l'étape `foyer` vient de **créer** un foyer
|
||||||
silence vers l'étape suivante si `GET /reference/sources` revient vide ou en
|
(`OnboardingHouseholdPage`'s `goToNextStep`) — rejoindre ou passer termine
|
||||||
erreur, plutôt que d'afficher une étape sans rien à choisir.
|
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
|
- **Pages `/parametres/*`** (`pages/settings/`, dans `AppLayout`) — l'ancienne
|
||||||
`HouseholdPage` combinée a été **éclatée en 5 pages** dédiées (voir la
|
`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
|
section suivante pour le détail de chacune), toutes en **hot saving** (pas
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue