# 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 `
`/`` + grille de cases à cocher, pas un `