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>
This commit is contained in:
Nicolas 2026-08-21 11:15:17 +02:00
parent 23d4fd4966
commit ae80537b7c
26 changed files with 73 additions and 67 deletions

View file

@ -15,11 +15,9 @@ import { ApiError, apiClient } from "../../api/client";
import { CheckboxOption } from "../../components/ui/Checkbox";
import { Dialog } from "../../components/ui/Dialog";
import { errorMessageService } from "../../services/error-message.service";
import { DietTagSelect } from "../recipes/DietTagSelect";
import { IngredientPicker } from "../recipes/IngredientPicker";
import { DietTagSelect } from "../recipes/badges/DietTagSelect";
import { IngredientPicker } from "../recipes/ingredients/IngredientPicker";
import { RecipeDetailPanel, type RecipeDetailState } from "../recipes/RecipeDetailPanel";
import { RecipeImportForm } from "../recipes/RecipeImportForm";
import { RecipeSourcesPanel, type SourceItemSelection } from "../recipes/RecipeSourcesPanel";
import { RecipeTable } from "../recipes/RecipeTable";
import {
isSourceTab,
@ -27,8 +25,13 @@ import {
type RecipesPageTab,
RecipeTabs,
} from "../recipes/RecipeTabs";
import { tryBuildCompleteImport } from "../recipes/recipe-import-draft";
import { useEnabledSources } from "../recipes/useEnabledSources";
import { RecipeImportForm } from "../recipes/sources/RecipeImportForm";
import {
RecipeSourcesPanel,
type SourceItemSelection,
} from "../recipes/sources/RecipeSourcesPanel";
import { tryBuildCompleteImport } from "../recipes/sources/recipe-import-draft";
import { useEnabledSources } from "../recipes/sources/useEnabledSources";
import "./recipe-picker-dialog.scss";
/** Debounce for the search field — same value as `RecipesPage`'s. */

View file

@ -1,6 +1,6 @@
import type { IngredientView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import { IngredientPicker } from "../recipes/IngredientPicker";
import { IngredientPicker } from "../recipes/ingredients/IngredientPicker";
// Reuses `IngredientPicker` verbatim (built for the recipe form's
// ingredient picker, features/recipes/) rather than a second search+browse
// field — same reference ingredient list, same category/search browsing,
@ -10,7 +10,7 @@ import { IngredientPicker } from "../recipes/IngredientPicker";
// recipes.scss — colocated there since it's the same "reference ingredient
// picker" visual family, even though this field lives in the profile
// feature).
import { IngredientTypeIcon } from "../recipes/ingredient-icons";
import { IngredientTypeIcon } from "../recipes/ingredients/ingredient-icons";
import "../recipes/recipes.scss";
import "./profile-forms.scss";

View file

@ -5,9 +5,9 @@ import { Link } from "react-router-dom";
import { ApiError, apiClient } from "../../api/client";
import { SourceLinkIcon } from "../../layouts/nav-icons";
import { errorMessageService } from "../../services/error-message.service";
import { AllergenBadges } from "./AllergenBadges";
import { FavoriteStarButton } from "./FavoriteStarButton";
import { StepDescription } from "./StepDescription";
import { AllergenBadges } from "./badges/AllergenBadges";
import { FavoriteStarButton } from "./badges/FavoriteStarButton";
import { StepDescription } from "./steps/StepDescription";
import "./recipes.scss";
/**

View file

@ -1,7 +1,7 @@
import type { RecipeSummaryView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import { AllergenBadges } from "./AllergenBadges";
import { DietBadges } from "./DietBadges";
import { AllergenBadges } from "./badges/AllergenBadges";
import { DietBadges } from "./badges/DietBadges";
import "./recipes.scss";
/**

View file

@ -1,6 +1,6 @@
import type { AllergyView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import "./recipes.scss";
import "../recipes.scss";
/**
* A row of allergen pills used both on {@link RecipeCard} (catalog list)

View file

@ -1,6 +1,6 @@
import type { DietView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import "./recipes.scss";
import "../recipes.scss";
/**
* A row of diet-regime pills the "associated regime" reminder tagged on a

View file

@ -1,7 +1,7 @@
import type { DietView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import { CheckboxOption } from "../../components/ui/Checkbox";
import "./recipes.scss";
import { CheckboxOption } from "../../../components/ui/Checkbox";
import "../recipes.scss";
/**
* Multi-select (checkbox grid, same pattern as `AllergySelect`) for the

View file

@ -1,8 +1,8 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { apiClient } from "../../api/client";
import { FavoriteIcon } from "../../layouts/nav-icons";
import "./recipes.scss";
import { apiClient } from "../../../api/client";
import { FavoriteIcon } from "../../../layouts/nav-icons";
import "../recipes.scss";
/**
* Star toggle for a recipe's favorite status the overlay button on

View file

@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next";
import "./recipes.scss";
import "../recipes.scss";
/**
* Single "faisable maison" pill for an ingredient flagged

View file

@ -7,13 +7,13 @@ import {
} from "@batch-cooking/shared";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { CheckboxOption } from "../../components/ui/Checkbox";
import { SettingsIcon } from "../../layouts/nav-icons";
import { AllergenBadges } from "./AllergenBadges";
import { DietBadges } from "./DietBadges";
import { CheckboxOption } from "../../../components/ui/Checkbox";
import { SettingsIcon } from "../../../layouts/nav-icons";
import { AllergenBadges } from "../badges/AllergenBadges";
import { DietBadges } from "../badges/DietBadges";
import { ReproducibleBadge } from "../badges/ReproducibleBadge";
import { CategoryIcon, IngredientTypeIcon, SubcategoryIcon } from "./ingredient-icons";
import { ReproducibleBadge } from "./ReproducibleBadge";
import "./recipes.scss";
import "../recipes.scss";
/** "No filter at this level" — a UI-only pseudo-value, never sent to/received from the API (see {@link INGREDIENT_CATEGORIES}/{@link INGREDIENT_CATEGORY_SUBCATEGORIES} for the real, closed sets). */
const ALL = "ALL" as const;

View file

@ -1,10 +1,10 @@
import type { IngredientView, UnitView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import { AllergenBadges } from "./AllergenBadges";
import { DietBadges } from "./DietBadges";
import { AllergenBadges } from "../badges/AllergenBadges";
import { DietBadges } from "../badges/DietBadges";
import { ReproducibleBadge } from "../badges/ReproducibleBadge";
import { IngredientTypeIcon } from "./ingredient-icons";
import { ReproducibleBadge } from "./ReproducibleBadge";
import "./recipes.scss";
import "../recipes.scss";
/** One selected ingredient line in the recipe form — the ingredient itself (picked via `IngredientPicker`) plus its quantity/unit for this recipe. Quantity is kept as a raw string while editing (not parsed to a number until submit) so an in-progress/invalid value doesn't fight the input; `unit` picks from `unitsCatalog` (a closed reference list, see `Unit` in schema.prisma) rather than free text. */
export function IngredientRow({

View file

@ -13,14 +13,14 @@ import {
} from "@batch-cooking/shared";
import { type FormEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { ApiError, apiClient } from "../../api/client";
import { makeClientKey } from "../../lib/client-key";
import { errorMessageService } from "../../services/error-message.service";
import { DietTagSelect } from "./DietTagSelect";
import { IngredientPicker } from "./IngredientPicker";
import { IngredientRow } from "./IngredientRow";
import { type StepDraft, StepListEditor } from "./StepListEditor";
import "./recipes.scss";
import { ApiError, apiClient } from "../../../api/client";
import { makeClientKey } from "../../../lib/client-key";
import { errorMessageService } from "../../../services/error-message.service";
import { DietTagSelect } from "../badges/DietTagSelect";
import { IngredientPicker } from "../ingredients/IngredientPicker";
import { IngredientRow } from "../ingredients/IngredientRow";
import { type StepDraft, StepListEditor } from "../steps/StepListEditor";
import "../recipes.scss";
/** In display order — mirrors `RecipeVisibility` (schema.prisma/shared types). Same list as `RecipeFormPage`. */
const VISIBILITY_OPTIONS: RecipeVisibility[] = ["PERSONAL", "HOUSE", "PUBLIC"];

View file

@ -1,10 +1,10 @@
import type { BrowsableSourceItemView, RecipeView } from "@batch-cooking/shared";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { apiClient } from "../../api/client";
import { RecipeDetailPanel, type RecipeDetailState } from "./RecipeDetailPanel";
import { apiClient } from "../../../api/client";
import { RecipeDetailPanel, type RecipeDetailState } from "../RecipeDetailPanel";
import { SourceItemTable } from "./SourceItemTable";
import "./recipes.scss";
import "../recipes.scss";
/** Debounce for the search field — same idea/value as `RecipesPage`'s own search. */
const SEARCH_DEBOUNCE_MS = 300;

View file

@ -1,6 +1,6 @@
import type { BrowsableSourceItemView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import "./recipes.scss";
import "../recipes.scss";
/**
* List of one source's browsable items (`RecipeSourcesPanel`) same

View file

@ -1,6 +1,6 @@
import type { SourceView } from "@batch-cooking/shared";
import { useEffect, useState } from "react";
import { apiClient } from "../../api/client";
import { apiClient } from "../../../api/client";
export type EnabledSourcesState =
| { status: "loading" }

View file

@ -1,7 +1,7 @@
import type { StepTechStepView } from "@batch-cooking/shared";
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { Tooltip } from "../../components/ui/Tooltip";
import { Tooltip } from "../../../components/ui/Tooltip";
import { splitDescriptionByTechSteps } from "./highlight-tech-steps";
/**

View file

@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import { makeClientKey } from "../../lib/client-key";
import "./recipes.scss";
import { makeClientKey } from "../../../lib/client-key";
import "../recipes.scss";
/** One in-progress preparation step in the recipe form. `key` is a client-only stable identity for React/reordering — the server derives the real `order`/`id` from array position on save (see `schemas/recipe.ts`), never from this. */
export interface StepDraft {

View file

@ -1,7 +1,7 @@
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 { RecipeImportForm } from "../../features/recipes/sources/RecipeImportForm";
import "../../features/recipes/recipes.scss";
/**

View file

@ -11,10 +11,10 @@ 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 { DietTagSelect } from "../../features/recipes/badges/DietTagSelect";
import { IngredientPicker } from "../../features/recipes/ingredients/IngredientPicker";
import { IngredientRow } from "../../features/recipes/ingredients/IngredientRow";
import { type StepDraft, StepListEditor } from "../../features/recipes/steps/StepListEditor";
import "../../features/recipes/recipes.scss";
import { makeClientKey } from "../../lib/client-key";
import { errorMessageService } from "../../services/error-message.service";

View file

@ -7,10 +7,6 @@ import {
RecipeDetailPanel,
type RecipeDetailState,
} from "../../features/recipes/RecipeDetailPanel";
import {
RecipeSourcesPanel,
type SourceItemSelection,
} from "../../features/recipes/RecipeSourcesPanel";
import { RecipeTable } from "../../features/recipes/RecipeTable";
import {
isSourceTab,
@ -19,7 +15,11 @@ import {
RecipeTabs,
sourceTabValue,
} from "../../features/recipes/RecipeTabs";
import { useEnabledSources } from "../../features/recipes/useEnabledSources";
import {
RecipeSourcesPanel,
type SourceItemSelection,
} from "../../features/recipes/sources/RecipeSourcesPanel";
import { useEnabledSources } from "../../features/recipes/sources/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`). */

View file

@ -4,7 +4,7 @@ import "./settings-pages.scss";
/**
* Third-party attribution routed at `/parametres/credits`. Exists solely
* to satisfy the CC BY 4.0 requirement on the ingredient icon set (see
* `features/recipes/ingredient-icons.tsx`'s `FilledIcon`-based components):
* `features/recipes/ingredients/ingredient-icons.tsx`'s `FilledIcon`-based components):
* most of it comes from foodiconpack.com's free "Common ingredient icons"
* and "Common Utensils" collections, which require crediting the source,
* linking the license, and noting that the icons were adapted (recolored

View file

@ -316,7 +316,7 @@ Table de référence (seedée, jamais créée/éditée/supprimée via l'API), co
|---|---|
| `id` | Identifiant |
| `key` | Slug unique — libellé dans `catalog.ingredients.<key>` (locale) |
| `icon` | `IngredientIcon` — ~20 pictogrammes génériques par *type* de chose (légume, bouteille d'huile, fromage…), pas un emoji par ingrédient (437 rejetés comme peu pro) — voir `apps/web/src/features/recipes/ingredient-icons.tsx` |
| `icon` | `IngredientIcon` — ~20 pictogrammes génériques par *type* de chose (légume, bouteille d'huile, fromage…), pas un emoji par ingrédient (437 rejetés comme peu pro) — voir `apps/web/src/features/recipes/ingredients/ingredient-icons.tsx` |
| `category` / `subcategory` | `IngredientCategory` (7 rayons) / `IngredientSubcategory` (racks plus fins) — organisation "rayon de supermarché français" pour permettre le parcours par catégorie dans le picker (400+ ingrédients, la recherche seule ne suffit pas) |
| `reproducible` | Vrai si raisonnablement faisable maison (un pain burger, une béchamel) plutôt qu'un achat systématique — juste un flag, pas un lien vers une recette précise (un ancien `alternateRecipeId` jamais câblé a été retiré) |

View file

@ -39,14 +39,17 @@ apps/web/src/
│ ├── 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
│ ├── RecipeTable.tsx / RecipeTabs.tsx / RecipeDetailPanel.tsx
│ ├── RecipeSourcesPanel.tsx / SourceItemTable.tsx / useEnabledSources.ts
│ ├── RecipeImportForm.tsx / recipe-import-draft.ts
│ ├── IngredientPicker.tsx / IngredientRow.tsx / StepListEditor.tsx / StepDescription.tsx
│ ├── highlight-tech-steps.ts / ingredient-icons.tsx
│ ├── DietTagSelect.tsx / DietBadges.tsx / AllergenBadges.tsx / ReproducibleBadge.tsx / FavoriteStarButton.tsx
│ └── recipes.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