"use client"; import { ChevronDown, Search, X } from "lucide-react"; import { useMemo, useState, type Dispatch } from "react"; import { CATALOG, CATALOG_BY_ID, PANTRY_STAPLE_IDS } from "@/app/lib/catalog"; import { RECIPES } from "@/app/lib/recipes"; import type { PlanAction } from "@/app/lib/usePlanState"; import type { Aisle, ItemId, ItemPreference, MealPlanState, StoreItem } from "@/app/types"; import { RadioGroup, type RadioOption } from "./RadioGroup"; interface IngredientPanelProps { state: MealPlanState; dispatch: Dispatch; } type Filter = "all" | "avoid" | "have"; const RECIPE_COUNT: ReadonlyMap = (() => { const counts = new Map(); for (const recipe of RECIPES) { for (const id of new Set(recipe.ingredients.map((i) => i.itemId))) counts.set(id, (counts.get(id) ?? 0) + 1); } return counts; })(); const FILTER_OPTIONS: readonly RadioOption[] = [ { value: "all", label: "All" }, { value: "avoid", label: "Avoided" }, { value: "have", label: "On hand" }, ]; const PREF_OPTIONS: readonly RadioOption[] = [ { value: "avoid", label: "Avoid", activeClass: "border border-status-danger-line bg-status-danger-bg text-status-danger font-semibold" }, { value: "use", label: "Use", activeClass: "border border-selected-line/60 bg-surface text-ink font-semibold shadow-2xs" }, { value: "have", label: "Have it", activeClass: "border border-status-ok-line bg-status-ok-bg text-status-ok font-semibold" }, ]; export function IngredientPanel({ state, dispatch }: IngredientPanelProps) { const [query, setQuery] = useState(""); const [filter, setFilter] = useState("all"); const preferenceOf = (id: ItemId): ItemPreference => state.avoidedItems.includes(id) ? "avoid" : state.onHandItems.includes(id) ? "have" : "use"; const setPreference = (itemIds: ItemId[], preference: ItemPreference) => dispatch({ type: "setItemPreference", itemIds, preference }); const needle = query.trim().toLowerCase(); const groups = useMemo(() => { const visible = CATALOG.filter((item) => { if (needle && !item.name.toLowerCase().includes(needle)) return false; if (filter === "avoid") return state.avoidedItems.includes(item.id); if (filter === "have") return state.onHandItems.includes(item.id); return true; }); const byAisle = new Map(); for (const item of [...visible].sort((a, b) => a.name.localeCompare(b.name))) { byAisle.set(item.aisle, [...(byAisle.get(item.aisle) ?? []), item]); } return [...byAisle].sort(([a], [b]) => a.localeCompare(b)); }, [needle, filter, state.avoidedItems, state.onHandItems]); const staplesAllOnHand = PANTRY_STAPLE_IDS.every((id) => state.onHandItems.includes(id)); return (

Pantry & ingredient exclusions

{state.onHandItems.length} on hand ($0) ·{" "} {state.avoidedItems.length} avoided

{state.avoidedItems.length > 0 && (
{state.avoidedItems.map((id) => ( ))}
)} {/* Search + filter */}
setQuery(e.target.value)} className="min-h-10 w-full rounded-lg border border-line-strong py-1.5 pr-3 pl-9 text-sm" aria-label="Search store items" />
{(state.avoidedItems.length > 0 || state.onHandItems.length > 0) && ( )}
{groups.length === 0 &&

No store items match your search.

} {groups.map(([aisle, items]) => (

{aisle}

i.id)} avoidedItems={state.avoidedItems} onSet={setPreference} />
    {items.map((item) => { const current = preferenceOf(item.id); const count = RECIPE_COUNT.get(item.id) ?? 0; return (
  • {item.name}

    Used in {count} recipe{count === 1 ? "" : "s"}

    setPreference([item.id], val)} size="xs" />
  • ); })}
))}
); } function AisleBulkToggle({ itemIds, avoidedItems, onSet, }: { itemIds: ItemId[]; avoidedItems: ItemId[]; onSet: (itemIds: ItemId[], preference: ItemPreference) => void; }) { const allAvoided = itemIds.every((id) => avoidedItems.includes(id)); return ( ); }