* fix(recipes): corrige plusieurs bugs d'import TheMealDB
- Les instructions TheMealDB numérotées sur leur propre ligne ("1\n\ntexte...\n\n2\n\ntexte...") créaient des étapes parasites ne contenant qu'un chiffre — filtrées désormais (#52).
- Un ingrédient compté sans mot d'unité dans le texte source (ex. "4 Egg Yolks") laissait l'import bloqué sur "Importer" indéfiniment, sans indication visuelle de la ligne en cause — matchUnit retombe maintenant sur l'unité générique "piece" quand une quantité a été extraite, et RecipeImportForm/RecipeFormPage surlignent désormais toute ligne dont l'unité manque, avec un message explicite (#53).
- Ajout de INGREDIENT_LABEL_SYNONYMS_EN pour reconnaître des formulations alternatives fréquentes chez les sources anglophones ("vanilla pod" en plus de "vanilla bean") sans élargir INGREDIENT_LABELS_EN à un tableau pour ses ~550 entrées (#54).
- Effet de bord découvert en vérifiant #53 de bout en bout : deux lignes source résolues vers le même ingrédient catalogue (ex. "Egg Yolks"/"Eggs" -> "Œuf") faisaient planter la création en 500 (contrainte unique recipe_id+ingredient_id) au lieu d'un 400 propre. createRecipeSchema rejette maintenant les ingredientId en double, et le formulaire d'import surligne les doublons avant même de soumettre.
Vérifié de bout en bout dans le navigateur (import réel de la recette "Flan" depuis TheMealDB, jusqu'au planning) en plus des tests ajoutés.
Closes #52, #53, #54
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(layout): la sidebar réduite écrasait la barre mobile
`isCollapsed` (rail icône seule sur desktop) persiste dans localStorage
indépendamment de la largeur de fenêtre — un utilisateur ayant réduit la
sidebar sur desktop puis ouvrant la même session sur mobile (ou réduisant
la fenêtre sous 640px) gardait `.app-sidebar.collapsed` (spécificité
0,2,0 : width 4.25rem, flex-direction column), qui l'emportait sur la
règle mobile `@media (max-width: 640px)` (spécificité 0,1,0) censée passer
la sidebar en barre horizontale pleine largeur.
Le bloc `&.collapsed` est maintenant scopé sous `@media (min-width: 641px)`
— le complément exact du breakpoint mobile — donc il ne s'applique plus du
tout en dessous.
Vérifié dans le navigateur : sidebar collapsed=true dans localStorage,
viewport 375px — la sidebar calcule bien width: 375px / flex-direction:
row (barre horizontale pleine largeur) au lieu de 4.25rem/column.
Closes #27
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs(readme): documente GET /planning?date=, plus /planning/current
Le README documentait encore `GET /planning/current` (401 sans session,
couvre "aujourd'hui"), une route qui n'existe plus — `planning.routes.ts`
ne définit que `GET /planning?date=YYYY-MM-DD` depuis l'introduction de la
grille de semaine complète. Sans session, `/planning/current` renvoie un
404 générique (route inexistante), pas le 401 documenté.
Documente aussi POST/DELETE /planning/items au passage, absents jusqu'ici.
Closes #55
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs: met à jour README et specs/ avec l'état réel du code
Le code avait beaucoup évolué depuis la dernière mise à jour de la
documentation (sources externes, import de recettes, planning en
grille, pages de paramètres, thème, tests Cucumber...) sans que
README.md/specs/*.md ne suivent. Tour complet du code (backend +
frontend) et réécriture :
- specs/batch-cooking-modele.md : schéma de données réécrit depuis
schema.prisma (foyer/admin/invitation, sources, catalogue
ingrédients/unités, techniques détectées, visibilité des recettes).
- specs/backend-architecture.md : foyer, préférences/goûts, planning,
référence, sources externes (adaptateurs/registre/sync), matching
ingrédients/techniques, isolation base de test, suppression de compte.
- specs/frontend-architecture.md : routing complet, sidebar/paramètres,
thème, planning + picker, catalogue + import, composants UI partagés,
tests Cypress+Cucumber.
- specs/batch-cooking-architecture.md : module Import passe de TODO à
implémenté.
- specs/error-handling.md : liste complète des ~19 codes d'erreur.
- README.md : réécriture pour refléter tout ce qui précède, plus la
note (dangereusement obsolète) sur le partage base de test/dev — le
fix existe déjà (apps/api/.env.test), la doc décrivait encore le bug.
* feat(ingredients): ajoute jaune/blanc d'oeuf, coriandre en poudre, viandes hachées
Complète le catalogue d'ingrédients de référence (seed data) :
- jaune d'oeuf / blanc d'oeuf (dairyAndCheese/eggs, aux côtés d'"egg")
- coriandre en poudre (condimentsAndSpices/spices, aux côtés de
corianderSeeds/freshCilantro déjà présents)
- viandes hachées manquantes : veau, porc, agneau (meatAndSeafood/meats,
aux côtés de groundBeef déjà présent), dinde et poulet
(meatAndSeafood/poultry)
Libellés ajoutés dans apps/web/src/locales/fr/translation.json (source
d'affichage) et packages/shared/src/data/catalog-labels-en.ts (matching
anglais pour l'import de recettes depuis des sources comme TheMealDB).
Aucune icône ni régime dédiés : héritent des défauts de leur groupe
(EGG/SPICE/MEAT/POULTRY, mêmes dietUids que leurs groupes respectifs).
282 tests apps/api toujours au vert (resetDatabase() reseed le
catalogue à chaque test).
* fix(i18n): retire le œ ligaturé des libellés français de l'œuf
"Œuf"/"Œufs" (ingrédient, sous-catégorie, allergène) et "Jaune/Blanc
d'œuf" (ajoutés par #60) s'écrivaient avec le œ ligaturé — remplacé par
"oe" (deux lettres) partout où le mot apparaît. Ne touche pas "bœuf"
(mot différent, non concerné).
Le scénario Cucumber recipe-form.feature qui sélectionne l'ingrédient
par son libellé affiché est mis à jour en conséquence.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(recipes): concatène les ingrédients dupliqués à l'import
Suite au retour utilisateur sur #53 (follow-up) : au lieu de bloquer
l'import et de demander à l'utilisateur de retirer une ligne en double
à la main, deux lignes source qui résolvent vers le même ingrédient
catalogue sont désormais fusionnées automatiquement, quantité
concaténée (sommée), avant même que l'écran de revue ne s'affiche.
- mergeDuplicateIngredients (recipe-translation.ts) : même unité des
deux côtés -> somme directe. Unité différente mais même UnitType
(MASS/VOLUME) -> conversion via toBaseFactor avant de sommer, exprimée
dans l'unité de la première ligne. UnitType différent, ou COUNT des
deux côtés (une "pincée" n'est pas une fraction fixe d'une "gousse",
cf. le commentaire de UnitView) -> jamais fusionnées, laissées en
double (createRecipeSchema/RecipeImportForm continuent de les
signaler, filet de sécurité déjà en place). Les lignes non résolues
(ingredientId: null) ne sont jamais fusionnées entre elles.
- rawText concaténé ("100g Sugar + 45g Sugar") pour la traçabilité.
- Branché dans previewSourceItem (sources.service.ts), juste après
translateRecipeIngredients — c'est le seul endroit où des doublons
peuvent apparaître (la création manuelle ne peut pas en produire,
IngredientPicker exclut déjà les ingrédients déjà sélectionnés).
Vérifié via l'API en local (import réel de "Flan" depuis TheMealDB) :
"100g Sugar"/"45g Sugar" -> une seule ligne Sucre, 145g.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* chore(lint): upgrade Biome vers 2.x, active noExplicitAny/noConsole/noFloatingPromises
`@biomejs/biome` passe de 1.9.4 à 2.5.9 (config migrée via `biome migrate
--write`) — nécessaire pour noFloatingPromises, une règle type-aware
apparue en 2.0 (nursery).
- noExplicitAny : déjà "recommended", actif depuis toujours, aucun changement.
- noConsole (biome.json) : bloque tout `console.*` sauf error/warn/info/
debug/table/assert — équivalent à "pas de console.log" sans interdire
les niveaux nommés (voir le nouveau log service dans le prochain commit,
qui centralise justement ces appels).
- noFloatingPromises (nursery) activé explicitement sous `rules.nursery`
sans avoir besoin d'activer le domaine "types" au sens large (ça aurait
aussi allumé des dizaines d'autres règles type-aware type
noUnresolvedImports/noUnnecessaryConditions, hors scope ici).
Le reste du diff, c'est soit du reformatage automatique (import sort, 2.x
ordonne différemment de 1.9.4 — `biome check --write --unsafe`), soit les
corrections des ~20 promesses flottantes que la nouvelle règle a fait
remonter :
- La plupart sont des `navigate(...)` non attendus (react-router v7 type
`navigate` en `void | Promise<void>`) — préfixés `void navigate(...)`,
aucun changement de comportement.
- Trois chargements initiaux en useEffect (OnboardingAllergensPage,
OnboardingDietPage, OnboardingHouseholdPage, HouseholdSettingsPage)
n'avaient jamais de `.catch()` du tout — ajouté (dégradation silencieuse
vers un état vide/par défaut, même raisonnement que le `.catch()` déjà
présent dans OnboardingSourcesPage).
- HouseholdSettingsPage : `loadHouse` était une fonction déclarée à chaque
render (donc une référence différente à chaque fois) utilisée comme
dépendance de useEffect ET passée en callback à des enfants — le
useEffect se re-déclenchait donc à chaque re-render provoqué par son
propre fetch, un vrai bug de boucle infinie de requêtes que
noFloatingPromises a fait remonter indirectement (via
useExhaustiveDependencies). Corrigé avec useCallback([]).
- RecipeDetailPanel : une clé de liste `${index}-...}` sur une liste
statique (draft.steps, sans id stable — DraftRecipeStepView n'en a pas)
— biome-ignore justifié, pas de bug réel.
- recipe.test.ts : variable `agent` non utilisée, retirée.
Vérifié : `pnpm --filter api test` (295/295), `pnpm lint` et `pnpm build`
clean sur tout le repo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(api): ajoute un log service pour les logs de fonctionnement côté serveur
Jusqu'ici, rien ne journalisait quoi que ce soit côté serveur : aucune
trace au démarrage à part un console.log ad hoc, et surtout aucune trace
des requêtes ni des erreurs gérées par ErrorHandlerService — un 500 en
production n'aurait laissé aucune trace exploitable.
- LoggerService (apps/api/src/lib/logger.service.ts) — classe (public
debug/info/warn/error, private emit), même convention que
ErrorHandlerService (packages/error-tools) : instance unique partagée
exportée (`export const logger = new LoggerService()`). Émet une ligne
JSON structurée par appel (timestamp/level/message + meta), filtrée par
seuil selon NODE_ENV (debug complet en dev, warn+ pendant les tests
pour ne pas alourdir la sortie de Mocha, info+ en production). Seul
endroit du code autorisé à toucher `console` directement (biome-ignore
justifié), toujours via une méthode nommée — jamais un console.log nu.
- requestLogger (middlewares/request-logger.ts) — une ligne par requête
terminée (méthode/chemin/statut/durée), montée en tout premier dans
app.ts, avant même setupCore (CORS/JSON/cookies), pour englober tout le
pipeline. Niveau déduit du statut (info/warn/error).
- errorLogger (middlewares/error-logger.ts) — monté juste avant
createErrorMiddleware : réutilise errorHandlerService.handle() (pur/
sans effet de bord) pour classifier l'erreur avant que la vraie réponse
ne soit construite, log en warn les 4xx routiniers (validation, 404,
401...) et en error les 5xx/exceptions non prévues (avec la stack).
- error-handler.service.ts : retire le `console.error(error)` ad hoc de
fromUnknownError — errorLogger voit désormais chaque erreur avant que
ce service ne la mappe, donc ce console.error faisait doublon (et
loggait en texte brut, pas en JSON structuré).
- server.ts : le console.log de démarrage passe par logger.info.
Vérifié : pnpm --filter api test (303/303, dont 8 nouveaux tests sur
LoggerService), pnpm lint/build clean, testé en live (pnpm dev:api +
curl) — logs JSON corrects pour un 200, un 404, un 401.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* style: préfixe tous les membres private/protected par _
Convention demandée par l'utilisateur : `emit` -> `_emit`, sur toutes les
classes du repo, pas seulement le nouveau code. `public` reste sans
préfixe.
- LoggerService (apps/api) : _minSeverity, _emit.
- ApiClient (apps/web) : _request (39 sites d'appel mis à jour).
- ErrorHandlerService (packages/error-tools) : _fromZodError,
_fromHttpError, _fromUnknownError.
- ExpressServer (packages/express-tools) : _app, _registeredRoutes.
Aucun changement de comportement — pur renommage interne, aucune méthode
private/protected n'était appelée depuis l'extérieur de sa classe.
Vérifié : pnpm --filter api test (303/303), pnpm lint/build clean sur
tout le repo (apps/api, apps/web, packages/*).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs(specs): documente les conventions de développement du repo
Nouveau specs/dev-conventions.md — jusqu'ici ces règles n'existaient que
dans l'historique de commits/PR (classes vs objets littéraux pour la
logique de service, préfixe _ sur private/protected, règles Biome
actives, log service, tests sans mocks de la DB, conventions git/PR...),
rien de centralisé pour un futur contributeur (humain ou Claude Code).
Référencé depuis README.md, section "Qualité / Tests".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(web): regroupe pages/ par section au lieu d'un dossier à plat
pages/ mélangeait 8 fichiers directement à sa racine (LoginPage,
SignupPage, PlanningPage+scss, RecipesPage, RecipeFormPage,
ImportRecipePage, ShoppingListPage, ComingSoonPage+scss) à côté de deux
sous-dossiers déjà groupés (onboarding/, settings/) — incohérent, et
difficile à parcourir une fois le nombre de pages monté. Un sous-dossier
par section routée, même règle que onboarding/settings existants :
- pages/auth/ — LoginPage, SignupPage
- pages/planning/ — PlanningPage + planning-page.scss
- pages/recipes/ — RecipesPage, RecipeFormPage, ImportRecipePage
- pages/shopping-list/ — ShoppingListPage
ComingSoonPage (+ .scss) déménage vers components/ui/ — ce n'est pas une
page routée elle-même (ShoppingListPage l'enveloppe), c'est un composant
UI générique réutilisable, sa place est aux côtés de Dialog/Tooltip/etc.,
pas dans pages/.
Chemins relatifs internes de chaque fichier déplacé mis à jour (un niveau
de profondeur en plus), imports dans App.tsx repointés, tri Biome
réappliqué. specs/frontend-architecture.md mis à jour (arborescence +
références de chemin).
Vérifié : pnpm build clean (apps/web, 1952 modules), pnpm lint clean sur
tout le repo, testé en live dans le navigateur (login/signup, planning,
recettes, nouvelle recette, liste de courses, paramètres) — aucune route
cassée.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(web): regroupe features/recipes/ par sous-domaine au lieu d'un dossier à plat
20 fichiers à plat -> badges/ (DietTagSelect, DietBadges, AllergenBadges,
ReproducibleBadge, FavoriteStarButton), ingredients/ (IngredientPicker,
IngredientRow, ingredient-icons), steps/ (StepListEditor, StepDescription,
highlight-tech-steps), sources/ (RecipeSourcesPanel, SourceItemTable,
RecipeImportForm, recipe-import-draft, useEnabledSources).
RecipeTable/RecipeTabs/RecipeDetailPanel et recipes.scss restent à la
racine (composants transverses aux sous-dossiers, partagés par plusieurs
d'entre eux). Chemins relatifs corrigés dans les fichiers déplacés et chez
tous leurs importeurs externes (pages/recipes/*, features/planning/
RecipePickerDialog.tsx, features/profile/DislikedIngredientsField.tsx),
doc mise à jour (specs/frontend-architecture.md, specs/batch-cooking-
modele.md).
Vérifié : tsc --noEmit, biome check, build complet, 303 tests API,
vérification live navigateur (planning, /recettes, /recettes/nouvelle).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(api): regroupe lib/ par sous-domaine au lieu d'un dossier à plat
9 fichiers à plat -> recipe-sources/ (recipe-source-adapter, recipe-source-
errors, recipe-source-registry) et recipe-matching/ (recipe-translation,
ingredient-matcher, tech-step-matcher). jwt.ts, safe-profile.ts et
logger.service.ts restent à la racine de lib/ (pas de sous-domaine
partagé avec les autres).
Chemins relatifs corrigés dans les fichiers déplacés (profondeur +1 vers
db/) et chez tous leurs importeurs (modules/sources, modules/recipe,
sources/*, db/recipe-source-sync.ts, 12 fichiers de test), doc mise à
jour (specs/backend-architecture.md, specs/batch-cooking-architecture.md).
Vérifié : tsc --noEmit, biome check, build complet, 303 tests API.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(api): regroupe test/ par sous-domaine, miroir de src/lib/
18 fichiers à plat -> recipe-matching/ (ingredient-matcher, recipe-
translation, tech-step-matcher — miroir de lib/recipe-matching/),
recipe-sources/ (json-ld-recipe, recipe-source, recipe-source-sync,
the-meal-db — miroir de lib/recipe-sources/), sources/ (sources,
sources-index — module + registration src/sources/index.ts).
Les tests par domaine API sans regroupement naturel (auth, health,
house, logger.service, planning, preferences, profile, recipe,
reference) restent à la racine de test/, un fichier par domaine — même
logique que jwt.ts/safe-profile.ts restés à la racine de lib/.
Chemins relatifs corrigés (../src/ -> ../../src/, ../test-support/ ->
../../test-support/ dans les fichiers déplacés qui appellent
resetDatabase). .mocharc.json ("test/**/*.test.ts") couvre déjà les
sous-dossiers, aucun changement de config nécessaire.
Vérifié : biome check, 303 tests API.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(convention): impose try/catch autour de chaque await/corps async
Nouvelle règle de dev : aucun await nu, et un corps de fonction/méthode
async doit intégralement vivre dans un try/catch (pas seulement la ou
les lignes qui awaitent). Documentée dans specs/dev-conventions.md avec
son périmètre (code applicatif — services/hooks/composants/middlewares
— routes *.routes.ts exemptées car déjà couvertes par
wrapAsyncHandler ; tests et scripts one-off exemptés aussi).
Appliqué rétroactivement à tout le code applicatif qui ne l'était pas
déjà :
- api : auth/house/profile/preferences/planning/reference/recipe/
sources .service.ts, recipe-source-sync.ts, recipe-translation.ts,
ingredient-matcher.ts, tech-step-matcher.ts, json-ld-recipe.ts,
the-meal-db.ts — un try/catch par fonction async, rethrow simple
(le middleware d'erreur logge déjà tout centralement, voir
error-logger.ts) sauf quand un catch avait déjà une logique propre
(ex. le retry de createHouse).
- web : api/client.ts (_request), AuthContext.tsx, ThemeContext.tsx,
AppLayout.tsx (handleLogout), HouseholdSettingsPage.tsx (handleCopy/
handleRemove/handleDelete/handleLeave) — la plupart des handlers de
formulaire avaient déjà ce pattern, seuls ceux qui laissaient un
await nu ont été corrigés.
lint/complexity/noUselessCatch désactivé dans biome.json (interdisait
justement le catch-qui-rethrow que cette convention impose).
Vérifié : tsc --noEmit (api+web), biome check (0 erreur, repo entier),
build complet, 303 tests API, vérification live navigateur (thème,
déconnexion, copie du code d'invitation).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(web): corrige l'import cassé de highlight-tech-steps.cy.tsx
Oubli lors du regroupement de features/recipes/ par sous-domaine
(refactor(web): regroupe features/recipes/...) : le déplacement de
highlight-tech-steps.ts vers features/recipes/steps/ n'avait pas été
répercuté dans ce test composant Cypress (hors de apps/web/src, donc
raté par la recherche de référence externe à l'époque) — faisait
planter le job e2e en CI ("Failed to fetch dynamically imported
module").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
774 lines
46 KiB
Markdown
774 lines
46 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 (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 <dialog> 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 ; sous-dossiers par sous-domaine, pas de fichiers à plat
|
||
│ ├── RecipeTable.tsx / RecipeTabs.tsx / RecipeDetailPanel.tsx # racine : composants transverses au sous-domaine (utilisés par plusieurs des sous-dossiers ci-dessous)
|
||
│ ├── recipes.scss # feuille de style partagée, importée depuis chaque sous-dossier via ../recipes.scss
|
||
│ ├── badges/
|
||
│ │ └── DietTagSelect.tsx / DietBadges.tsx / AllergenBadges.tsx / ReproducibleBadge.tsx / FavoriteStarButton.tsx
|
||
│ ├── ingredients/
|
||
│ │ └── IngredientPicker.tsx / IngredientRow.tsx / ingredient-icons.tsx
|
||
│ ├── steps/
|
||
│ │ └── StepListEditor.tsx / StepDescription.tsx / highlight-tech-steps.ts
|
||
│ └── sources/
|
||
│ └── RecipeSourcesPanel.tsx / SourceItemTable.tsx / RecipeImportForm.tsx / recipe-import-draft.ts / useEnabledSources.ts
|
||
├── 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 :<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["/ → PlanningPage"]
|
||
LAYOUT --> ROUTE_RECIPES["/recettes, /recettes/:id,<br/>/recettes/sources/:sourceKey/:externalId → RecipesPage"]
|
||
LAYOUT --> ROUTE_RECIPE_FORM["/recettes/nouvelle,<br/>/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 `<Navigate>`, 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 `<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).
|
||
|
||
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.<clé>` /
|
||
`layout.settings.nav.<clé>` — 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<br/>(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 `<fieldset>`/`<legend>` + grille de cases à cocher,
|
||
pas un `<select multiple>` — bien plus repérable/tapable, notamment sur
|
||
mobile. Prend un `legend` en prop : chaque page consommatrice le rend
|
||
**deux fois** (allergies / intolérances, filtrées côté client via
|
||
`AllergyView.kind`), mais la sélection reste une seule liste d'IDs partagée
|
||
entre les deux groupes. `DislikedIngredientsField` — recherche + ajout +
|
||
puces retirables pour la liste personnelle d'ingrédients "pas aimés" ;
|
||
réutilise `IngredientPicker` (voir plus bas) tel quel. **Explicitement
|
||
distinct d'`AllergySelect`** : une préférence de **goût**, jamais une
|
||
contrainte médicale — ne déclenche jamais un avertissement de sécurité,
|
||
juste un badge 🚫 discret sur la fiche recette (`RecipeDetailPanel`).
|
||
Sauvegardé via `GET`/`PATCH /profile/disliked-ingredients` (remplacement
|
||
complet, pas de fusion).
|
||
|
||
### Deux bugs de state trouvés en testant dans le navigateur
|
||
|
||
1. **Course entre `navigate()` et `RedirectIfAuthenticated`** — voir la note sur
|
||
`RedirectIfAuthenticated` plus haut. `SignupPage` doit maintenant rediriger vers
|
||
`/onboarding/regime`, pas `/`, ce qui a rendu visible une course de state
|
||
auparavant invisible.
|
||
2. **`user.dietId` périmé sur les pages de paramètres** — l'ancienne page
|
||
combinée initialisait le régime affiché depuis `useAuth().user.dietId`, un
|
||
instantané d'`AuthContext` jamais rafraîchi après une modification faite
|
||
directement via `apiClient` (qui ne touche pas le contexte). Une navigation
|
||
SPA aller-retour sans rechargement complet ré-affichait donc l'ancienne
|
||
valeur après une sauvegarde. Fix, toujours en place dans `PreferencesPage` :
|
||
la page fetch son propre profil frais (`apiClient.me()`) au montage plutôt
|
||
que de dépendre du contexte, et `AuthContext.refreshUser()` (re-fetch
|
||
`GET /auth/me`) est appelée après une sauvegarde réussie du régime — pour
|
||
que le reste de l'app (pas seulement cette page) reste cohérent.
|
||
|
||
---
|
||
|
||
## Thème (clair/sombre/système)
|
||
|
||
`features/theme/ThemeContext.tsx` — ce que la note "future switch de thème"
|
||
plus bas (SCSS et theming) anticipait est maintenant réel.
|
||
|
||
- `ThemePreference` = `"LIGHT" | "DARK" | "SYSTEM"` (`packages/shared`'s
|
||
`THEME_PREFERENCES`). `ThemeProvider` doit être imbriqué **dans**
|
||
`AuthProvider` (il lit `useAuth().user`).
|
||
- L'effet qui charge la préférence est indexé sur **`user?.id`**, pas sur
|
||
`user` lui-même — `AuthContext`'s `user` change de référence à chaque
|
||
`refreshUser()`, ce qui ne doit pas redéclencher un fetch des préférences
|
||
(documenté par un commentaire `biome-ignore
|
||
lint/correctness/useExhaustiveDependencies`).
|
||
- Sans utilisateur : retombe sur `"SYSTEM"`. Avec un utilisateur :
|
||
`apiClient.getPreferences()` (`GET /preferences`), échec avalé
|
||
silencieusement — même posture "non fatale" que `AuthContext`'s propre
|
||
`me()`.
|
||
- **`applyTheme(theme)`** : `SYSTEM` **supprime** l'attribut
|
||
`document.documentElement.dataset.theme` plutôt que d'y écrire la chaîne
|
||
littérale `"system"` — sans attribut `data-theme`, la media query
|
||
`prefers-color-scheme` de `_theme.scss` reprend la main naturellement.
|
||
`LIGHT`/`DARK` posent `data-theme="light"`/`"dark"`.
|
||
- **`setTheme(newTheme)`** : appelle `apiClient.updatePreferences(theme)`
|
||
(`PATCH /preferences`) **avant** de mettre à jour l'état local — la
|
||
persistance est **côté serveur** (via l'API/la base), contrairement au repli
|
||
de la sidebar qui, lui, ne vit que dans `localStorage`.
|
||
- Consommé par `UserPreferencesPage` (`/parametres/preferences-utilisateur`)
|
||
via `useTheme()` — un simple groupe de boutons radio (`RadioOption` ×
|
||
`THEME_PREFERENCES`).
|
||
|
||
---
|
||
|
||
## Pages de paramètres (`/parametres/*`)
|
||
|
||
L'ancienne `HouseholdPage` combinée est éclatée en 5 pages dédiées
|
||
(`pages/settings/*.tsx`, styles partagés `settings-pages.scss`) :
|
||
|
||
- **`AccountSettingsPage`** (`/compte`) — identité en **lecture seule**
|
||
(prénom/nom/email, éditer n'est pas encore une fonctionnalité demandée) plus
|
||
une "zone dangereuse" : suppression du compte après re-saisie du mot de
|
||
passe (`useAuth().deleteAccount(password)` → `DELETE /auth/me`).
|
||
- **`PreferencesPage`** (`/preferences`, "Préférences alimentaires") — le
|
||
volet **personnel** : régime (`DietSelect`, sauvegarde immédiate +
|
||
`refreshUser()`), allergies/intolérances (deux `AllergySelect`, debounce
|
||
500ms), et `DislikedIngredientsField` (debounce 500ms) — le pendant
|
||
"toujours modifiable" des étapes régime/allergènes du wizard, mêmes
|
||
composants de champ.
|
||
- **`HouseholdSettingsPage`** (`/foyer`) — le volet **foyer**. Deux layouts
|
||
selon `useAuth().user.houseId` :
|
||
- **Sans foyer** : créer (`POST /house`) ou rejoindre par code d'invitation
|
||
(`POST /house/join`).
|
||
- **Avec foyer** : nom renommable (debounce 600ms, `PATCH /house/current`),
|
||
code d'invitation affiché + copie presse-papier, liste des membres avec
|
||
badge admin, bouton de retrait réservé à l'admin (`DELETE
|
||
/house/members/:id`), une section **Sources** (`SourceSelect` — n'importe
|
||
quel membre, pas seulement l'admin, peut activer/désactiver quelles
|
||
sources externes le foyer voit, debounce 500ms, `PATCH
|
||
/house/current/sources`), et soit la suppression du foyer (admin,
|
||
confirmation en deux temps, `DELETE /house/current`) soit le départ
|
||
(non-admin, sans confirmation puisque ça n'affecte que le partant, `POST
|
||
/house/leave`). Recharge `GET /house/current` après chaque mutation
|
||
plutôt qu'un patch optimiste — délibéré, ce sont des actions peu
|
||
fréquentes/réfléchies.
|
||
- **`UserPreferencesPage`** (`/preferences-utilisateur`) — juste le thème
|
||
(voir la section précédente). Nom volontairement distinct de `/preferences`
|
||
malgré la collision terminologique : "comment l'app se présente" vs "les
|
||
contraintes alimentaires du foyer".
|
||
- **`CreditsPage`** (`/credits`) — page d'attribution pure, existe pour
|
||
satisfaire la licence CC BY 4.0 du jeu d'icônes d'ingrédients
|
||
(foodiconpack.com, voir plus bas).
|
||
|
||
---
|
||
|
||
## Planning (`/`)
|
||
|
||
`pages/planning/PlanningPage.tsx` (+ `planning-page.scss`) — remplace l'ancienne
|
||
`HomePage`. Grille complète de la semaine, pas un simple tableau du jour :
|
||
7 colonnes (jours) × 5 lignes (`petit-dejeuner`, `collation`, `dejeuner`,
|
||
`gouter`, `diner` — `WEEK_DAYS`/`MEALS` de `packages/shared`), avec un
|
||
regroupement visuel "moments de la journée" (matin/midi/après-midi/soir) via
|
||
une bordure appuyée après `collation`/`dejeuner`/`gouter`.
|
||
|
||
- **Navigation de semaine** — `WeekNavigator` (flèches précédent/suivant,
|
||
`addWeeks(weekStart, ±1)` de `@batch-cooking/date-tools`) + un libellé
|
||
cliquable ouvrant un `CalendarPopover` (grille mensuelle via
|
||
`buildCalendarMonth`, cliquer un jour saute à sa semaine, lundi-first). Un
|
||
badge "aujourd'hui" s'affiche quand la semaine visible est la semaine
|
||
courante.
|
||
- `GET /planning?date=YYYY-MM-DD` renvoie `PlanningView | null` — `null` est
|
||
un état normal (rien à afficher pour cette semaine), pas un message
|
||
d'erreur séparé : les boutons "+" de chaque case suffisent à communiquer
|
||
l'état vide.
|
||
- **Ajouter une recette** — le "+" d'une case ouvre `RecipePickerDialog` pour
|
||
ce créneau `(date, weekDay, meal)`, monté **conditionnellement** (seulement
|
||
tant que la case est ouverte) pour que son état interne reparte à zéro à
|
||
chaque ouverture, sans reset manuel.
|
||
- **Mise à jour locale** — après ajout/retrait, `planning.items` est patché
|
||
côté client plutôt que refetché (un objet `Planning` factice `id: -1` est
|
||
construit si aucun n'existait encore, puisque seul `.items` est jamais lu
|
||
sur cette page). Le retrait est **optimiste** (retiré localement
|
||
immédiatement, `DELETE /planning/items/:id`, restauré en cas d'échec).
|
||
- Chaque recette planifiée s'affiche en pastille `"{nom} · ×{portions}"` avec
|
||
un bouton de retrait (✕).
|
||
|
||
### `RecipePickerDialog` — parcourir, prévisualiser, importer
|
||
|
||
`features/planning/RecipePickerDialog.tsx` (+ `recipe-picker-dialog.scss`) —
|
||
un seul dialogue, jusqu'à 3 étapes, conçu autour d'un principe : **on
|
||
prévisualise avant de confirmer**, rien n'est engagé par un simple clic de
|
||
ligne.
|
||
|
||
1. **Parcourir** (étape par défaut) — embarque `RecipeTabs` +
|
||
`RecipeTable`/`RecipeDetailPanel` (onglets réguliers) ou
|
||
`RecipeSourcesPanel` (onglet d'une source) : exactement l'UI du catalogue
|
||
`/recettes`, avec en plus trois filtres propres au contexte "je cherche
|
||
quoi cuisiner" (pas juste "je consulte") : un filtre multi-ingrédients
|
||
(`IngredientPicker`), un filtre multi-régimes (`DietTagSelect`), et une
|
||
case "convient à tout le foyer" (affichée seulement si le viewer a un
|
||
foyer) — tous branchés sur les query params de `GET /recipes`. Cliquer une
|
||
ligne **sélectionne/prévisualise seulement**, jamais ne valide — un pied de
|
||
dialogue épinglé ("Confirmer", actif dès qu'une prévisualisation existe)
|
||
est ce qui agit réellement.
|
||
2. **Confirmer les portions** (une vraie recette est prévisualisée) — petit
|
||
formulaire "combien de portions ?" (pré-rempli depuis
|
||
`RecipeSummaryView.portions`), puis `POST /planning/items`.
|
||
3. **Revue intégrée** (un item de source *pas encore importé* est prévisualisé
|
||
et nécessite une intervention humaine) — rend `RecipeImportForm`
|
||
directement à l'intérieur du même `Dialog` (`planningSlot` transmis pour
|
||
qu'un import réussi ajoute aussi au planning en un seul submit) : évite de
|
||
naviguer vers `ImportRecipePage` et de perdre la recherche/les filtres/le
|
||
créneau du picker.
|
||
|
||
**L'import transparent** — décrit dans le composant comme "la seule action de
|
||
toute l'app qui importe vraiment un item de source... puisqu'un item de
|
||
source ne devient une vraie Recipe sauvegardée qu'en conséquence du fait que
|
||
quelqu'un l'ajoute à son planning" :
|
||
1. `GET /sources/:sourceKey/preview/:externalId` → `RecipeImportDraftView`.
|
||
2. `tryBuildCompleteImport(draft)` (`recipe-import-draft.ts`) tente de
|
||
construire un `CreateRecipeInput` soumissible **sans aucun formulaire, sans
|
||
personne impliquée** — renvoie `null` dès qu'un jugement humain est
|
||
nécessaire (une ligne d'ingrédient non résolue, `portions` manquant).
|
||
3. Si non-null : `POST /sources/:sourceKey/import/:externalId` puis `POST
|
||
/planning/items` — ajouté au créneau **sans écran supplémentaire**, comme
|
||
n'importe quelle autre recette.
|
||
4. Si `tryBuildCompleteImport` renvoie `null`, ou si l'un des deux appels
|
||
échoue : bascule sur l'étape 3 ci-dessus (revue intégrée).
|
||
5. Cas limite géré explicitement : si l'import réussit mais que l'ajout au
|
||
planning échoue ensuite, la recette est **déjà sauvegardée** — plutôt que
|
||
de retenter tout l'import, navigation vers `/recettes/:id` (même repli que
|
||
`RecipeImportForm`'s propre submit, voir plus bas).
|
||
|
||
---
|
||
|
||
## Recettes — catalogue, favoris, import depuis une source externe
|
||
|
||
`pages/recipes/RecipesPage.tsx` — routée sur `/recettes`, `/recettes/:id` **et**
|
||
`/recettes/sources/:sourceKey/:externalId` (le **même composant** pour les
|
||
trois) : une vue **maître-détail**, pas une navigation vers une page séparée —
|
||
la barre d'onglets + le tableau restent montés, seul le panneau de détail
|
||
change avec le paramètre d'URL.
|
||
|
||
### Onglets — `RecipeTabs.tsx`
|
||
|
||
Quatre onglets réels, en base — `favoris` / `perso` / `foyer` / `publique` —
|
||
pas de "toutes" : toute recette tombe sous exactement un des trois derniers
|
||
via sa propre `visibility`, `favoris` est un filtre transverse orthogonal.
|
||
**Plus un onglet par source externe activée pour le foyer** (chaque source
|
||
activée devient sa propre tab) — valeur `"source:<key>"`, icône propre à la
|
||
source (`iconUrl` si elle en a un, sinon `SourcesIcon` générique), nom non
|
||
traduit (nom propre). Concept **propre au web** : absent du type `RecipeTab`
|
||
partagé, l'API n'a pas de valeur `tab=source:...` — parcourir une source est
|
||
un endpoint entièrement différent (`GET /sources/:sourceKey/browse`).
|
||
`useEnabledSources.ts` (`Promise.all([getSources(), getHouseSourceIds()])`)
|
||
calcule la liste des sources activées, partagé par `RecipesPage` et
|
||
`RecipePickerDialog`.
|
||
|
||
### Parcourir une source — `RecipeSourcesPanel.tsx`
|
||
|
||
Contenu de l'onglet d'une source : sa propre paire maître-détail —
|
||
`SourceItemTable` (liste paginée, `GET /sources/:sourceKey/browse?query=&cursor=`,
|
||
`nextCursor`) + `RecipeDetailPanel` pour la prévisualisation. Scopé à un seul
|
||
`sourceKey` (prop fixe) — remonté avec `key={sourceKey}` en changeant de
|
||
source, même convention "monté seulement tant que pertinent" que `Dialog`.
|
||
Clic sur une ligne :
|
||
- **Déjà importé** (`alreadyImported && recipeId !== null`) : `GET
|
||
/recipes/:id`, prévisualisé en état `"loaded"`.
|
||
- **Pas encore importé** : `GET /sources/:sourceKey/preview/:externalId`,
|
||
prévisualisé en état `"loaded-draft"`.
|
||
|
||
### Flux d'import complet (parcourir → prévisualiser → revue → sauvegarder)
|
||
|
||
- **Prévisualisation** — `RecipeDetailPanel`'s état `"loaded-draft"` : photo,
|
||
icône de lien vers la source (`SourceLinkIcon`, ouvre l'URL d'origine dans
|
||
un nouvel onglet), nom, portions, description, étapes (avec surlignage des
|
||
techniques, voir plus bas) — **pas** d'actions favori/modifier/supprimer, et
|
||
volontairement pas de bouton "importer" manuel : un item de source n'est
|
||
sauvegardé qu'en conséquence de son ajout au planning (voir
|
||
`RecipePickerDialog` plus haut) ou d'une soumission de revue explicite.
|
||
- **Formulaire de revue — `RecipeImportForm.tsx`** — pré-rempli depuis le
|
||
brouillon, structurellement identique à `RecipeFormPage` (mêmes
|
||
`IngredientRow`/`IngredientPicker`/`StepListEditor`/`DietTagSelect`, même
|
||
forme de payload `CreateRecipeInput`), plus une section **"à compléter"**
|
||
pour les lignes d'ingrédient non résolues automatiquement
|
||
(`ingredient-matcher.ts`, côté API) : chaque ligne montre son texte brut, un
|
||
bouton "Choisir un ingrédient" (ouvre `IngredientPicker`, la quantité brute
|
||
est conservée) ou un bouton pour l'écarter. `canSubmit` exige zéro ligne non
|
||
résolue restante — **aucune recette invalide n'est jamais silencieusement
|
||
devinée/abandonnée** (décision produit explicite). Soumet vers `POST
|
||
/sources/:sourceKey/import/:externalId` au lieu de `POST /recipes`.
|
||
Prop optionnelle `planningSlot` : en cas de succès, appelle aussi `POST
|
||
/planning/items` avec les portions du formulaire.
|
||
- **`RecipeImportForm` a été extrait d'`ImportRecipePage`** pour que
|
||
`RecipePickerDialog` puisse l'embarquer directement comme une de ses étapes
|
||
— `pages/recipes/ImportRecipePage.tsx` (`/recettes/importer/:sourceKey/:externalId`)
|
||
n'en est plus que le wrapper d'une **route de secours autonome et
|
||
partageable** (favori enregistré, page rechargée en plein milieu du flux),
|
||
plus le chemin principal. Elle décode toujours défensivement
|
||
`?planningDate=&planningWeekDay=&planningMeal=` (validés contre
|
||
`WEEK_DAYS`/`MEALS`) pour ce chemin historique.
|
||
|
||
### Surlignage des techniques et infobulle
|
||
|
||
- `highlight-tech-steps.ts`'s `splitDescriptionByTechSteps(description,
|
||
techSteps)` découpe une description d'étape en segments texte/technique à
|
||
partir des offsets `start`/`end` de chaque `StepTechStepView` (calculés
|
||
côté API par `matchTechStepSpans`, voir
|
||
[backend-architecture.md](./backend-architecture.md#détection-des-techniques--tech-step-matcherts)).
|
||
Trie défensivement par `start` et élimine silencieusement tout span aux
|
||
bornes invalides (négatif, hors texte, chevauchant un span déjà accepté) —
|
||
dégrade en "ne pas surligner celui-ci" plutôt que de planter/déformer
|
||
l'affichage.
|
||
- `StepDescription.tsx` rend les segments : texte brut tel quel, technique
|
||
entourée d'un vrai `<button type="button">` (pas un `<mark>` — nativement
|
||
focusable au clavier/lecteur d'écran) dans un `Tooltip`
|
||
(`components/ui/Tooltip.tsx`) dont le contenu est
|
||
`t(\`catalog.techSteps.${techStep.key}\`)`.
|
||
|
||
### Icônes d'ingrédients et badge "reproductible"
|
||
|
||
- `ingredient-icons.tsx` — ~20 pictogrammes génériques (remplace un ancien
|
||
schéma à un emoji par ingrédient, 437 cas, jugé peu professionnel/incohérent
|
||
en revue produit), groupés par **type de chose** (légume, bouteille,
|
||
fromage…) plutôt que par ingrédient précis. La plupart viennent du pack CC
|
||
BY 4.0 de foodiconpack.com (voir `CreditsPage`) via un wrapper `FilledIcon`
|
||
(glyphes pleins) ; trois (`BreadIcon`, `DoughIcon`, `SproutIcon`) sans bon
|
||
équivalent dans ce pack restent dessinés à la main (wrapper `Icon`,
|
||
traits) — dimensionnés en CSS pour que le mélange se lise comme un seul jeu
|
||
cohérent. `CATEGORY_ICON`/`SUBCATEGORY_ICON` donnent une icône
|
||
*représentative* par catégorie/sous-catégorie pour les lignes de
|
||
`IngredientPicker` (un choix éditorial, pas une donnée dérivée).
|
||
- `ReproducibleBadge.tsx` — rien si `!reproducible`
|
||
(`IngredientView.reproducible`, "raisonnablement faisable maison"). Pastille
|
||
simple dans la grille du picker, ou (avec `searchLabel`, dans
|
||
`IngredientRow`) un lien `<a>` classique (pas un `<Link>` router,
|
||
volontairement, pour ne jamais faire quitter un formulaire de recette en
|
||
cours) ouvrant `/recettes?search=<nom>` dans un nouvel onglet.
|
||
|
||
### Badges régime/allergènes et autres pièces
|
||
|
||
- `AllergenBadges.tsx` / `DietBadges.tsx` — listes de pastilles simples,
|
||
rendent `null` sur un tableau vide. `DietBadges` utilise le token
|
||
`--color-tag` (jamais `--color-allergen`) pour rester visuellement distinct
|
||
d'un avertissement de sécurité.
|
||
- `DietTagSelect.tsx` — fieldset multi-sélection (via `CheckboxOption`) pour
|
||
taguer manuellement le(s) régime(s) associé(s) d'une recette — utilisé dans
|
||
`RecipeFormPage`, `RecipeImportForm`, et les filtres de `RecipePickerDialog`.
|
||
- `RecipeTable.tsx` — tableau du catalogue (photo/nom+marque favori/allergènes/
|
||
régimes), clic sur une ligne = sélection (pas de navigation, le détail
|
||
s'affiche à côté dans `RecipeDetailPanel`).
|
||
- `RecipeDetailPanel.tsx` — union discriminée `"empty" | "loading" | "loaded" |
|
||
"loaded-draft" | "not-found" | "error"`. Croise les ingrédients de la recette
|
||
avec `dislikedIngredientIds` (préférence de goût du viewer) pour n'afficher
|
||
un badge 🚫 que sur les ingrédients concernés. Prop `showActions` (défaut
|
||
`true`) masque l'étoile favori + les boutons Modifier/Supprimer dans les
|
||
contextes de prévisualisation seule (`RecipePickerDialog`,
|
||
`RecipeSourcesPanel`).
|
||
- `FavoriteStarButton.tsx` — bascule optimiste (`POST`/`DELETE
|
||
/recipes/:id/favorite`, restaurée en cas d'échec).
|
||
- `IngredientPicker.tsx` — parcours à deux niveaux catégorie→sous-catégorie
|
||
(rayons de supermarché) + recherche + grille de cartes, remplace un ancien
|
||
dropdown autocomplete plat (400+ ingrédients, la recherche seule ne suffit
|
||
pas). Un menu "options d'affichage" bascule les badges
|
||
allergène/régime/reproductible par carte (préférence UI locale, pas
|
||
persistée). Réutilisé par `RecipeFormPage`, `RecipeImportForm`, le filtre
|
||
ingrédients de `RecipePickerDialog`, et `DislikedIngredientsField`.
|
||
- `IngredientRow.tsx` / `StepListEditor.tsx` — ligne d'ingrédient sélectionnée
|
||
(icône, nom, quantité, unité, badges, retrait) et éditeur d'étapes ordonné
|
||
(boutons monter/descendre, pas de drag-and-drop) du formulaire recette.
|
||
- `SourceItemTable.tsx` — tableau de parcours d'une source (photo+nom, badge
|
||
"déjà importé" au lieu des colonnes allergènes/régime — un item de source
|
||
n'est résolu contre les catalogues qu'à la prévisualisation).
|
||
|
||
---
|
||
|
||
## Foyer — sources externes activées
|
||
|
||
- **`features/house/SourceSelect.tsx`** — grille de cases à cocher pour quelles
|
||
sources un foyer voit, partagée telle quelle par `OnboardingSourcesPage` et
|
||
`HouseholdSettingsPage`'s section Sources. Chaque ligne : logo optionnel
|
||
(`iconUrl`), nom propre (non traduit), badge officiel/non-officiel
|
||
(`household.sources.official`/`unofficial`) pour juger la fiabilité d'une
|
||
source scrapée vs une API officielle. Sélection vide = état de départ
|
||
normal (aucune ligne `HouseSource` = masqué).
|
||
|
||
---
|
||
|
||
## Composants UI partagés (`components/ui/`)
|
||
|
||
- **`Dialog.tsx`** (+ `dialog.scss`) — primitive de modale bâtie sur l'élément
|
||
**`<dialog>` natif** (`showModal()`), pas une div `role="dialog"` — piège de
|
||
focus, fermeture sur Échap et arrière-plan obtenus gratuitement. Première
|
||
modale de l'app (chaque confirmation avant, ex. les zones dangereuses des
|
||
pages de paramètres, était une révélation en deux temps inline). Montée
|
||
seulement pendant qu'elle est ouverte. Le clic sur l'arrière-plan pour
|
||
fermer compare les coordonnées du clic au rectangle du panneau
|
||
(`getBoundingClientRect()` — un clic sur l'élément `<dialog>` lui-même se
|
||
produit à la fois pour un vrai clic d'arrière-plan *et* pour son propre
|
||
padding non rempli, seule la comparaison de coordonnées distingue les deux),
|
||
attaché impérativement plutôt que via `onClick` JSX (Échap couvre déjà le
|
||
cas clavier). Prop `footer` optionnelle pour un bandeau d'action épinglé
|
||
sous le corps défilant — utilisé par `RecipePickerDialog`.
|
||
- **`Checkbox.tsx`** (`CheckboxOption`) — factorise le balisage "carte
|
||
sélectionnable" (input natif caché + coche + texte) auparavant dupliqué
|
||
entre `AllergySelect`, `DietTagSelect`, le menu d'affichage
|
||
d'`IngredientPicker`. La classe `is-selected` est posée en JS depuis le
|
||
booléen `checked` (un chaînage CSS `:has(:checked)` s'est révélé peu fiable
|
||
entre navigateurs).
|
||
- **`Radio.tsx`** (`RadioOption<T extends string>`) — pendant `type="radio"`
|
||
de `CheckboxOption`, même balisage/apparence, sémantique radio native pour
|
||
l'exclusion mutuelle via un `name` partagé — utilisé par le sélecteur de
|
||
thème d'`UserPreferencesPage`.
|
||
- **`Tooltip.tsx`** (+ `tooltip.scss`) — infobulle **CSS-only** (pas de
|
||
librairie de positionnement) : wrapper `position: relative`, affichée via
|
||
`:hover`/`:focus-within` (aucun état JS). `children` doit être un seul
|
||
élément focusable ; cloné pour y attacher `aria-describedby` (lecteurs
|
||
d'écran). Utilisé par `StepDescription.tsx` pour l'infobulle des techniques.
|
||
- **`ComingSoonPage.tsx`** (+ `.scss`) — placeholder générique (`title`/
|
||
`description`) pour une section routée sans backend, voir
|
||
[Sections sans backend](#sections-sans-backend--comingsoonpage) plus haut.
|
||
|
||
---
|
||
|
||
## 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 de premier niveau : `common` (libellés génériques réutilisés
|
||
partout), `errors` (voir [error-handling.md](./error-handling.md)), `auth`
|
||
(`auth.login.*`/`auth.signup.*`), `layout` (nav de la sidebar dont
|
||
`layout.settings.nav.*` pour le sous-menu Paramètres — `AppLayout`),
|
||
`planning` (grille de la semaine, `RecipePickerDialog`), `recipes`
|
||
(catalogue, tabs, import), `shoppingList` (page stub, voir `ComingSoonPage`
|
||
plus haut), `onboarding` (wizard d'inscription), `household` (titre +
|
||
`form.*`, champs partagés par le wizard et `/parametres/foyer`, plus
|
||
`household.sources.*` pour le badge officiel/non-officiel), `account` /
|
||
`preferences` / `userPreferences` / `credits` (les quatre autres pages de
|
||
paramètres), `catalog` (libellés des tables de référence —
|
||
`catalog.diets.<key>`, `catalog.allergens.<key>`, `catalog.ingredients.<key>`,
|
||
`catalog.units.<key>`, `catalog.techSteps.<key>` — un slug `key` de
|
||
`schema.prisma` par entrée, jamais le libellé lui-même stocké en base, voir
|
||
[batch-cooking-modele.md](./batch-cooking-modele.md)).
|
||
- 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 a permis le switch de thème clair/sombre/système (voir
|
||
[Thème](#thème-clairsombresystème) plus haut) en redéfinissant juste ces
|
||
variables sous `[data-theme="dark"]` (et sous `prefers-color-scheme: dark`
|
||
quand aucun `data-theme` n'est posé, cas `SYSTEM`), 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.
|
||
|
||
---
|
||
|
||
## Tests (Cypress + Cucumber)
|
||
|
||
`apps/web/cypress.config.ts` déclare deux "testing types" indépendants :
|
||
|
||
- **`e2e`** — `specPattern` couvre à la fois les specs Cypress classiques
|
||
(`cypress/e2e/**/*.cy.ts`) **et** des fichiers Gherkin
|
||
(`cypress/e2e/**/*.feature`), via `@badeball/cypress-cucumber-preprocessor`
|
||
(+ un bundler esbuild).
|
||
- **`component`** (nouveau) — `cypress/component/**/*.cy.tsx`, monte un seul
|
||
composant UI générique à la fois (`components/ui/*`), sans routeur ni
|
||
backend — lancé via le script `cy:run:component` (nouveau, à côté de
|
||
`cy:open`/`cy:run`/`e2e`). Premier test de ce type dans le repo : mounter
|
||
`CheckboxOption` isolément, sans jamais visiter une page routée complète.
|
||
|
||
### Motif Gherkin
|
||
|
||
Pour chaque `.feature` (ex. `planning.feature`), un fichier de steps `.ts` du
|
||
même nom dans le même dossier (`planning.ts`) porte les fixtures/steps
|
||
**propres à cette feature** (mocks `cy.intercept`, interactions DOM
|
||
spécifiques à ce parcours) — délibérément **pas** partagés entre features, le
|
||
lookup de steps du préprocesseur Cucumber n'étant pas global à tout
|
||
`cypress/e2e/`. Features présentes : `account`, `auth`, `household-settings`,
|
||
`onboarding`, `planning`, `preferences`, `recipe-form`, `recipes`,
|
||
`recipe-sources`, `user-preferences`.
|
||
|
||
Les steps réellement partagés (par **toutes** les features) vivent dans
|
||
`cypress/support/step_definitions/` : `common.steps.ts` (connexion, navigation
|
||
générique — ex. `"I am signed in as {string} {string}"`), plus
|
||
`household-mutations.steps.ts`, `profile-mutations.steps.ts`,
|
||
`reference-data.steps.ts`. `common.steps.ts` évite délibérément un hook
|
||
Cucumber `Before()` : l'enregistrer ferait lire par le runtime navigateur du
|
||
préprocesseur un membre d'enum (`messages.HookType.BEFORE_TEST_CASE`) absent
|
||
de la version CommonJS de `@cucumber/messages` sur laquelle ce repo est pinné
|
||
(`pnpm.overrides`) — chaque scénario planterait avec "Cannot read properties
|
||
of undefined". La réinitialisation du profil se fait donc en ligne, dans le
|
||
step "I am signed in as..." par lequel commence de toute façon chaque chaîne
|
||
de scénario.
|
||
|
||
### Migration en cours — `.cy.ts` et `.feature` coexistent
|
||
|
||
Les fichiers `.cy.ts` classiques restants (`account`, `household-settings`,
|
||
`layout`, `planning-page`, `preferences`, `recipes`, `sidebar`, `smoke`,
|
||
`user-preferences`) ne sont **pas** un découpage définitif voulu — c'est une
|
||
migration en cours vers Cucumber. Certains parcours (bascule favori,
|
||
suppression de recette) ont déjà été migrés vers un couple `.feature`+`.ts`
|
||
dédié, laissant dans le `.cy.ts` d'origine ce qui n'est pas encore migré
|
||
(parcours de navigation/affichage plus larges, contrôles structurels de layout,
|
||
le check global "redirection si non authentifié" de `smoke.cy.ts`). Les deux
|
||
styles tournent dans la même commande `cy:run` puisqu'ils partagent le même
|
||
`specPattern`.
|
||
|
||
Toujours vrai par ailleurs (hérité de l'état précédent) : les tests mockent
|
||
l'API via `cy.intercept` plutôt que de dépendre d'un vrai backend — le job e2e
|
||
de la CI ne provisionne pas de Postgres/API, seulement le serveur de dev Vite.
|
||
Le comportement réel de l'API est couvert par la suite Mocha d'`apps/api` (voir
|
||
[backend-architecture.md](./backend-architecture.md), contre une vraie base).
|
||
|
||
> **Cypress ne peut pas tourner en local dans un environnement Windows
|
||
> sandboxé** : Chromium/Electron headless plante au lancement du process GPU
|
||
> (`GPU process isn't usable`) — pas un problème introduit par une
|
||
> modification du code. `pnpm --filter web e2e` fonctionne normalement en CI
|
||
> et sur une machine de dev classique ; dans cet environnement précis, vérifier
|
||
> manuellement via le serveur de dev (`pnpm dev:web` + `pnpm dev:api`, pas le
|
||
> conteneur Docker qui sert le frontend buildé, pas le serveur Vite).
|