batchCooking/specs/frontend-architecture.md
Nicolas 72472581d6 Tests + docs: sidebar/planning Cypress coverage, frontend-architecture.md (step 5/5)
- 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.
2026-08-16 21:09:31 +02:00

190 lines
9.6 KiB
Markdown

# 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
```mermaid
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`) 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.
---
## `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
<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](./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`), enveloppe `fetch` avec `credentials: "include"` (requis pour que
le cookie de session httpOnly parte/revienne, l'API et le web étant sur des
origines différentes). Lève `ApiError` (porteuse du `code` d'erreur) pour toute
réponse non-2xx.
- `ErrorMessageService` (`services/error-message.service.ts`) — convertit un `code`
d'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éfaut `fr`), importé
une seule fois pour son effet de bord dans `main.tsx`, avant le premier rendu.
- `locales/fr/translation.json` — toutes les chaînes françaises, organisées par
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).
- 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.
---
## 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](./backend-architecture.md#auth--reslocals-pas-daugmentation-du-namespace-express)
: le profil authentifié passe par `res.locals` (mécanisme natif d'Express), pas
par un `declare global` sur `Express.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.ts` fixe
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érencer `var(--token)`, jamais une couleur/valeur en dur.
- **`styles/global.scss`** — importé une seule fois, dans `main.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.scss` a été chargé une fois —
un fichier `.scss` de composant/page les consomme directement via `var(--token)`,
sans avoir besoin de `@use` le 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.