Tests + docs: onboarding/foyer Cypress coverage, specs updates (step 6/6)
- apps/web/cypress/e2e/onboarding.cy.ts — parcours complet (rempli et entièrement skippé) signup → 3 étapes → home, mêmes conventions cy.intercept que le reste. - apps/web/cypress/e2e/household.cy.ts — /foyer : préremplissage, et sauvegarde indépendante de chacune des 3 sections. - specs/frontend-architecture.md : nouvelle section "Parcours profil — foyer, régime, allergènes" (diagramme mermaid, les deux bugs de state trouvés en testant dans le navigateur), arborescence et namespaces i18n à jour. - README.md : nouvelle section "Parcours profil — foyer, régime, allergènes", section sidebar mise à jour (Foyer & profil n'est plus un stub). Cypress lui-même ne peut pas tourner en local dans ce sandbox (voir la note existante dans le README) — vérifié via `tsc --noEmit` sur les specs + parcours manuel complet dans le navigateur (les deux à travers les 5 commits précédents de cette feature). Clôt la feature profil/foyer/régime/allergènes (6 commits, cette PR) : seed+référence -> endpoints foyer/profil -> composants partagés -> wizard d'inscription -> page /foyer -> ce commit.
This commit is contained in:
parent
5d88e28810
commit
9722f4a27b
4 changed files with 346 additions and 22 deletions
53
README.md
53
README.md
|
|
@ -258,17 +258,54 @@ Une fois connecté, l'utilisateur atterrit sur `src/layouts/AppLayout.tsx` — s
|
|||
`<Outlet />` pour la route active — montée une seule fois comme route parente de tout
|
||||
l'espace authentifié (`App.tsx`), pas dupliquée par page. `src/pages/HomePage.tsx`
|
||||
(routée sur `/`) affiche le planning de la semaine du foyer (`GET /planning/current`,
|
||||
voir plus haut) avec ses états chargement/erreur/vide/rempli ; `Recettes`, `Liste de
|
||||
courses` et `Foyer & profil` n'ont pas encore de backend dédié et rendent pour
|
||||
l'instant le même composant `ComingSoonPage`. Détail complet (pourquoi une seule
|
||||
route parente, pourquoi un composant stub partagé) :
|
||||
voir plus haut) avec ses états chargement/erreur/vide/rempli ; `Recettes` et `Liste de
|
||||
courses` n'ont pas encore de backend dédié et rendent pour l'instant le même
|
||||
composant `ComingSoonPage` — `Foyer & profil` (`src/pages/HouseholdPage.tsx`), lui,
|
||||
est une vraie page (voir section suivante). Détail complet (pourquoi une seule route
|
||||
parente, pourquoi un composant stub partagé) :
|
||||
[specs/frontend-architecture.md](specs/frontend-architecture.md#applayout--sidebar-commune-à-lespace-connecté).
|
||||
|
||||
## Parcours profil — foyer, régime, allergènes (apps/web)
|
||||
|
||||
- `src/features/profile/` — `HouseNameField`, `DietSelect`, `AllergySelect` : champs
|
||||
contrôlés et "dumb" (reçoivent leurs données en props, ne fetchent rien
|
||||
eux-mêmes), partagés par les deux surfaces ci-dessous. `AllergySelect` utilise une
|
||||
grille de cases à cocher dans un `<fieldset>`/`<legend>` plutôt qu'un
|
||||
`<select multiple>` — bien plus repérable/tapable, notamment sur mobile.
|
||||
- `src/pages/onboarding/` — wizard de 3 écrans lancé une fois juste après
|
||||
l'inscription (`OnboardingHouseholdPage` → `OnboardingDietPage` →
|
||||
`OnboardingAllergensPage`, routes `/onboarding/{foyer,regime,allergenes}`).
|
||||
Chaque étape a un unique bouton "Continuer" qui envoie la valeur courante (y
|
||||
compris "aucune" pour régime/allergènes) — pas de bouton "Passer" séparé, skip
|
||||
implicite. Routes top-level `RequireAuth`, **pas** nichées sous `AppLayout` :
|
||||
wizard plein écran sans sidebar, même langage visuel que `/login`/`/signup`.
|
||||
- `src/pages/HouseholdPage.tsx` (routée sur `/foyer`) — mêmes trois réglages,
|
||||
modifiables à tout moment, chaque section (foyer/régime/allergènes) avec son
|
||||
propre bouton "Enregistrer" (3 ressources API indépendantes).
|
||||
|
||||
**Piège trouvé en testant dans le navigateur** : `RedirectIfAuthenticated` (garde de
|
||||
`/login`/`/signup`) réagissait à *chaque* changement de `user`, pas seulement à la
|
||||
vérification initiale — un `navigate()` explicite dans le gestionnaire de soumission
|
||||
d'un formulaire qu'elle protège (ex. `SignupPage` après `signup()`, qui met `user` à
|
||||
jour) entre alors en course avec le propre `<Navigate>` de la garde. Invisible tant
|
||||
que les deux ciblaient "/", devenu un vrai bug dès que `SignupPage` a dû rediriger
|
||||
ailleurs (`/onboarding/foyer`). Fix : la décision de redirection est verrouillée une
|
||||
seule fois, au moment où `isLoading` passe à `false`, plus jamais réévaluée après.
|
||||
|
||||
**Autre piège, même méthode** : `HouseholdPage` initialisait le régime affiché depuis
|
||||
`useAuth().user.dietId` (un instantané jamais rafraîchi après une modification faite
|
||||
directement via `apiClient`, qui ne touche pas `AuthContext`) — revenait à l'ancienne
|
||||
valeur après un aller-retour de navigation SPA sans rechargement complet. Fix : la
|
||||
page fetch son propre profil frais (`apiClient.me()`) au montage, et
|
||||
`AuthContext.refreshUser()` (nouveau) est appelé après une sauvegarde réussie du
|
||||
régime pour que le reste de l'app reste cohérent aussi.
|
||||
|
||||
Tests Cypress (`apps/web/cypress/e2e/`) : `smoke.cy.ts` + `auth.cy.ts` +
|
||||
`home-planning.cy.ts` mockent l'API via `cy.intercept` plutôt que de dépendre d'un
|
||||
vrai backend — le job e2e de la CI ne provisionne pas de Postgres/API, seulement le
|
||||
serveur de dev Vite. Le comportement réel de l'API est couvert par les suites
|
||||
Mocha/Cucumber d'`apps/api` (contre une vraie base).
|
||||
`home-planning.cy.ts` + `onboarding.cy.ts` + `household.cy.ts` mockent l'API via
|
||||
`cy.intercept` plutôt que de dépendre d'un vrai backend — le job e2e de la CI ne
|
||||
provisionne pas de Postgres/API, seulement le serveur de dev Vite. Le comportement
|
||||
réel de l'API est couvert par les suites Mocha/Cucumber d'`apps/api` (contre une
|
||||
vraie base).
|
||||
|
||||
> **Cypress ne peut pas tourner en local dans un environnement Windows sandboxé** :
|
||||
> Chromium/Electron headless plante au lancement du process GPU
|
||||
|
|
|
|||
94
apps/web/cypress/e2e/household.cy.ts
Normal file
94
apps/web/cypress/e2e/household.cy.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// Mocks the API via cy.intercept — see auth.cy.ts for the rationale.
|
||||
|
||||
const authenticatedProfile = {
|
||||
id: 1,
|
||||
firstName: "Alice",
|
||||
lastName: "Martin",
|
||||
email: "alice@example.com",
|
||||
tokenVersion: 0,
|
||||
houseId: 1,
|
||||
dietId: 2,
|
||||
};
|
||||
|
||||
describe("Household & profile settings (/foyer)", () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
|
||||
cy.intercept("GET", "**/house/current", {
|
||||
statusCode: 200,
|
||||
body: { id: 1, name: "Chez Alice" },
|
||||
});
|
||||
cy.intercept("GET", "**/reference/diets", {
|
||||
statusCode: 200,
|
||||
body: [
|
||||
{ id: 1, name: "Omnivore" },
|
||||
{ id: 2, name: "Végétarien" },
|
||||
],
|
||||
});
|
||||
cy.intercept("GET", "**/reference/allergies", {
|
||||
statusCode: 200,
|
||||
body: [
|
||||
{ id: 1, name: "Arachides" },
|
||||
{ id: 2, name: "Gluten" },
|
||||
],
|
||||
});
|
||||
cy.intercept("GET", "**/profile/allergies", { statusCode: 200, body: [2] });
|
||||
});
|
||||
|
||||
it("loads the current household name, regime and allergens", () => {
|
||||
cy.visit("/foyer");
|
||||
|
||||
cy.get("#houseName").should("have.value", "Chez Alice");
|
||||
cy.get("#diet").should("have.value", "2");
|
||||
cy.contains("label", "Gluten").find("input[type=checkbox]").should("be.checked");
|
||||
cy.contains("label", "Arachides").find("input[type=checkbox]").should("not.be.checked");
|
||||
});
|
||||
|
||||
it("saves the household name independently of the other sections", () => {
|
||||
cy.intercept("PATCH", "**/house/current", {
|
||||
statusCode: 200,
|
||||
body: { id: 1, name: "Chez les Martin" },
|
||||
}).as("renameHouse");
|
||||
|
||||
cy.visit("/foyer");
|
||||
cy.get("#houseName").clear();
|
||||
cy.get("#houseName").type("Chez les Martin");
|
||||
cy.get("#houseName")
|
||||
.closest("form")
|
||||
.within(() => cy.contains("button", "Enregistrer").click());
|
||||
|
||||
cy.wait("@renameHouse").its("request.body").should("deep.equal", { name: "Chez les Martin" });
|
||||
cy.get("#houseName").closest("form").contains("Enregistré ✓").should("be.visible");
|
||||
});
|
||||
|
||||
it("saves the regime independently of the other sections", () => {
|
||||
cy.intercept("PATCH", "**/profile/diet", {
|
||||
statusCode: 200,
|
||||
body: { ...authenticatedProfile, dietId: 1 },
|
||||
}).as("updateDiet");
|
||||
|
||||
cy.visit("/foyer");
|
||||
cy.get("#diet").select("Omnivore");
|
||||
cy.get("#diet")
|
||||
.closest("form")
|
||||
.within(() => cy.contains("button", "Enregistrer").click());
|
||||
|
||||
cy.wait("@updateDiet").its("request.body").should("deep.equal", { dietId: 1 });
|
||||
cy.get("#diet").closest("form").contains("Enregistré ✓").should("be.visible");
|
||||
});
|
||||
|
||||
it("saves the allergen selection independently of the other sections", () => {
|
||||
cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [1, 2] }).as(
|
||||
"updateAllergies",
|
||||
);
|
||||
|
||||
cy.visit("/foyer");
|
||||
cy.contains("label", "Arachides").find("input[type=checkbox]").check();
|
||||
cy.contains("fieldset", "Allergies").within(() => {
|
||||
cy.contains("button", "Enregistrer").click();
|
||||
});
|
||||
|
||||
cy.wait("@updateAllergies")
|
||||
.its("request.body")
|
||||
.should("deep.equal", { allergyIds: [2, 1] });
|
||||
});
|
||||
});
|
||||
129
apps/web/cypress/e2e/onboarding.cy.ts
Normal file
129
apps/web/cypress/e2e/onboarding.cy.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// Mocks the API via cy.intercept — see auth.cy.ts for the rationale (no
|
||||
// live backend in this CI job; apps/api's own Mocha/Cucumber suites cover
|
||||
// real API behavior against a real database).
|
||||
|
||||
const signupResponse = {
|
||||
id: 1,
|
||||
firstName: "Alice",
|
||||
lastName: "Martin",
|
||||
email: "alice@example.com",
|
||||
tokenVersion: 0,
|
||||
houseId: 1,
|
||||
dietId: null,
|
||||
};
|
||||
|
||||
describe("Onboarding wizard (household → regime → allergens)", () => {
|
||||
it("walks through all three steps after signup and lands on the home", () => {
|
||||
cy.intercept("GET", "**/auth/me", { statusCode: 401 });
|
||||
cy.intercept("POST", "**/auth/signup", { statusCode: 201, body: signupResponse }).as("signup");
|
||||
cy.intercept("GET", "**/house/current", {
|
||||
statusCode: 200,
|
||||
body: { id: 1, name: "Foyer de Alice" },
|
||||
});
|
||||
cy.intercept("PATCH", "**/house/current", {
|
||||
statusCode: 200,
|
||||
body: { id: 1, name: "Chez Alice" },
|
||||
}).as("renameHouse");
|
||||
cy.intercept("GET", "**/reference/diets", {
|
||||
statusCode: 200,
|
||||
body: [
|
||||
{ id: 1, name: "Omnivore" },
|
||||
{ id: 2, name: "Végétarien" },
|
||||
],
|
||||
});
|
||||
cy.intercept("PATCH", "**/profile/diet", {
|
||||
statusCode: 200,
|
||||
body: { ...signupResponse, dietId: 2 },
|
||||
}).as("updateDiet");
|
||||
cy.intercept("GET", "**/reference/allergies", {
|
||||
statusCode: 200,
|
||||
body: [
|
||||
{ id: 1, name: "Arachides" },
|
||||
{ id: 2, name: "Gluten" },
|
||||
],
|
||||
});
|
||||
cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [1] }).as(
|
||||
"updateAllergies",
|
||||
);
|
||||
cy.intercept("GET", "**/planning/current", { statusCode: 200, body: null });
|
||||
|
||||
cy.visit("/signup");
|
||||
cy.get("#firstName").type("Alice");
|
||||
cy.get("#lastName").type("Martin");
|
||||
cy.get("#email").type("alice@example.com");
|
||||
cy.get("#password").type("correct-horse-battery-staple");
|
||||
cy.contains("button", "Créer mon profil").click();
|
||||
cy.wait("@signup");
|
||||
|
||||
// Step 1/3 — household name, prefilled with the auto-generated default.
|
||||
cy.url().should("include", "/onboarding/foyer");
|
||||
cy.contains("Étape 1 sur 3").should("be.visible");
|
||||
cy.get("#houseName").should("have.value", "Foyer de Alice");
|
||||
cy.get("#houseName").clear();
|
||||
cy.get("#houseName").type("Chez Alice");
|
||||
cy.contains("button", "Continuer").click();
|
||||
cy.wait("@renameHouse").its("request.body").should("deep.equal", { name: "Chez Alice" });
|
||||
|
||||
// Step 2/3 — dietary regime.
|
||||
cy.url().should("include", "/onboarding/regime");
|
||||
cy.contains("Étape 2 sur 3").should("be.visible");
|
||||
cy.get("#diet").select("Végétarien");
|
||||
cy.contains("button", "Continuer").click();
|
||||
cy.wait("@updateDiet").its("request.body").should("deep.equal", { dietId: 2 });
|
||||
|
||||
// Step 3/3 — allergens/intolerances, then finish.
|
||||
cy.url().should("include", "/onboarding/allergenes");
|
||||
cy.contains("Étape 3 sur 3").should("be.visible");
|
||||
cy.contains("label", "Arachides").find("input[type=checkbox]").check();
|
||||
cy.contains("button", "Terminer").click();
|
||||
cy.wait("@updateAllergies")
|
||||
.its("request.body")
|
||||
.should("deep.equal", { allergyIds: [1] });
|
||||
|
||||
cy.url().should("eq", `${Cypress.config().baseUrl}/`);
|
||||
cy.contains("h1", "Planning de la semaine").should("be.visible");
|
||||
});
|
||||
|
||||
it("lets every step be skipped without changing anything", () => {
|
||||
cy.intercept("GET", "**/auth/me", { statusCode: 401 });
|
||||
cy.intercept("POST", "**/auth/signup", { statusCode: 201, body: signupResponse });
|
||||
cy.intercept("GET", "**/house/current", {
|
||||
statusCode: 200,
|
||||
body: { id: 1, name: "Foyer de Alice" },
|
||||
});
|
||||
cy.intercept("PATCH", "**/house/current", {
|
||||
statusCode: 200,
|
||||
body: { id: 1, name: "Foyer de Alice" },
|
||||
}).as("renameHouse");
|
||||
cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [] });
|
||||
cy.intercept("PATCH", "**/profile/diet", { statusCode: 200, body: signupResponse }).as(
|
||||
"updateDiet",
|
||||
);
|
||||
cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [] });
|
||||
cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [] }).as(
|
||||
"updateAllergies",
|
||||
);
|
||||
cy.intercept("GET", "**/planning/current", { statusCode: 200, body: null });
|
||||
|
||||
cy.visit("/signup");
|
||||
cy.get("#firstName").type("Alice");
|
||||
cy.get("#lastName").type("Martin");
|
||||
cy.get("#email").type("alice@example.com");
|
||||
cy.get("#password").type("correct-horse-battery-staple");
|
||||
cy.contains("button", "Créer mon profil").click();
|
||||
|
||||
cy.url().should("include", "/onboarding/foyer");
|
||||
cy.contains("button", "Continuer").click();
|
||||
cy.wait("@renameHouse").its("request.body").should("deep.equal", { name: "Foyer de Alice" });
|
||||
|
||||
cy.url().should("include", "/onboarding/regime");
|
||||
cy.contains("button", "Continuer").click();
|
||||
cy.wait("@updateDiet").its("request.body").should("deep.equal", { dietId: null });
|
||||
|
||||
cy.url().should("include", "/onboarding/allergenes");
|
||||
cy.contains("button", "Terminer").click();
|
||||
cy.wait("@updateAllergies").its("request.body").should("deep.equal", { allergyIds: [] });
|
||||
|
||||
cy.url().should("eq", `${Cypress.config().baseUrl}/`);
|
||||
});
|
||||
});
|
||||
|
|
@ -14,15 +14,18 @@ apps/web/src/
|
|||
├── i18n/
|
||||
│ └── i18n.ts # config i18next, importé une fois (main.tsx) pour son effet de bord
|
||||
├── locales/
|
||||
│ └── fr/translation.json # libellés français (errors.*, auth.*, layout.*, home.*, recipes.*, shoppingList.*, household.*)
|
||||
│ └── fr/translation.json # libellés français (errors.*, auth.*, layout.*, home.*, recipes.*, shoppingList.*, onboarding.*, household.*)
|
||||
├── services/
|
||||
│ └── error-message.service.ts # ErrorMessageService — code d'erreur → clé i18next
|
||||
├── features/
|
||||
│ └── auth/ # tout ce qui concerne l'authentification
|
||||
│ ├── AuthContext.tsx # état global (profil connecté, login/signup/logout)
|
||||
│ ├── RequireAuth.tsx # garde de route : redirige vers /login si non connecté
|
||||
│ ├── RedirectIfAuthenticated.tsx # garde de route inverse (pour /login, /signup)
|
||||
│ └── auth-form.scss # styles partagés par LoginPage et SignupPage
|
||||
│ ├── auth/ # tout ce qui concerne l'authentification
|
||||
│ │ ├── AuthContext.tsx # état global (profil connecté, login/signup/logout, refreshUser)
|
||||
│ │ ├── RequireAuth.tsx # garde de route : redirige vers /login si non connecté
|
||||
│ │ ├── RedirectIfAuthenticated.tsx # garde de route inverse (pour /login, /signup)
|
||||
│ │ └── auth-form.scss # styles partagés par LoginPage et SignupPage
|
||||
│ └── profile/ # champs du parcours foyer/régime/allergènes, voir plus bas
|
||||
│ ├── HouseNameField.tsx / DietSelect.tsx / AllergySelect.tsx
|
||||
│ └── profile-forms.scss # styles partagés par les trois
|
||||
├── layouts/
|
||||
│ └── AppLayout.tsx + .scss # sidebar (nav + user/logout) commune à tout l'espace connecté, voir plus bas
|
||||
├── pages/
|
||||
|
|
@ -30,7 +33,11 @@ apps/web/src/
|
|||
│ ├── SignupPage.tsx / .scss (via auth-form.scss, partagé)
|
||||
│ ├── HomePage.tsx + HomePage.scss # planning de la semaine (routée sur "/")
|
||||
│ ├── ComingSoonPage.tsx + .scss # placeholder partagé par les sections sans backend encore
|
||||
│ ├── RecipesPage.tsx / ShoppingListPage.tsx / HouseholdPage.tsx # fines enveloppes autour de ComingSoonPage
|
||||
│ ├── RecipesPage.tsx / ShoppingListPage.tsx # fines enveloppes autour de ComingSoonPage
|
||||
│ ├── HouseholdPage.tsx + .scss # foyer/régime/allergènes, éditable à tout moment (routée sur "/foyer")
|
||||
│ └── onboarding/ # wizard d'inscription (foyer → régime → allergènes), voir plus bas
|
||||
│ ├── OnboardingHouseholdPage.tsx / OnboardingDietPage.tsx / OnboardingAllergensPage.tsx
|
||||
│ └── onboarding.scss # styles partagés par les trois
|
||||
├── styles/
|
||||
│ ├── _theme.scss # tokens de design (couleurs, espacements, typographie)
|
||||
│ └── global.scss # reset minimal + import du theme — importé une seule fois (main.tsx)
|
||||
|
|
@ -82,6 +89,11 @@ flowchart TB
|
|||
(redirige un utilisateur déjà connecté vers `/`). Les deux affichent `null` tant
|
||||
que la vérification initiale est en cours, pour éviter un flash de contenu suivi
|
||||
d'une redirection.
|
||||
- **`RedirectIfAuthenticated` verrouille sa décision une seule fois** (au moment où
|
||||
`isLoading` passe à `false`), au lieu de réagir à chaque changement de `user` —
|
||||
bug trouvé en construisant le wizard d'inscription (voir plus bas) : un
|
||||
`navigate()` explicite dans le formulaire qu'elle protège entrait en course avec
|
||||
son propre `<Navigate>`, invisible tant que les deux ciblaient "/".
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -110,12 +122,62 @@ README racine).
|
|||
|
||||
### Sections sans backend — `ComingSoonPage`
|
||||
|
||||
`Recettes`, `Liste de courses` et `Foyer & profil` n'ont pas encore de backend
|
||||
dédié (seul `/planning/current` existe, voir le README). Chacune a néanmoins sa
|
||||
propre route/page (`RecipesPage.tsx`, etc. — choix délibéré pour que construire la
|
||||
vraie fonctionnalité plus tard soit réécrire un fichier dédié, pas éclater une
|
||||
route générique), mais toutes rendent le même composant `ComingSoonPage`
|
||||
(`title`/`description`) pour éviter de tripler un même bloc de markup.
|
||||
`Recettes` et `Liste de courses` n'ont pas encore de backend dédié (seuls
|
||||
`/planning/current` et le parcours foyer/profil ci-dessous existent, voir le
|
||||
README). Chacune a néanmoins sa propre route/page (`RecipesPage.tsx`, etc. — choix
|
||||
délibéré pour que construire la vraie fonctionnalité plus tard soit réécrire un
|
||||
fichier dédié, pas éclater une route générique), mais toutes rendent le même
|
||||
composant `ComingSoonPage` (`title`/`description`) pour éviter de tripler un même
|
||||
bloc de markup.
|
||||
|
||||
---
|
||||
|
||||
## Parcours profil — foyer, régime, allergènes
|
||||
|
||||
Deux surfaces, mêmes composants de champ (`features/profile/`) :
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
SIGNUP["SignupPage<br/>(POST /auth/signup)"] --> OB1["/onboarding/foyer"]
|
||||
OB1 --> OB2["/onboarding/regime"]
|
||||
OB2 --> OB3["/onboarding/allergenes"]
|
||||
OB3 --> HOME["/ (home)"]
|
||||
|
||||
SIDEBAR["Sidebar : Foyer & profil"] --> SETTINGS["/foyer (HouseholdPage)"]
|
||||
```
|
||||
|
||||
- **`pages/onboarding/`** — wizard de 3 écrans, 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`, délibérément un fichier à part plutôt qu'un import de
|
||||
`auth-form.scss` — même choix que `HomePage.scss` avant elle, voir plus haut).
|
||||
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.
|
||||
- **`pages/HouseholdPage.tsx`** (routée `/foyer`, dans `AppLayout`) — les mêmes
|
||||
trois réglages, éditables à tout moment. Trois sections, trois boutons
|
||||
"Enregistrer" indépendants (3 ressources API distinctes : `PATCH /house/current`,
|
||||
`/profile/diet`, `/profile/allergies`).
|
||||
- **`features/profile/`** — `HouseNameField`, `DietSelect`, `AllergySelect` : champs
|
||||
contrôlés, "dumb" (reçoivent `diets`/`allergies` en props plutôt que de les
|
||||
fetcher). `AllergySelect` est un `<fieldset>`/`<legend>` + grille de cases à
|
||||
cocher, pas un `<select multiple>` — bien plus repérable/tapable, notamment sur
|
||||
mobile (voir la note Capacitor plus haut).
|
||||
|
||||
### Deux bugs de state trouvés en testant dans le navigateur
|
||||
|
||||
1. **Course entre `navigate()` et `RedirectIfAuthenticated`** — voir la note sur
|
||||
`RedirectIfAuthenticated` plus haut. `SignupPage` doit maintenant rediriger vers
|
||||
`/onboarding/foyer`, pas `/`, ce qui a rendu visible une course de state
|
||||
auparavant invisible.
|
||||
2. **`user.dietId` périmé sur `/foyer`** — `HouseholdPage` initialisait le régime
|
||||
affiché depuis `useAuth().user.dietId`, un instantané d'`AuthContext` jamais
|
||||
rafraîchi après une modification faite directement via `apiClient` (qui ne
|
||||
touche pas le contexte). Une navigation SPA aller-retour sans rechargement
|
||||
complet ré-affichait donc l'ancienne valeur après une sauvegarde. Fix :
|
||||
`HouseholdPage` fetch son propre profil frais (`apiClient.me()`) au montage
|
||||
plutôt que de dépendre du contexte, et `AuthContext.refreshUser()` (nouvelle
|
||||
méthode, re-fetch `GET /auth/me`) est appelée après une sauvegarde réussie du
|
||||
régime — pour que le reste de l'app (pas seulement cette page) reste cohérent.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -146,7 +208,9 @@ JSON, jamais codé en dur dans un composant.
|
|||
namespace : `errors.*` (voir [error-handling.md](./error-handling.md)),
|
||||
`auth.login.*` / `auth.signup.*`, `layout.*` (nav de la sidebar, salutation,
|
||||
déconnexion — `AppLayout`), `home.*` (planning), `recipes.*` / `shoppingList.*`
|
||||
/ `household.*` (copie des pages stub, voir `ComingSoonPage` plus haut).
|
||||
(copie des pages stub, voir `ComingSoonPage` plus haut), `onboarding.*` (wizard
|
||||
d'inscription) et `household.*` (titre + `form.*`, champs partagés par le wizard
|
||||
et `/foyer`).
|
||||
- Dans un composant : `const { t } = useTranslation(); t("auth.login.title")`.
|
||||
- Ajouter une langue : créer `locales/<lng>/translation.json` avec les mêmes clés,
|
||||
ajouter `resources.<lng>` dans `i18n/i18n.ts` — aucun composant à toucher.
|
||||
|
|
|
|||
Loading…
Reference in a new issue