planmymeal/app/components/RecipeDialog.tsx
Cayden Schwechten df5dd3a70e Move UI to BoardUI with slate + salmon theme; add PDF plan export
- 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>
2026-11-06 16:36:33 -06:00

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 &amp; cook mode
<ArrowRight className="size-4" aria-hidden />
</Link>
</div>
</div>
)}
</dialog>
);
}