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>
71 lines
2.6 KiB
TypeScript
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>
|
|
);
|
|
}
|