batchCooking/apps/web/src/features/recipes/RecipeTabs.tsx
Nicolas b5a12cf489 feat(recipes): onglet Sources — parcourir les recettes externes (étape 2/4)
Deuxième étape du chantier "onglet Sources" : l'UI de parcours, construite
contre les endpoints backend de l'étape 1 (#45). L'onglet désactivé
placeholder de RecipeTabs devient un vrai onglet fonctionnel.

- RecipeTabs.tsx : nouveau type RecipesPageTab (RecipeTab | "sources") —
  gardé hors du type partagé RecipeTab puisque l'API n'a pas de
  tab=sources à valider. Un prop `tabs` optionnel restreint quels onglets
  s'affichent — RecipePickerDialog (choix d'une recette pour un planning)
  s'y restreint aux 4 onglets réels, parcourir des sources externes en
  plein milieu de ce dialogue n'a pas de sens sans le flux de revue/import.
- Nouveau RecipeSourcesPanel.tsx : contenu de l'onglet "Sources" —
  autonome (son propre master-detail), ne partage pas le fetching
  RecipeTab de RecipesPage puisqu'il parcourt le catalogue *live* d'une
  source (GET /sources/:key/browse), pas la table Recipe sauvegardée.
  Sélecteur de source si le foyer en a activé plusieurs ; sélectionner un
  item déjà importé navigue directement vers la vraie recette
  (SourceItemTable + navigate), un item pas encore importé affiche un
  aperçu en lecture seule (SourceItemPreviewPanel, réutilise
  StepDescription — les tech steps sont donc déjà surlignés dans
  l'aperçu).
- Bug trouvé et corrigé en écrivant le scénario Cypress : cliquer un item
  déjà importé changeait l'URL mais restait affiché sur l'onglet Sources
  (RecipesPage ne rend RecipeDetailPanel/RecipeTable qu'en dehors de
  l'onglet "sources"). RecipeSourcesPanel prend maintenant un callback
  `onViewImportedRecipe` pour repasser sur un onglet réel avant de
  naviguer.

Tests : nouveau recipe-sources.feature (parcours utilisateur complet —
onglet vide, parcours avec items importés/non importés, aperçu avec
surlignage de technique) ; recipes.cy.ts corrigé (assertion obsolète sur
l'ancien placeholder désactivé). Étape suivante (3/4) : écran de revue
(corriger les ingrédients non résolus) + finalisation de l'import.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 16:14:20 +02:00

71 lines
2.6 KiB
TypeScript

import type { RecipeTab } from "@batch-cooking/shared";
import type { LucideIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import {
AccountIcon,
FavoriteIcon,
HouseholdIcon,
PublicIcon,
SourcesIcon,
} from "../../layouts/nav-icons";
import "./recipes.scss";
/**
* A tab of the recipe catalog — either a real {@link RecipeTab} (`GET
* /recipes?tab=`, `recipe.service.ts`'s `listRecipes`) or `"sources"`, a
* web-only mode that doesn't query the recipe table at all: it browses a
* household-enabled external source's own catalog live
* (`GET /sources/:sourceKey/browse`, `RecipeSourcesPanel`) instead of
* listing saved `Recipe` rows. Kept out of the shared `RecipeTab` type on
* purpose — the API has no `tab=sources` to validate.
*/
export type RecipesPageTab = RecipeTab | "sources";
/** Every possible tab, in display order, with its icon — reuses `AccountIcon`/`HouseholdIcon` from the sidebar's own icon set (see nav-icons.tsx) rather than a second "person"/"house" glyph. */
const ALL_TABS: Array<{ value: RecipesPageTab; Icon: LucideIcon }> = [
{ value: "favoris", Icon: FavoriteIcon },
{ value: "perso", Icon: AccountIcon },
{ value: "foyer", Icon: HouseholdIcon },
{ value: "publique", Icon: PublicIcon },
{ value: "sources", Icon: SourcesIcon },
];
/**
* Catalog tab bar — Favoris / Perso / Foyer / Publique / Sources by
* default (`/recettes`, `RecipesPage`). `tabs` narrows which of those
* show — `RecipePickerDialog` (picking a recipe for a planning slot)
* passes just the four real ones: browsing external sources mid-dialog,
* without the review/import flow, doesn't make sense there yet (its
* `onChange` narrows the result back to `RecipeTab` itself, safe exactly
* because `tabs` guarantees `"sources"` is never clickable there). No
* "toutes" tab among the real ones: every recipe a viewer can see falls
* under exactly one of perso/foyer/publique (its own visibility) — see
* `recipe.service.ts`'s `listRecipes`.
*/
export function RecipeTabs({
active,
onChange,
tabs = ALL_TABS.map((tab) => tab.value),
}: {
active: RecipesPageTab;
onChange: (tab: RecipesPageTab) => void;
tabs?: readonly RecipesPageTab[];
}) {
const { t } = useTranslation();
return (
<div className="recipe-tabs">
{ALL_TABS.filter(({ value }) => tabs.includes(value)).map(({ value, Icon }) => (
<button
key={value}
type="button"
className={`recipe-tabs__tab${value === active ? " active" : ""}`}
onClick={() => onChange(value)}
>
<Icon aria-hidden="true" />
{t(`recipes.tabs.${value}`)}
</button>
))}
</div>
);
}