import { INGREDIENT_CATEGORIES, INGREDIENT_CATEGORY_SUBCATEGORIES, type IngredientCategory, type IngredientSubcategory, type IngredientView, } 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 { ReproducibleBadge } from "./ReproducibleBadge"; import { CategoryIcon, IngredientTypeIcon, SubcategoryIcon } from "./ingredient-icons"; 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; /** * Browsable ingredient picker — a two-level category/subcategory drill-down * plus search plus a card grid, replacing the earlier `IngredientAutocomplete` * (a plain type-to-filter dropdown). With 400+ reference ingredients, search * alone doesn't scale to actually *finding* one, and a single flat list of * 7 aisles wouldn't either (some — "Condiments & épices" — are 100+ items * deep) — so picking a category reveals a second row of subcategory chips * scoped to it (reset whenever the category changes, since a subcategory * from the previous one wouldn't apply). Search still narrows within (or * across) whatever's selected for when the name is already known. * * Receives `ingredients` as a prop rather than fetching them itself — same * rationale as `AllergySelect`/`DietSelect`. `excludeIds` (already-selected * ingredients — a recipe's ingredient list, or a profile's disliked list) * keeps the same one from being added twice. * * The settings menu trailing the search input shows/hides the allergen/diet * badge rows on every card — a display preference local to this picker (not * persisted), for whoever finds two rows of badges per card too noisy while * just browsing/searching by name. A labeled checkbox menu rather than two * bare icon-only toggle buttons — those turned out too ambiguous on their * own (unclear what each icon meant without a label attached). */ export function IngredientPicker({ ingredients, excludeIds, onSelect, }: { ingredients: IngredientView[]; excludeIds: number[]; onSelect: (ingredient: IngredientView) => void; }) { const { t } = useTranslation(); const [category, setCategory] = useState(ALL); const [subcategory, setSubcategory] = useState(ALL); const [query, setQuery] = useState(""); // Purely a display preference for this picker's own card grid — doesn't // touch which ingredients `visible` includes, only whether their // allergen/diet badges render. Defaults to shown (the previous, only // behavior); collapsing them is an opt-in for whoever finds two rows of // badges per card too noisy while just browsing/searching by name. const [showAllergens, setShowAllergens] = useState(true); const [showDiets, setShowDiets] = useState(true); const [showReproducible, setShowReproducible] = useState(true); const [isDisplayMenuOpen, setIsDisplayMenuOpen] = useState(false); function selectCategory(next: IngredientCategory | typeof ALL) { setCategory(next); setSubcategory(ALL); } const normalizedQuery = query.trim().toLowerCase(); const visible = ingredients.filter((ingredient) => { if (excludeIds.includes(ingredient.id)) return false; if (category !== ALL) { if (ingredient.category !== category) return false; if (subcategory !== ALL && ingredient.subcategory !== subcategory) return false; } // Matches against the *displayed* (translated) label, not the raw slug // key — searching "œuf" should find "Œuf" the way it always has, not // require typing its key. if (normalizedQuery.length > 0) { const label = t(`catalog.ingredients.${ingredient.key}`).toLowerCase(); if (!label.includes(normalizedQuery)) return false; } return true; }); function handleSelect(ingredient: IngredientView) { onSelect(ingredient); setQuery(""); } return (
setQuery(e.target.value)} placeholder={t("recipes.form.searchIngredientPlaceholder")} />
{isDisplayMenuOpen && (
{t("recipes.form.showDietsLabel")} {t("recipes.form.showAllergensLabel")} {t("recipes.form.showReproducibleLabel")}
)}
{INGREDIENT_CATEGORIES.map((c) => ( ))}
{category !== ALL && (
{INGREDIENT_CATEGORY_SUBCATEGORIES[category].map((s) => ( ))}
)} {visible.length === 0 ? (

{t("recipes.form.noIngredientFound")}

) : (
{visible.map((ingredient) => ( ))}
)}
); }