From 23d4fd4966ad9afb8c1e4f2c77e7cf33a2f293cf Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 21 Aug 2026 10:53:17 +0200 Subject: [PATCH] =?UTF-8?q?refactor(web):=20regroupe=20pages/=20par=20sect?= =?UTF-8?q?ion=20au=20lieu=20d'un=20dossier=20=C3=A0=20plat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/web/src/App.tsx | 14 ++++----- .../ui}/ComingSoonPage.scss | 0 apps/web/src/components/ui/ComingSoonPage.tsx | 26 ++++++++++++++++ apps/web/src/pages/ComingSoonPage.tsx | 23 -------------- apps/web/src/pages/{ => auth}/LoginPage.tsx | 10 +++---- apps/web/src/pages/{ => auth}/SignupPage.tsx | 10 +++---- .../src/pages/{ => planning}/PlanningPage.tsx | 4 +-- .../pages/{ => planning}/planning-page.scss | 0 .../pages/{ => recipes}/ImportRecipePage.tsx | 4 +-- .../pages/{ => recipes}/RecipeFormPage.tsx | 16 +++++----- .../src/pages/{ => recipes}/RecipesPage.tsx | 17 ++++++----- .../{ => shopping-list}/ShoppingListPage.tsx | 2 +- specs/frontend-architecture.md | 30 ++++++++++++------- 13 files changed, 85 insertions(+), 71 deletions(-) rename apps/web/src/{pages => components/ui}/ComingSoonPage.scss (100%) create mode 100644 apps/web/src/components/ui/ComingSoonPage.tsx delete mode 100644 apps/web/src/pages/ComingSoonPage.tsx rename apps/web/src/pages/{ => auth}/LoginPage.tsx (92%) rename apps/web/src/pages/{ => auth}/SignupPage.tsx (94%) rename apps/web/src/pages/{ => planning}/PlanningPage.tsx (98%) rename apps/web/src/pages/{ => planning}/planning-page.scss (100%) rename apps/web/src/pages/{ => recipes}/ImportRecipePage.tsx (96%) rename apps/web/src/pages/{ => recipes}/RecipeFormPage.tsx (95%) rename apps/web/src/pages/{ => recipes}/RecipesPage.tsx (95%) rename apps/web/src/pages/{ => shopping-list}/ShoppingListPage.tsx (82%) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index b5c7695..d3c44ed 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -2,22 +2,22 @@ import { Navigate, Route, Routes } from "react-router-dom"; import { RedirectIfAuthenticated } from "./features/auth/RedirectIfAuthenticated"; import { RequireAuth } from "./features/auth/RequireAuth"; import { AppLayout } from "./layouts/AppLayout"; -import { ImportRecipePage } from "./pages/ImportRecipePage"; -import { LoginPage } from "./pages/LoginPage"; +import { LoginPage } from "./pages/auth/LoginPage"; +import { SignupPage } from "./pages/auth/SignupPage"; import { OnboardingAllergensPage } from "./pages/onboarding/OnboardingAllergensPage"; import { OnboardingDietPage } from "./pages/onboarding/OnboardingDietPage"; import { OnboardingHouseholdPage } from "./pages/onboarding/OnboardingHouseholdPage"; import { OnboardingSourcesPage } from "./pages/onboarding/OnboardingSourcesPage"; -import { PlanningPage } from "./pages/PlanningPage"; -import { RecipeFormPage } from "./pages/RecipeFormPage"; -import { RecipesPage } from "./pages/RecipesPage"; -import { ShoppingListPage } from "./pages/ShoppingListPage"; -import { SignupPage } from "./pages/SignupPage"; +import { PlanningPage } from "./pages/planning/PlanningPage"; +import { ImportRecipePage } from "./pages/recipes/ImportRecipePage"; +import { RecipeFormPage } from "./pages/recipes/RecipeFormPage"; +import { RecipesPage } from "./pages/recipes/RecipesPage"; import { AccountSettingsPage } from "./pages/settings/AccountSettingsPage"; import { CreditsPage } from "./pages/settings/CreditsPage"; import { HouseholdSettingsPage } from "./pages/settings/HouseholdSettingsPage"; import { PreferencesPage } from "./pages/settings/PreferencesPage"; import { UserPreferencesPage } from "./pages/settings/UserPreferencesPage"; +import { ShoppingListPage } from "./pages/shopping-list/ShoppingListPage"; /** * Top-level route table. Every authenticated section is nested under one diff --git a/apps/web/src/pages/ComingSoonPage.scss b/apps/web/src/components/ui/ComingSoonPage.scss similarity index 100% rename from apps/web/src/pages/ComingSoonPage.scss rename to apps/web/src/components/ui/ComingSoonPage.scss diff --git a/apps/web/src/components/ui/ComingSoonPage.tsx b/apps/web/src/components/ui/ComingSoonPage.tsx new file mode 100644 index 0000000..bebebf9 --- /dev/null +++ b/apps/web/src/components/ui/ComingSoonPage.tsx @@ -0,0 +1,26 @@ +import "./ComingSoonPage.scss"; + +interface ComingSoonPageProps { + title: string; + description: string; +} + +/** + * Placeholder rendered by a section that has a route/sidebar entry but no + * real feature behind it yet — today only `pages/shopping-list/ShoppingListPage.tsx` + * (`Recettes`/`Foyer & profil` both grew real backends since this was + * written, see `pages/recipes/`/`pages/settings/`). Kept as a shared, + * reusable component (`components/ui/`, not itself a routed page) rather + * than inlined into that one page, so a future stub section doesn't need to + * hand-roll the same markup — the page that needs it still gets its own + * file (and its own copy, via i18n), just wrapping this instead of + * rewriting it. + */ +export function ComingSoonPage({ title, description }: ComingSoonPageProps) { + return ( +
+

{title}

+

{description}

+
+ ); +} diff --git a/apps/web/src/pages/ComingSoonPage.tsx b/apps/web/src/pages/ComingSoonPage.tsx deleted file mode 100644 index 0d5a5df..0000000 --- a/apps/web/src/pages/ComingSoonPage.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import "./ComingSoonPage.scss"; - -interface ComingSoonPageProps { - title: string; - description: string; -} - -/** - * Placeholder rendered by every section that has a route/sidebar entry but - * no real feature behind it yet (Recettes, Liste de courses, Foyer & - * profil — see `RecipesPage.tsx` etc.). One shared component instead of - * three near-identical markup blocks; each page still gets its own file - * (and its own copy, via i18n) so building out a real feature later means - * rewriting one dedicated file, not splitting a generic route. - */ -export function ComingSoonPage({ title, description }: ComingSoonPageProps) { - return ( -
-

{title}

-

{description}

-
- ); -} diff --git a/apps/web/src/pages/LoginPage.tsx b/apps/web/src/pages/auth/LoginPage.tsx similarity index 92% rename from apps/web/src/pages/LoginPage.tsx rename to apps/web/src/pages/auth/LoginPage.tsx index 8961f6e..aba3a37 100644 --- a/apps/web/src/pages/LoginPage.tsx +++ b/apps/web/src/pages/auth/LoginPage.tsx @@ -2,13 +2,13 @@ import { ErrorCode, loginSchema } from "@batch-cooking/shared"; import { type FormEvent, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; -import { ApiError } from "../api/client"; -import { useAuth } from "../features/auth/AuthContext"; +import { ApiError } from "../../api/client"; +import { useAuth } from "../../features/auth/AuthContext"; // Shared with SignupPage — see the file for why it's colocated in // features/auth/ rather than duplicated per page. -import "../features/auth/auth-form.scss"; -import { fieldErrorsFrom } from "../lib/zod-errors"; -import { errorMessageService } from "../services/error-message.service"; +import "../../features/auth/auth-form.scss"; +import { fieldErrorsFrom } from "../../lib/zod-errors"; +import { errorMessageService } from "../../services/error-message.service"; /** * Login form. Validates client-side first (via the shared `loginSchema`, diff --git a/apps/web/src/pages/SignupPage.tsx b/apps/web/src/pages/auth/SignupPage.tsx similarity index 94% rename from apps/web/src/pages/SignupPage.tsx rename to apps/web/src/pages/auth/SignupPage.tsx index 8c481db..8335230 100644 --- a/apps/web/src/pages/SignupPage.tsx +++ b/apps/web/src/pages/auth/SignupPage.tsx @@ -2,13 +2,13 @@ import { ErrorCode, signupSchema } from "@batch-cooking/shared"; import { type FormEvent, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; -import { ApiError } from "../api/client"; -import { useAuth } from "../features/auth/AuthContext"; +import { ApiError } from "../../api/client"; +import { useAuth } from "../../features/auth/AuthContext"; // Shared with LoginPage — see the file for why it's colocated in // features/auth/ rather than duplicated per page. -import "../features/auth/auth-form.scss"; -import { fieldErrorsFrom } from "../lib/zod-errors"; -import { errorMessageService } from "../services/error-message.service"; +import "../../features/auth/auth-form.scss"; +import { fieldErrorsFrom } from "../../lib/zod-errors"; +import { errorMessageService } from "../../services/error-message.service"; /** * Signup form (profile creation). Validates client-side first (via the diff --git a/apps/web/src/pages/PlanningPage.tsx b/apps/web/src/pages/planning/PlanningPage.tsx similarity index 98% rename from apps/web/src/pages/PlanningPage.tsx rename to apps/web/src/pages/planning/PlanningPage.tsx index e9e082e..7b59c41 100644 --- a/apps/web/src/pages/PlanningPage.tsx +++ b/apps/web/src/pages/planning/PlanningPage.tsx @@ -15,8 +15,8 @@ import { } from "@batch-cooking/shared"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { apiClient } from "../api/client"; -import { type PlanningSlot, RecipePickerDialog } from "../features/planning/RecipePickerDialog"; +import { apiClient } from "../../api/client"; +import { type PlanningSlot, RecipePickerDialog } from "../../features/planning/RecipePickerDialog"; import "./planning-page.scss"; /** Load state for the `GET /planning` call — a discriminated union so a stale/impossible combination (e.g. "loading" with data) can't be represented. */ diff --git a/apps/web/src/pages/planning-page.scss b/apps/web/src/pages/planning/planning-page.scss similarity index 100% rename from apps/web/src/pages/planning-page.scss rename to apps/web/src/pages/planning/planning-page.scss diff --git a/apps/web/src/pages/ImportRecipePage.tsx b/apps/web/src/pages/recipes/ImportRecipePage.tsx similarity index 96% rename from apps/web/src/pages/ImportRecipePage.tsx rename to apps/web/src/pages/recipes/ImportRecipePage.tsx index 6314c9c..51e5689 100644 --- a/apps/web/src/pages/ImportRecipePage.tsx +++ b/apps/web/src/pages/recipes/ImportRecipePage.tsx @@ -1,8 +1,8 @@ import { MEALS, type Meal, WEEK_DAYS, type WeekDay } from "@batch-cooking/shared"; import { useTranslation } from "react-i18next"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; -import { RecipeImportForm } from "../features/recipes/RecipeImportForm"; -import "../features/recipes/recipes.scss"; +import { RecipeImportForm } from "../../features/recipes/RecipeImportForm"; +import "../../features/recipes/recipes.scss"; /** * Reads and validates `?planningDate=&planningWeekDay=&planningMeal=` off diff --git a/apps/web/src/pages/RecipeFormPage.tsx b/apps/web/src/pages/recipes/RecipeFormPage.tsx similarity index 95% rename from apps/web/src/pages/RecipeFormPage.tsx rename to apps/web/src/pages/recipes/RecipeFormPage.tsx index 66de3cd..8a13fb1 100644 --- a/apps/web/src/pages/RecipeFormPage.tsx +++ b/apps/web/src/pages/recipes/RecipeFormPage.tsx @@ -10,14 +10,14 @@ import { import { type FormEvent, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useParams } from "react-router-dom"; -import { ApiError, apiClient } from "../api/client"; -import { DietTagSelect } from "../features/recipes/DietTagSelect"; -import { IngredientPicker } from "../features/recipes/IngredientPicker"; -import { IngredientRow } from "../features/recipes/IngredientRow"; -import { type StepDraft, StepListEditor } from "../features/recipes/StepListEditor"; -import "../features/recipes/recipes.scss"; -import { makeClientKey } from "../lib/client-key"; -import { errorMessageService } from "../services/error-message.service"; +import { ApiError, apiClient } from "../../api/client"; +import { DietTagSelect } from "../../features/recipes/DietTagSelect"; +import { IngredientPicker } from "../../features/recipes/IngredientPicker"; +import { IngredientRow } from "../../features/recipes/IngredientRow"; +import { type StepDraft, StepListEditor } from "../../features/recipes/StepListEditor"; +import "../../features/recipes/recipes.scss"; +import { makeClientKey } from "../../lib/client-key"; +import { errorMessageService } from "../../services/error-message.service"; /** In display order — mirrors `RecipeVisibility` (schema.prisma/shared types). */ const VISIBILITY_OPTIONS: RecipeVisibility[] = ["PERSONAL", "HOUSE", "PUBLIC"]; diff --git a/apps/web/src/pages/RecipesPage.tsx b/apps/web/src/pages/recipes/RecipesPage.tsx similarity index 95% rename from apps/web/src/pages/RecipesPage.tsx rename to apps/web/src/pages/recipes/RecipesPage.tsx index 7d963b2..0a963b9 100644 --- a/apps/web/src/pages/RecipesPage.tsx +++ b/apps/web/src/pages/recipes/RecipesPage.tsx @@ -2,22 +2,25 @@ import { ErrorCode, type RecipeSummaryView } from "@batch-cooking/shared"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom"; -import { ApiError, apiClient } from "../api/client"; -import { RecipeDetailPanel, type RecipeDetailState } from "../features/recipes/RecipeDetailPanel"; +import { ApiError, apiClient } from "../../api/client"; +import { + RecipeDetailPanel, + type RecipeDetailState, +} from "../../features/recipes/RecipeDetailPanel"; import { RecipeSourcesPanel, type SourceItemSelection, -} from "../features/recipes/RecipeSourcesPanel"; -import { RecipeTable } from "../features/recipes/RecipeTable"; +} from "../../features/recipes/RecipeSourcesPanel"; +import { RecipeTable } from "../../features/recipes/RecipeTable"; import { isSourceTab, parseSourceTabValue, type RecipesPageTab, RecipeTabs, sourceTabValue, -} from "../features/recipes/RecipeTabs"; -import { useEnabledSources } from "../features/recipes/useEnabledSources"; -import "../features/recipes/recipes.scss"; +} from "../../features/recipes/RecipeTabs"; +import { useEnabledSources } from "../../features/recipes/useEnabledSources"; +import "../../features/recipes/recipes.scss"; /** Debounce for the search field — avoids firing a request on every keystroke, same idea as the household name's autosave (`HouseholdSettingsPage`). */ const SEARCH_DEBOUNCE_MS = 300; diff --git a/apps/web/src/pages/ShoppingListPage.tsx b/apps/web/src/pages/shopping-list/ShoppingListPage.tsx similarity index 82% rename from apps/web/src/pages/ShoppingListPage.tsx rename to apps/web/src/pages/shopping-list/ShoppingListPage.tsx index 090be39..5d76b17 100644 --- a/apps/web/src/pages/ShoppingListPage.tsx +++ b/apps/web/src/pages/shopping-list/ShoppingListPage.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { ComingSoonPage } from "./ComingSoonPage"; +import { ComingSoonPage } from "../../components/ui/ComingSoonPage"; /** Shopping list section — routed at `/liste-de-courses`. No backend yet, stub for now. */ export function ShoppingListPage() { diff --git a/specs/frontend-architecture.md b/specs/frontend-architecture.md index 1e2020d..560448d 100644 --- a/specs/frontend-architecture.md +++ b/specs/frontend-architecture.md @@ -21,7 +21,8 @@ apps/web/src/ │ └── 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 +│ ├── 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) @@ -49,13 +50,17 @@ apps/web/src/ ├── 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/ -│ ├── LoginPage.tsx / SignupPage.tsx (via auth-form.scss, partagé) -│ ├── PlanningPage.tsx + planning-page.scss # grille de la semaine (routée sur "/"), voir plus bas -│ ├── 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) -│ ├── ComingSoonPage.tsx + .scss / ShoppingListPage.tsx # placeholder, section sans backend +├── 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 @@ -347,7 +352,7 @@ L'ancienne `HouseholdPage` combinée est éclatée en 5 pages dédiées ## Planning (`/`) -`pages/PlanningPage.tsx` (+ `planning-page.scss`) — remplace l'ancienne +`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 @@ -427,7 +432,7 @@ quelqu'un l'ajoute à son planning" : ## Recettes — catalogue, favoris, import depuis une source externe -`pages/RecipesPage.tsx` — routée sur `/recettes`, `/recettes/:id` **et** +`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 @@ -485,7 +490,7 @@ Clic sur une ligne : /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/ImportRecipePage.tsx` (`/recettes/importer/:sourceKey/:externalId`) + — `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 @@ -608,6 +613,9 @@ Clic sur une ligne : `: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. ---