# 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 (common.*, errors.*, auth.*, layout.*, planning.*, recipes.*, shoppingList.*, onboarding.*, household.*, account.*, preferences.*, userPreferences.*, credits.*, catalog.*) ├── services/ │ └── error-message.service.ts # ErrorMessageService — code d'erreur → clé i18next ├── components/ │ └── ui/ # primitives réutilisables partout, voir plus bas │ ├── Dialog.tsx + dialog.scss # modale (élément natif) │ ├── Checkbox.tsx / Radio.tsx # "carte sélectionnable" (CheckboxOption/RadioOption) │ ├── Tooltip.tsx + tooltip.scss # infobulle CSS-only │ └── ComingSoonPage.tsx + .scss # placeholder générique, section sans backend (ex. Liste de courses) — pas une page routée elle-même, un composant que la page routée (pages/shopping-list/ShoppingListPage.tsx) enveloppe ├── features/ │ ├── auth/ # authentification │ │ ├── AuthContext.tsx # état global (profil connecté, login/signup/logout, refreshUser, deleteAccount) │ │ ├── RequireAuth.tsx / RedirectIfAuthenticated.tsx # gardes de route │ │ └── auth-form.scss # styles partagés par LoginPage et SignupPage │ ├── theme/ │ │ └── ThemeContext.tsx # clair/sombre/système, voir plus bas │ ├── profile/ # champs du parcours régime/allergènes/goûts (personnels) │ │ ├── HouseNameField.tsx / DietSelect.tsx / AllergySelect.tsx / DislikedIngredientsField.tsx │ │ └── profile-forms.scss │ ├── house/ # préférences propres au foyer │ │ ├── SourceSelect.tsx # quelles sources externes le foyer voit │ │ └── house-forms.scss │ ├── planning/ │ │ ├── RecipePickerDialog.tsx # dialogue "ajouter au planning" — parcourir/prévisualiser/importer, voir plus bas │ │ └── recipe-picker-dialog.scss │ └── recipes/ # catalogue, import, édition — voir plus bas │ ├── RecipeTable.tsx / RecipeTabs.tsx / RecipeDetailPanel.tsx │ ├── RecipeSourcesPanel.tsx / SourceItemTable.tsx / useEnabledSources.ts │ ├── RecipeImportForm.tsx / recipe-import-draft.ts │ ├── IngredientPicker.tsx / IngredientRow.tsx / StepListEditor.tsx / StepDescription.tsx │ ├── highlight-tech-steps.ts / ingredient-icons.tsx │ ├── DietTagSelect.tsx / DietBadges.tsx / AllergenBadges.tsx / ReproducibleBadge.tsx / FavoriteStarButton.tsx │ └── recipes.scss ├── layouts/ │ ├── AppLayout.tsx + .scss # sidebar (nav, sous-menu Paramètres, menu compte) commune à tout l'espace connecté, voir plus bas │ └── nav-icons.tsx # ré-export nommé des icônes lucide-react utilisées par la sidebar ├── pages/ # un sous-dossier par section routée — jamais tous les fichiers à plat, un seul composant par sous-dossier n'est pas un problème (cohérence de rangement avant tout) │ ├── auth/ │ │ └── LoginPage.tsx / SignupPage.tsx (via features/auth/auth-form.scss, partagé) │ ├── planning/ │ │ └── PlanningPage.tsx + planning-page.scss # grille de la semaine (routée sur "/"), voir plus bas │ ├── recipes/ │ │ ├── RecipesPage.tsx # vue maître-détail du catalogue (routée sur /recettes, /recettes/:id, /recettes/sources/:sourceKey/:externalId) │ │ ├── RecipeFormPage.tsx # création/édition manuelle (/recettes/nouvelle, /recettes/:id/modifier) │ │ └── ImportRecipePage.tsx # route de secours autonome pour un import (/recettes/importer/:sourceKey/:externalId) │ ├── shopping-list/ │ │ └── ShoppingListPage.tsx # enveloppe components/ui/ComingSoonPage.tsx — section sans backend │ ├── settings/ # ancienne HouseholdPage éclatée en 5 pages, voir plus bas │ │ ├── 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 │ ├── OnboardingDietPage.tsx / OnboardingHouseholdPage.tsx / OnboardingSourcesPage.tsx / OnboardingAllergensPage.tsx │ └── onboarding.scss ├── styles/ │ ├── _theme.scss # tokens de design (couleurs, espacements, typographie) — variantes clair/sombre │ └── global.scss # reset minimal + import du theme — importé une seule fois (main.tsx) ├── lib/ │ ├── zod-errors.ts # utilitaire : erreurs zod → { champ: message } │ └── client-key.ts # makeClientKey() — identité React locale/éphémère pour une ligne de brouillon (ingrédient/étape en cours d'édition), jamais envoyée au serveur ; volontairement pas crypto.randomUUID() (indisponible hors contexte sécurisé, ex. Capacitor) ├── App.tsx # table de routes └── main.tsx # point d'entrée : providers (Router, AuthProvider, ThemeProvider) + 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 (`PlanningPage.tsx` + `planning-page.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["/ → PlanningPage"] LAYOUT --> ROUTE_RECIPES["/recettes, /recettes/:id,
/recettes/sources/:sourceKey/:externalId → RecipesPage"] LAYOUT --> ROUTE_RECIPE_FORM["/recettes/nouvelle,
/recettes/:id/modifier → RecipeFormPage"] LAYOUT --> ROUTE_IMPORT["/recettes/importer/:sourceKey/:externalId → ImportRecipePage"] LAYOUT --> ROUTE_SHOPPING["/liste-de-courses → ShoppingListPage"] LAYOUT --> ROUTE_SETTINGS["/parametres/* → pages de paramètres"] ROUTE_OLD["/foyer"] -->|"redirige"| ROUTE_SETTINGS 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 "/". - **Table de routes complète** (`App.tsx`) : sous l'unique parent `RequireAuth`+`AppLayout` — `/` (`PlanningPage`), `/recettes` / `/recettes/:id` / `/recettes/sources/:sourceKey/:externalId` (les trois rendent **le même composant** `RecipesPage`, voir plus bas), `/recettes/nouvelle` / `/recettes/:id/modifier` (`RecipeFormPage`), `/recettes/importer/:sourceKey/:externalId` (`ImportRecipePage`, route de secours autonome), `/liste-de-courses` (`ShoppingListPage`, toujours un stub), et les cinq pages `/parametres/*` (compte, préférences, foyer, préférences-utilisateur, crédits — voir plus bas). `/foyer` (l'URL de 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`. --- ## `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`, voir la table complète plus haut). `AppLayout` (`layouts/AppLayout.tsx`) rend une 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). La sidebar a grandi avec l'app : - **Icônes** (`layouts/nav-icons.tsx`) — ré-export nommé d'icônes `lucide-react` (remplace un ancien jeu de SVG dessinés à la main) : `PlanningIcon`, `RecipesIcon`, `ShoppingListIcon`, `SettingsIcon`, `AccountIcon`, `DietPreferencesIcon`, `HouseholdIcon`, `UserPreferencesIcon`, `CreditsIcon`, `FavoriteIcon`, `PublicIcon`, `SourcesIcon`, `SourceLinkIcon`, `ChevronLeftIcon`. Toujours accompagnée d'un libellé/tooltip, donc `aria-hidden="true"` est posé par chaque appelant. - **Structure** : bloc marque ("batchCooking" complet, réduit à "bC" en mode replié), nav principale (Planning `/`, Recettes `/recettes`, Liste de courses `/liste-de-courses`), un `SettingsMenu` repliable, un `AccountMenu`, un pied de page avec le numéro de version. - **Rail repliable** — `isCollapsed` persisté dans `localStorage` (`batchcooking:sidebarCollapsed`) : un simple toggle de classe (`.app-sidebar.collapsed`) masque les `.label` en CSS ; chaque item garde son icône et gagne un `title` en tooltip. - **`SettingsMenu`** — révèle Compte (`/parametres/compte`), Préférences (`/parametres/preferences`), Foyer (`/parametres/foyer`), Préférences utilisateur (`/parametres/preferences-utilisateur`), Crédits (`/parametres/credits`). Ouvert par défaut si la route courante est déjà sous `/parametres`, replié sinon — indépendant du repli de toute la sidebar. - **`AccountMenu`** — remplace l'ancien simple "bonjour + déconnexion" : un menu déroulant depuis l'avatar/nom en pied de sidebar, avec un raccourci vers `/parametres/compte` et la déconnexion ; se referme après l'une ou l'autre action. - La correspondance route↔nav utilise les clés i18n `layout.nav.` / `layout.settings.nav.` — ajouter une entrée de nav est un item de tableau + une clé de locale, rien d'autre. ### Sections sans backend — `ComingSoonPage` Seule `Liste de courses` (`ShoppingListPage`) n'a pas encore de backend dédié (le module « Calcul batch-cooking » reste `TODO`, voir [batch-cooking-architecture.md](./batch-cooking-architecture.md)) et rend le composant partagé `ComingSoonPage` (`title`/`description`). `Recettes` a maintenant un vrai backend complet (catalogue, import depuis des sources externes, favoris — voir plus bas) et ne passe plus par ce stub. --- ## Parcours profil — onboarding et pages de paramètres Deux surfaces partagent les mêmes composants de champ (`features/profile/` + `features/house/`) : le wizard d'inscription (une fois) et les pages `/parametres/*` (à tout moment). ```mermaid flowchart LR SIGNUP["SignupPage
(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)"] SETTINGSMENU["Sidebar : Paramètres"] --> S1["/parametres/compte"] SETTINGSMENU --> S2["/parametres/preferences"] SETTINGSMENU --> S3["/parametres/foyer"] SETTINGSMENU --> S4["/parametres/preferences-utilisateur"] SETTINGSMENU --> S5["/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 `/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 de bouton "Enregistrer" — chaque section sauvegarde peu après la dernière modification, 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 quand 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`, et **`DislikedIngredientsField`** (nouveau) : champs contrôlés, "dumb" (reçoivent leurs données en props plutôt que de les fetcher). `AllergySelect` est un `
`/`` + grille de cases à cocher, pas un `