init
This commit is contained in:
commit
c19b910c36
80 changed files with 16134 additions and 0 deletions
111
app/components/RecipeDialog.tsx
Normal file
111
app/components/RecipeDialog.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
"use client";
|
||||
|
||||
import { AlertTriangle, ArrowRight, ArrowRightLeft, Trash2, X } from "lucide-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";
|
||||
|
||||
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-2xl border border-line bg-surface p-0 text-ink shadow-2xl sm:m-auto sm:max-w-3xl sm:rounded-2xl"
|
||||
>
|
||||
{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
|
||||
type="button"
|
||||
onClick={() => onSwap(slotKey)}
|
||||
className="trim-cap inline-flex min-h-10 items-center gap-2 rounded-lg bg-brand-600 px-4 py-2 text-sm font-semibold text-on-accent transition-colors hover:bg-brand-500"
|
||||
>
|
||||
<ArrowRightLeft className="size-4" aria-hidden />
|
||||
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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue