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>
66 lines
2.2 KiB
TypeScript
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>
|
|
);
|
|
}
|