"use client"; import { RiArrowDownSLine as ChevronDown, RiArrowUpSLine as ChevronUp, RiRepeat2Line as Repeat2, RiSearchLine as Search, RiEqualizerLine as SlidersHorizontal, RiCloseLine as X } from "@remixicon/react"; import { useEffect, useMemo, useRef, useState } from "react"; import { Cart } from "@/app/lib/grocery"; import { CUISINES, DAY_NAMES, STYLES, activeRecipeIds, describeBlockers, mealKindFor, mealLabel, recipeBlockers, toggle } from "@/app/lib/plan"; import { RECIPES, RECIPES_BY_ID } from "@/app/lib/recipes"; import { formatUsd } from "@/app/lib/units"; import type { CookingStyle, Cuisine, MealPlanState, Recipe } from "@/app/types"; import { Chip } from "./Chip"; import { StyleIcon } from "./Icons"; import { RecipeDetail } from "./RecipeDetail"; import { Button } from "@/components/base/buttons/button"; interface MealSelectorModalProps { slotKey: string | null; state: MealPlanState; onPick: (recipeId: string) => void; onClose: () => void; } export function MealSelectorModal({ slotKey, state, onPick, onClose }: MealSelectorModalProps) { const dialogRef = useRef(null); useEffect(() => { const dialog = dialogRef.current; if (!dialog) return; if (slotKey && !dialog.open) dialog.showModal(); if (!slotKey && dialog.open) dialog.close(); return () => { if (dialog.open) dialog.close(); }; }, [slotKey]); return ( e.target === dialogRef.current && onClose()} aria-labelledby="selector-title" className="m-0 mt-auto max-h-[90dvh] w-full max-w-none overflow-hidden rounded-t-3xl border border-line bg-surface p-0 shadow-2xl sm:m-auto sm:max-w-2xl sm:rounded-3xl" > {slotKey && } ); } interface Candidate { recipe: Recipe; addedCost: number; reusedCount: number; blockedReason: string; } function SelectorContent({ slotKey, state, onPick, onClose, }: { slotKey: string; state: MealPlanState; onPick: (id: string) => void; onClose: () => void; }) { const [dayIndex, mealNumber] = slotKey.split("-").map(Number); const slotKind = mealKindFor(mealNumber); const currentId = state.slots[slotKey]; const [query, setQuery] = useState(""); const [filtersOpen, setFiltersOpen] = useState(false); const [cuisines, setCuisines] = useState(state.preferredCuisines); const [styles, setStyles] = useState(state.preferredStyles); const [showAllKinds, setShowAllKinds] = useState(false); const [showIncompatible, setShowIncompatible] = useState(false); const [expandedId, setExpandedId] = useState(null); const candidates = useMemo(() => { const others = activeRecipeIds(state); if (currentId) others.splice(others.indexOf(currentId), 1); const cart = new Cart({ householdSize: state.householdSize, onHandItems: state.onHandItems }, others); const itemsInCart = new Set(others.flatMap((id) => RECIPES_BY_ID.get(id)?.ingredients.map((i) => i.itemId) ?? [])); return RECIPES.map((recipe) => ({ recipe, addedCost: cart.marginal(recipe.id).checkout, reusedCount: recipe.ingredients.filter((i) => itemsInCart.has(i.itemId)).length, blockedReason: describeBlockers(recipeBlockers(recipe, state)), })).sort((a, b) => a.addedCost - b.addedCost); }, [state, currentId]); const needle = query.trim().toLowerCase(); const visible = candidates.filter( ({ recipe, blockedReason }) => (showAllKinds || recipe.kind === slotKind) && (showIncompatible || blockedReason === "") && (cuisines.length === 0 || cuisines.includes(recipe.cuisine)) && (styles.length === 0 || recipe.styles.some((s) => styles.includes(s))) && (needle === "" || recipe.title.toLowerCase().includes(needle)), ); const activeFilterCount = cuisines.length + styles.length + (showAllKinds ? 1 : 0) + (showIncompatible ? 1 : 0); return (
{/* Compact sticky header so 80%+ of mobile viewport shows recipes */}

{DAY_NAMES[dayIndex]} · {mealLabel(mealNumber)}

Choose a dish

Sorted by marginal cost added to your current grocery cart

setQuery(e.target.value)} className="min-h-10 w-full rounded-lg border border-line-strong py-2 pr-3 pl-9 text-sm" />
{filtersOpen && (
{CUISINES.map((c) => ( setCuisines((list) => toggle(list, c.id))}> {c.label} ))}
{STYLES.map((st) => ( setStyles((list) => toggle(list, st.id))}> {st.label} ))}
)}
{/* Results */}
    {visible.length === 0 && (
  • No recipes match these filters.
  • )} {visible.map(({ recipe, addedCost, reusedCount, blockedReason }) => { const disabled = blockedReason !== ""; const expanded = expandedId === recipe.id; return (
  • +{formatUsd(Math.max(0, addedCost))}

    {addedCost < 0.01 ? "in cart" : "to cart"}

    {expanded && (
    {!disabled && ( )}
    )}
  • ); })}
); }