# Architecture frontend — Projet Batch-cooking
> Documentation de l'organisation d'`apps/web` : structure des dossiers, routing,
> gestion des erreurs, et conventions de style (SCSS/theming).
---
## Structure des dossiers
```
apps/web/src/
├── api/
│ └── client.ts # ApiClient — appels fetch vers l'API (voir error-handling.md)
├── 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.*, 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, 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/
│ ├── LoginPage.tsx / .scss (via auth-form.scss, partagé)
│ ├── 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 # 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)
├── lib/
│ └── zod-errors.ts # utilitaire : erreurs zod → { champ: message }
├── App.tsx # table de routes
└── main.tsx # point d'entrée : providers (Router, AuthProvider) + imports i18n/CSS globaux
```
**Règle de placement des styles** : un style spécifique à un seul composant/page vit
dans un fichier `.scss` au même niveau que ce composant (`HomePage.tsx` +
`HomePage.scss`). Un style partagé par plusieurs composants d'une même feature vit
dans le dossier de la feature (`features/auth/auth-form.scss`, utilisé par
`LoginPage` et `SignupPage`). Seuls le reset et les tokens globaux vivent dans
`styles/`.
---
## Routing et gardes d'authentification
```mermaid
flowchart TB
START(("Visite de l'app"))
CHECK{"AuthProvider : GET /auth/me"}
START --> CHECK
CHECK -->|"200 (session valide)"| AUTHED["user défini"]
CHECK -->|"401 (pas de session)"| ANON["user = null"]
AUTHED --> LAYOUT["RequireAuth → AppLayout (sidebar)"]
LAYOUT --> ROUTE_HOME["/ → HomePage (planning)"]
LAYOUT --> ROUTE_RECIPES["/recettes → RecipesPage"]
LAYOUT --> ROUTE_SHOPPING["/liste-de-courses → ShoppingListPage"]
LAYOUT --> ROUTE_HOUSEHOLD["/foyer → HouseholdPage"]
AUTHED --> ROUTE_LOGIN_A["/login ou /signup"]
ROUTE_LOGIN_A -->|"RedirectIfAuthenticated"| ROUTE_HOME
ANON --> ROUTE_HOME_A["/, /recettes, ..."]
ROUTE_HOME_A -->|"RequireAuth"| ROUTE_LOGIN["/login"]
ANON --> ROUTE_LOGIN2["/login ou /signup → rendu normal"]
```
- `AuthContext` (`features/auth/AuthContext.tsx`) appelle `GET /auth/me` une seule
fois au montage pour restaurer la session depuis le cookie httpOnly — c'est ce qui
permet à un rechargement de page de garder l'utilisateur connecté.
- `RequireAuth` et `RedirectIfAuthenticated` sont deux gardes de route
(`react-router-dom`) qui lisent cet état : la première protège tout l'espace
connecté (voir `AppLayout` ci-dessous), la seconde protège `/login` et `/signup`
(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 ``, invisible tant que les deux ciblaient "/".
---
## `AppLayout` — sidebar commune à l'espace connecté
`App.tsx` monte **un seul** `RequireAuth` + `AppLayout` comme route parente de
toutes les routes authentifiées (routes imbriquées `react-router-dom`) :
```tsx
}>
} />
} />
} />
} />
```
`AppLayout` (`layouts/AppLayout.tsx`) rend une sidebar (marque, nav des sections,
nom de l'utilisateur + déconnexion en pied de sidebar) et un `` qui affiche
la route enfant matchée via `` — la garde d'auth et le chrome de
navigation ne sont donc écrits qu'une fois, pas dupliqués par page comme
`RequireAuth` l'était individuellement avant cette feature. En dessous de 640px la
sidebar devient une barre horizontale (voir `AppLayout.scss`) — pertinent tôt
puisque l'app est prévue pour être embarquée par Capacitor plus tard (voir le
README racine).
### Sections sans backend — `ComingSoonPage`
`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 (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
réglages, éditables à tout moment, en **hot saving** (retour fonctionnel : pas de
bouton "Enregistrer"). Chaque section sauvegarde peu après la dernière
modification (nom du foyer et allergènes/intolérances debouncés — 600ms/500ms —
régime immédiat) — 3 ressources API indépendantes (`PATCH /house/current`,
`/profile/diet`, `/profile/allergies`), 3 cycles de sauvegarde indépendants.
Déclenché depuis le handler `onChange` de chaque champ, jamais un `useEffect`
générique sur la valeur — un tel effect se déclencherait aussi au chargement
initial (le `GET` peuple le même state), sans distinction propre entre "vient
d'être chargé" et "vient d'être modifié".
- **`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 `