- New apps/web/cypress/e2e/home-planning.cy.ts (mocked API, same cy.intercept convention as auth.cy.ts): - sidebar nav between sections + active-link highlighting - user name + logout from the sidebar footer - home planning: empty / loaded (table rows) / error states - specs/frontend-architecture.md: documents AppLayout (single RequireAuth+AppLayout parent route, nested routes via <Outlet />), the ComingSoonPage stub pattern, updated folder tree and i18n namespace list, updated routing diagram. - README.md: new "Accueil, sidebar & sections" section; notes the Cypress-can't-run-headless-here environment limitation (confirmed pre-existing on main) and the docker-vs-local-dev CORS_ORIGIN gotcha hit while manually verifying this feature. Closes out the home-page-after-login feature (5 commits, this PR): GET /planning/current -> AppLayout -> routing/stub pages -> HomePage planning view -> this commit.
9.6 KiB
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.*, 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
├── 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 / HouseholdPage.tsx # fines enveloppes autour de ComingSoonPage
├── 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
flowchart TB
START(("Visite de l'app"))
CHECK{"AuthProvider :<br/>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) appelleGET /auth/meune 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é.RequireAuthetRedirectIfAuthenticatedsont deux gardes de route (react-router-dom) qui lisent cet état : la première protège tout l'espace connecté (voirAppLayoutci-dessous), la seconde protège/loginet/signup(redirige un utilisateur déjà connecté vers/). Les deux affichentnulltant que la vérification initiale est en cours, pour éviter un flash de contenu suivi d'une redirection.
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) :
<Route element={<RequireAuth><AppLayout /></RequireAuth>}>
<Route path="/" element={<HomePage />} />
<Route path="/recettes" element={<RecipesPage />} />
<Route path="/liste-de-courses" element={<ShoppingListPage />} />
<Route path="/foyer" element={<HouseholdPage />} />
</Route>
AppLayout (layouts/AppLayout.tsx) rend une sidebar (marque, nav des sections,
nom de l'utilisateur + déconnexion en pied de sidebar) et un <main> qui affiche
la route enfant matchée via <Outlet /> — 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, 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.
Client API et gestion des erreurs
Voir error-handling.md pour le détail du contrat d'erreurs partagé avec l'API. En résumé côté frontend :
ApiClient(api/client.ts) — classe avec instance unique exportée (apiClient), enveloppefetchaveccredentials: "include"(requis pour que le cookie de session httpOnly parte/revienne, l'API et le web étant sur des origines différentes). LèveApiError(porteuse ducoded'erreur) pour toute réponse non-2xx.ErrorMessageService(services/error-message.service.ts) — convertit uncoded'erreur numérique en clé de traduction, résolue via i18next.
i18n (internationalisation)
i18next + react-i18next — pas de solution maison : tout le texte affiché (libellés de formulaire, boutons, messages d'erreur) vient de fichiers de locale JSON, jamais codé en dur dans un composant.
i18n/i18n.ts— initialise l'instance i18next (langue par défautfr), importé une seule fois pour son effet de bord dansmain.tsx, avant le premier rendu.locales/fr/translation.json— toutes les chaînes françaises, organisées par namespace :errors.*(voir 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, voirComingSoonPageplus haut).- Dans un composant :
const { t } = useTranslation(); t("auth.login.title"). - Ajouter une langue : créer
locales/<lng>/translation.jsonavec les mêmes clés, ajouterresources.<lng>dansi18n/i18n.ts— aucun composant à toucher.
Note sur les fichiers .d.ts
Aucun fichier .d.ts écrit à la main dans apps/web : le
/// <reference types="vite/client" /> généré par défaut par Vite (habituellement
vite-env.d.ts) est remplacé par "types": ["vite/client"] dans
tsconfig.app.json — même effet (typage de import.meta.env, imports d'assets),
sans fichier dédié.
- Côté
apps/api, aucune augmentation de type globale n'est utilisée du tout — voir - backend-architecture.md
- le profil authentifié passe par
res.locals(mécanisme natif d'Express), pas par undeclare globalsurExpress.Request.
SCSS et theming
sass(Dart Sass) est utilisé via le support natif de Vite — aucune config supplémentaire needed au-delà d'avoir le package installé (vite.config.tsfixe juste l'API moderne de Sass pour éviter un warning de dépréciation).styles/_theme.scss— tokens de design exposés en custom properties CSS sur:root(--color-primary,--space-md, etc.), pas en simples variables SCSS : ça les rend disponibles au runtime, pas seulement à la compilation — ce qui permettrait un futur switch de thème (ex. mode sombre) en redéfinissant juste ces variables, sans reconstruire les feuilles de style. Toute nouvelle règle CSS doit référencervar(--token), jamais une couleur/valeur en dur.styles/global.scss— importé une seule fois, dansmain.tsx. Contient uniquement le reset minimal et l'import du thème (@use "./theme"). Rien de spécifique à une page/un composant n'y va.- Les tokens étant des custom properties CSS (pas des variables Sass), ils sont
disponibles globalement au runtime dès que
global.scssa été chargé une fois — un fichier.scssde composant/page les consomme directement viavar(--token), sans avoir besoin de@usele partiel theme (ce serait un import sans effet, puisqu'aucun symbole Sass n'en est consommé). Chaque fichier documente en commentaire à quoi correspond chaque règle un peu non-triviale.