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>
This commit is contained in:
Nicolas 2026-08-21 10:53:17 +02:00
parent eab28aa017
commit 23d4fd4966
13 changed files with 85 additions and 71 deletions

View file

@ -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

View file

@ -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 (
<div className="coming-soon-page">
<h1>{title}</h1>
<p>{description}</p>
</div>
);
}

View file

@ -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 (
<div className="coming-soon-page">
<h1>{title}</h1>
<p>{description}</p>
</div>
);
}

View file

@ -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`,

View file

@ -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

View file

@ -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. */

View file

@ -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

View file

@ -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"];

View file

@ -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;

View file

@ -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() {

View file

@ -21,7 +21,8 @@ apps/web/src/
│ └── 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
│ ├── 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.
---