batchCooking/apps/web/src/features/recipes/IngredientRow.tsx
Nicolas 978fe71a11 feat(recipes): flag les ingrédients faisables maison + suggestion de recherche
Remplace la FK morte `Ingredient.alternateRecipeId` (jamais branchée nulle
part — confirmé par exploration : zéro usage en dehors de schema.prisma)
par un flag booléen `reproducible`, plus simple : pas de liaison
recette↔ingrédient en base, juste une info "ça vaut le coup d'être fait
maison" plus un raccourci de recherche.

- Migration : drop `alternate_recipe` (colonne + FK), ajoute
  `reproducible BOOLEAN NOT NULL DEFAULT false` sur `ingredients`.
- `reference-seed-data.ts` : `IngredientSeed` gagne `reproducible?`,
  threadé dans le flatten + la réconciliation `seedReferenceData`. Premier
  lot de 27 ingrédients marqués (pains, pâtes à cuire, sauces de base,
  bouillons/fonds) — même logique que la curation Ciqual : un lot solide
  plutôt qu'exhaustif sur les 546 ingrédients.
- `IngredientView` (shared) + les deux endroits qui la construisent
  (`reference.service.ts`, `recipe.service.ts`) gagnent `reproducible`.
- `ReproducibleBadge` (nouveau) : pastille "Faisable maison" — simple
  dans `IngredientPicker` (avec son propre toggle d'affichage), lien
  cliquable dans `IngredientRow` vers `/recettes?search=<nom>` ouvert
  dans un nouvel onglet (pour ne jamais perdre le formulaire de recette
  en cours — pas de persistance de brouillon dans `RecipeFormPage`).
- `RecipesPage` lit `?search=` au montage pour permettre ce deep-link.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 19:09:39 +02:00

66 lines
2.2 KiB
TypeScript

import type { IngredientView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import { AllergenBadges } from "./AllergenBadges";
import { DietBadges } from "./DietBadges";
import { ReproducibleBadge } from "./ReproducibleBadge";
import { IngredientTypeIcon } from "./ingredient-icons";
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/unit are kept as raw strings while editing (not parsed to a number until submit) so an in-progress/invalid value doesn't fight the input. */
export function IngredientRow({
ingredient,
quantity,
unit,
onQuantityChange,
onUnitChange,
onRemove,
}: {
ingredient: IngredientView;
quantity: string;
unit: string;
onQuantityChange: (quantity: string) => void;
onUnitChange: (unit: string) => void;
onRemove: () => void;
}) {
const { t } = useTranslation();
return (
<li className="ingredient-row">
<span className="ingredient-row__icon" aria-hidden="true">
<IngredientTypeIcon icon={ingredient.icon} />
</span>
<span className="ingredient-row__name">{t(`catalog.ingredients.${ingredient.key}`)}</span>
<input
type="number"
min="0"
step="any"
className="ingredient-row__quantity"
value={quantity}
onChange={(e) => onQuantityChange(e.target.value)}
aria-label={t("recipes.form.quantityLabel")}
/>
<input
type="text"
className="ingredient-row__unit"
value={unit}
onChange={(e) => onUnitChange(e.target.value)}
placeholder={t("recipes.form.unitPlaceholder")}
aria-label={t("recipes.form.unitLabel")}
/>
<AllergenBadges allergens={ingredient.allergens} />
<DietBadges diets={ingredient.diets} />
<ReproducibleBadge
reproducible={ingredient.reproducible}
searchLabel={t(`catalog.ingredients.${ingredient.key}`)}
/>
<button
type="button"
className="ingredient-row__remove"
onClick={onRemove}
title={t("recipes.form.removeIngredient")}
>
</button>
</li>
);
}