- Adopt BoardUI (Pro init): theme tokens, Inter, .dark mode, Button, SegmentedControl, Meter, Skeleton and ThemeToggle; remap the app's color aliases onto BoardUI tokens and swap lucide for Remix icons - Retint BoardUI's neutral ramp to cool slate with a salmon accent; drop the dotted background; raise muted text contrast - Simplify the landing page and remove the overlap explorer - Planner: explicit "I have this" toggle on grocery lines, pantry panel above the menu with the staples list spelled out, sticky aisle header fix, and a "Download PDF" plan export (jsPDF, lazy-loaded) - Recipes: replace the family cabbage soup with Cade's Cabbage Soup (adds Bone-In Ham Steak to the catalog); remove Ham, Bean & Cabbage Soup - Remove unused BoardUI components, dependencies and exports Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
4.4 KiB
TypeScript
107 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { RiAlertLine as AlertTriangle, RiArrowRightLine as ArrowRight, RiArrowLeftRightLine as ArrowRightLeft, RiDeleteBinLine as Trash2, RiCloseLine as X } from "@remixicon/react";
|
|
import Link from "next/link";
|
|
import { useEffect, useRef } from "react";
|
|
import { DAY_NAMES, describeBlockers, isCookable, mealLabel, recipeBlockers } from "@/app/lib/plan";
|
|
import { RECIPES_BY_ID } from "@/app/lib/recipes";
|
|
import type { MealPlanState } from "@/app/types";
|
|
import { RecipeDetail, RecipeMeta } from "./RecipeDetail";
|
|
import { Button } from "@/components/base/buttons/button";
|
|
|
|
interface RecipeDialogProps {
|
|
slotKey: string | null;
|
|
state: MealPlanState;
|
|
onClose: () => void;
|
|
onSwap: (slotKey: string) => void;
|
|
onRemove: (slotKey: string, recipeTitle: string) => void;
|
|
}
|
|
|
|
export function RecipeDialog({ slotKey, state, onClose, onSwap, onRemove }: RecipeDialogProps) {
|
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
const recipe = slotKey ? RECIPES_BY_ID.get(state.slots[slotKey]) : undefined;
|
|
const open = Boolean(slotKey && recipe);
|
|
|
|
useEffect(() => {
|
|
const dialog = dialogRef.current;
|
|
if (!dialog) return;
|
|
if (open && !dialog.open) dialog.showModal();
|
|
if (!open && dialog.open) dialog.close();
|
|
return () => {
|
|
if (dialog.open) dialog.close();
|
|
};
|
|
}, [open]);
|
|
|
|
const [dayIndex, mealNumber] = (slotKey ?? "0-1").split("-").map(Number);
|
|
|
|
return (
|
|
<dialog
|
|
ref={dialogRef}
|
|
onClose={onClose}
|
|
onClick={(e) => e.target === dialogRef.current && onClose()}
|
|
aria-labelledby="recipe-dialog-title"
|
|
className="m-0 mt-auto max-h-[92dvh] w-full max-w-none overflow-y-auto rounded-t-3xl border border-line bg-surface p-0 text-ink shadow-2xl sm:m-auto sm:max-w-3xl sm:rounded-3xl"
|
|
>
|
|
{open && recipe && slotKey && (
|
|
<div className="p-5 sm:p-7">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div>
|
|
<p className="font-mono text-xs tracking-wider text-ink-faint uppercase">
|
|
{DAY_NAMES[dayIndex]} · {mealLabel(mealNumber)}
|
|
</p>
|
|
<h2 id="recipe-dialog-title" className="mt-0.5 font-serif text-2xl font-semibold tracking-tight sm:text-3xl">
|
|
{recipe.title}
|
|
</h2>
|
|
<p className="mt-1.5 text-ink-muted">{recipe.description}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
aria-label="Close"
|
|
className="grid size-10 shrink-0 place-items-center rounded-lg text-ink-muted transition-colors hover:bg-raised hover:text-ink"
|
|
>
|
|
<X className="size-5" aria-hidden />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<RecipeMeta recipe={recipe} />
|
|
</div>
|
|
|
|
{!isCookable(recipe, state) && (
|
|
<p className="mt-4 flex items-center gap-2 rounded-lg border border-status-warn-line bg-status-warn-bg px-3.5 py-2.5 text-sm text-status-warn">
|
|
<AlertTriangle className="size-4 shrink-0" aria-hidden />
|
|
<span>{describeBlockers(recipeBlockers(recipe, state))}. Consider swapping this meal.</span>
|
|
</p>
|
|
)}
|
|
|
|
<div className="mt-6">
|
|
<RecipeDetail recipe={recipe} servings={state.householdSize} showMeta={false} interactive />
|
|
</div>
|
|
|
|
<div className="mt-8 flex flex-wrap items-center gap-2.5 border-t border-line pt-4">
|
|
<Button leadingIcon={ArrowRightLeft} onClick={() => onSwap(slotKey)}>
|
|
Replace dish
|
|
</Button>
|
|
<button
|
|
type="button"
|
|
onClick={() => onRemove(slotKey, recipe.title)}
|
|
className="trim-cap inline-flex min-h-10 items-center gap-1.5 rounded-lg px-3.5 py-2 text-sm font-medium text-ink-muted transition-colors hover:bg-status-danger-bg hover:text-status-danger"
|
|
>
|
|
<Trash2 className="size-4" aria-hidden />
|
|
Remove from plan
|
|
</button>
|
|
<Link
|
|
href={`/recipes/${recipe.id}`}
|
|
onClick={onClose}
|
|
className="ml-auto inline-flex min-h-10 items-center gap-1.5 text-sm font-medium text-brand-300 hover:underline"
|
|
>
|
|
Full recipe & cook mode
|
|
<ArrowRight className="size-4" aria-hidden />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</dialog>
|
|
);
|
|
}
|