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>
This commit is contained in:
parent
b02c7a024b
commit
df5dd3a70e
56 changed files with 4749 additions and 788 deletions
292
app/page.tsx
292
app/page.tsx
|
|
@ -1,53 +1,17 @@
|
|||
import { ArrowRight, BookHeart, Flame, ReceiptText, SlidersHorizontal } from "lucide-react";
|
||||
import { RiArrowRightLine, RiHeart3Line, RiRestaurantLine, RiShoppingBasket2Line, RiWallet3Line } from "@remixicon/react";
|
||||
import Link from "next/link";
|
||||
import { OverlapExplorer, type OverlapComboSnapshot, type OverlapDish } from "./components/OverlapExplorer";
|
||||
import { RotatingPreview, type PreviewSet } from "./components/RotatingPreview";
|
||||
import { Logo, SiteHeader } from "./components/SiteHeader";
|
||||
import { CATALOG, PANTRY_STAPLE_IDS } from "./lib/catalog";
|
||||
import { PANTRY_STAPLE_IDS } from "./lib/catalog";
|
||||
import { buildGroceryList } from "./lib/grocery";
|
||||
import { APPLIANCES, CUISINES } from "./lib/plan";
|
||||
import { CUISINES } from "./lib/plan";
|
||||
import { RECIPES } from "./lib/recipes";
|
||||
import { formatUsd } from "./lib/units";
|
||||
import { buttonStyles } from "@/components/base/buttons/button";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Landing page (server component, fully static).
|
||||
//
|
||||
// Every statistic, menu preview, and interactive overlap combination is
|
||||
// derived from the real recipe & store catalog at build time so zero catalog
|
||||
// payload needs to ship to the landing-page client bundle.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DEMO_RECIPE_IDS = ["chicken-curry", "red-lentil-dal", "chana-masala", "garlic-butter-shrimp-and-rice"] as const;
|
||||
|
||||
const OVERLAP_DATA = (() => {
|
||||
const options = { householdSize: 2, onHandItems: PANTRY_STAPLE_IDS };
|
||||
const dishes: OverlapDish[] = DEMO_RECIPE_IDS.map((id) => {
|
||||
const r = RECIPES.find((x) => x.id === id)!;
|
||||
const cuisineLabel = CUISINES.find((c) => c.id === r.cuisine)?.label ?? r.cuisine;
|
||||
return {
|
||||
id,
|
||||
title: r.title,
|
||||
cuisine: `${cuisineLabel} · ${r.prepTime} min`,
|
||||
standaloneCheckout: buildGroceryList([id], options).checkoutTotal,
|
||||
};
|
||||
});
|
||||
|
||||
const combos: Record<string, OverlapComboSnapshot> = {};
|
||||
const totalMasks = 1 << DEMO_RECIPE_IDS.length;
|
||||
for (let mask = 1; mask < totalMasks; mask++) {
|
||||
const picked = DEMO_RECIPE_IDS.filter((_, idx) => (mask & (1 << idx)) !== 0);
|
||||
const key = [...picked].sort().join(",");
|
||||
const summary = buildGroceryList(picked, options);
|
||||
combos[key] = {
|
||||
checkoutTotal: summary.checkoutTotal,
|
||||
usedTotal: summary.usedTotal,
|
||||
carryoverValue: summary.carryoverValue,
|
||||
sharedNames: summary.lines.filter((l) => l.usedBy.length > 1 && !l.onHand).map((l) => l.item.name),
|
||||
packageCount: summary.lines.length,
|
||||
};
|
||||
}
|
||||
return { dishes, combos };
|
||||
})();
|
||||
// Landing page (server component, fully static). Preview menus are priced from the
|
||||
// real recipe & store catalog at build time, so no catalog ships to the client.
|
||||
|
||||
const PREVIEW_THEMES: { theme: string; blurb: string; meals: [slot: string, recipeId: string][] }[] = [
|
||||
{ theme: "Comfort classics", blurb: "Cozy & thrifty", meals: [["Mon · Dinner", "chicken-pot-pie"], ["Tue · Lunch", "chicken-noodle-soup"], ["Tue · Dinner", "beef-stroganoff"]] },
|
||||
|
|
@ -79,208 +43,110 @@ const PREVIEW_SETS: PreviewSet[] = PREVIEW_THEMES.map(({ theme, blurb, meals })
|
|||
|
||||
const STEPS = [
|
||||
{
|
||||
num: "01",
|
||||
title: "Set your register target",
|
||||
body: "Choose a weekly grocery ceiling, 1–7 days, meals per day, and household size. Ingredient quantities scale automatically.",
|
||||
icon: RiWallet3Line,
|
||||
title: "Tell us your budget",
|
||||
body: "How much you want to spend, how many people you're feeding, and which days you're cooking. We'll do the crunching for you.",
|
||||
},
|
||||
{
|
||||
num: "02",
|
||||
title: "Match your real kitchen",
|
||||
body: "Toggle only the appliances you own—stovetop, air fryer, rice cooker, microwave. Exclude allergens or mark items already on your shelf.",
|
||||
icon: RiRestaurantLine,
|
||||
title: "Pick meals you'll love",
|
||||
body: "Fill the week in one tap or choose dish by dish. We only suggest recipes your kitchen can actually make.",
|
||||
},
|
||||
{
|
||||
num: "03",
|
||||
title: "Pack the week with overlap",
|
||||
body: "Auto-fill prioritizes dishes that share open packages of produce, dairy, and aromatics—or pick slot-by-slot sorted by marginal cart cost.",
|
||||
},
|
||||
{
|
||||
num: "04",
|
||||
title: "Walk the aisles once",
|
||||
body: "Export one consolidated list grouped by store aisle, priced in whole packages the way checkout actually charges.",
|
||||
icon: RiShoppingBasket2Line,
|
||||
title: "Shop with one list",
|
||||
body: "Get a single grocery list sorted by aisle, priced the way the store really rings it up (based on averages from October 2026, Central USA).",
|
||||
},
|
||||
];
|
||||
|
||||
const FAMILY_COUNT = RECIPES.filter((r) => r.styles.includes("caydens_corner")).length;
|
||||
|
||||
const ctaClass = "h-11 gap-1.5 rounded-xl px-5 text-body-medium";
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<div className="flex min-h-dvh flex-col">
|
||||
<SiteHeader active="home" />
|
||||
|
||||
<main className="flex-1">
|
||||
{/* --- Editorial Hero ------------------------------------------------ */}
|
||||
<section className="paper-grain border-b border-line">
|
||||
<div className="mx-auto grid max-w-7xl items-center gap-12 px-4 py-14 sm:px-6 lg:grid-cols-[1.1fr_0.9fr] lg:py-22">
|
||||
<div>
|
||||
<p className="font-mono text-xs tracking-widest text-brand-300 uppercase">
|
||||
Weekly Meal & Grocery Ledger · Saved Locally
|
||||
</p>
|
||||
<h1 className="mt-3 font-serif text-4xl leading-[1.08] font-semibold tracking-tight text-balance sm:text-5xl lg:text-[3.4rem]">
|
||||
Cook a full week around{" "}
|
||||
<span className="italic text-brand-300">what groceries actually cost at checkout.</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-xl text-base leading-relaxed text-pretty text-ink-muted sm:text-lg">
|
||||
Most meal planners divide a pinch of cumin into imaginary pennies. Plan my Meals prices whole store packages,
|
||||
pairs recipes that share ingredients, and filters strictly to the appliances on your counter.
|
||||
</p>
|
||||
{/* Hero */}
|
||||
<section className="mx-auto grid max-w-6xl items-center gap-12 px-4 py-16 sm:px-6 lg:grid-cols-2 lg:py-24">
|
||||
<div>
|
||||
<h1 className="text-display-4-semibold text-balance text-text-primary sm:text-display-2-semibold">
|
||||
Good food all week, <span className="text-brand-300">tailored to your budget.</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-lg text-headline-regular text-pretty text-ink-muted">
|
||||
Plan My Meals takes the guesswork out of dinner. Build an entire week of meals around what’s already in your pantry, instantly price out missing ingredients, and explore over 400 high-quality recipes—100% free.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-wrap items-center gap-3">
|
||||
<Link
|
||||
href="/plan"
|
||||
className="trim-cap inline-flex min-h-12 items-center gap-2 rounded-lg bg-brand-600 px-6 py-3 font-semibold text-on-accent shadow-2xs transition-colors hover:bg-brand-500"
|
||||
>
|
||||
Start this week's plan
|
||||
<ArrowRight className="size-4" strokeWidth={2} aria-hidden />
|
||||
</Link>
|
||||
<Link
|
||||
href="/recipes"
|
||||
className="trim-cap inline-flex min-h-12 items-center gap-2 rounded-lg border border-line-strong bg-surface px-5 py-3 font-medium text-ink transition-colors hover:bg-raised"
|
||||
>
|
||||
Browse {RECIPES.length} recipes
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Architectural Ledger Metrics */}
|
||||
<dl className="mt-10 grid max-w-xl grid-cols-2 gap-x-6 gap-y-4 border-t border-line pt-6 sm:grid-cols-4">
|
||||
<div>
|
||||
<dt className="text-xs text-ink-faint">Tested recipes</dt>
|
||||
<dd className="font-mono text-2xl font-semibold text-ink tabular-nums">{RECIPES.length}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs text-ink-faint">Global cuisines</dt>
|
||||
<dd className="font-mono text-2xl font-semibold text-ink tabular-nums">{CUISINES.length}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs text-ink-faint">Appliances</dt>
|
||||
<dd className="font-mono text-2xl font-semibold text-ink tabular-nums">{APPLIANCES.length}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs text-ink-faint">Priced items</dt>
|
||||
<dd className="font-mono text-2xl font-semibold text-ink tabular-nums">{CATALOG.length}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="mt-8 flex flex-wrap items-center gap-3">
|
||||
<Link href="/plan" className={cx(buttonStyles.base, buttonStyles.variant.primary, ctaClass)}>
|
||||
Plan my week
|
||||
<RiArrowRightLine className="size-5" aria-hidden />
|
||||
</Link>
|
||||
<Link href="/recipes" className={cx(buttonStyles.base, buttonStyles.variant.secondary, ctaClass)}>
|
||||
Browse recipes
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<RotatingPreview sets={PREVIEW_SETS} />
|
||||
</div>
|
||||
|
||||
<RotatingPreview sets={PREVIEW_SETS} />
|
||||
</section>
|
||||
|
||||
{/* --- Interactive Package Overlap Proof ---------------------------- */}
|
||||
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:py-22">
|
||||
<div className="mb-8 max-w-2xl">
|
||||
<p className="font-mono text-xs tracking-widest text-brand-300 uppercase">Why the math matters</p>
|
||||
<h2 className="mt-2 font-serif text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
|
||||
Built for the checkout lane, not fictional per-gram math
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<OverlapExplorer dishes={OVERLAP_DATA.dishes} combos={OVERLAP_DATA.combos} />
|
||||
|
||||
{/* Three concise architectural pillars */}
|
||||
<div className="mt-8 grid gap-px overflow-hidden rounded-xl border border-line bg-line lg:grid-cols-3">
|
||||
<div className="bg-surface p-6">
|
||||
<ReceiptText className="size-5 text-brand-300" strokeWidth={1.75} aria-hidden />
|
||||
<h3 className="mt-3 font-serif text-lg font-semibold">Whole-package register totals</h3>
|
||||
<p className="mt-1.5 text-sm leading-relaxed text-ink-muted">
|
||||
You can't buy 0.3 lb of rice. Every ingredient rounds up to real store packages, showing both what you pay
|
||||
today and what carries over into next week's pantry.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-surface p-6">
|
||||
<Flame className="size-5 text-brand-300" strokeWidth={1.75} aria-hidden />
|
||||
<h3 className="mt-3 font-serif text-lg font-semibold">Appliance-honest filtering</h3>
|
||||
<p className="mt-1.5 text-sm leading-relaxed text-ink-muted">
|
||||
Cooking in a dorm or studio without an oven? Uncheck it once and every suggested recipe is guaranteed to work on
|
||||
the gear you actually own.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-surface p-6">
|
||||
<SlidersHorizontal className="size-5 text-brand-300" strokeWidth={1.75} aria-hidden />
|
||||
<h3 className="mt-3 font-serif text-lg font-semibold">Three-way pantry control</h3>
|
||||
<p className="mt-1.5 text-sm leading-relaxed text-ink-muted">
|
||||
Mark any store item as <strong className="font-medium text-ink">Have it</strong> ($0 at checkout),{" "}
|
||||
<strong className="font-medium text-ink">Use</strong>, or <strong className="font-medium text-ink">Avoid</strong>{" "}
|
||||
across an entire aisle like Seafood or Dairy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- Exposed-Border Workflow Ledger ------------------------------- */}
|
||||
<section id="how-it-works" className="border-y border-line bg-surface">
|
||||
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:py-20">
|
||||
<div className="max-w-2xl">
|
||||
<p className="font-mono text-xs tracking-widest text-brand-300 uppercase">How it works</p>
|
||||
<h2 className="mt-2 font-serif text-3xl font-semibold tracking-tight text-balance">
|
||||
Four steps from weekly budget to aisle-sorted list
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ol className="mt-10 grid gap-8 border-t border-line pt-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{STEPS.map((step) => (
|
||||
<li key={step.num} className="flex flex-col">
|
||||
<span className="font-mono text-xs font-semibold tracking-widest text-brand-300">{step.num}</span>
|
||||
<h3 className="mt-2 font-serif text-xl font-semibold">{step.title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-ink-muted">{step.body}</p>
|
||||
{/* How it works */}
|
||||
<section id="how-it-works" className="bg-background-secondary-default">
|
||||
<div className="mx-auto max-w-6xl px-4 py-16 sm:px-6 lg:py-20">
|
||||
<h2 className="text-display-4-semibold text-balance text-text-primary">Three easy steps</h2>
|
||||
<ol className="mt-8 grid gap-4 md:grid-cols-3">
|
||||
{STEPS.map(({ icon: Icon, title, body }) => (
|
||||
<li key={title} className="rounded-3xl border border-border-button-default bg-background-primary-default p-6">
|
||||
<span className="grid size-10 place-items-center rounded-full bg-button-ghost-background text-button-ghost-foreground">
|
||||
<Icon className="size-5" aria-hidden />
|
||||
</span>
|
||||
<h3 className="mt-4 text-title-3-semibold text-text-primary">{title}</h3>
|
||||
<p className="mt-1.5 text-body-regular text-ink-muted">{body}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- Editorial Cuisines & Cayden's Corner Spotlight --------------- */}
|
||||
<section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:py-22">
|
||||
<div className="grid gap-10 lg:grid-cols-[1.2fr_0.8fr] lg:items-center">
|
||||
<div>
|
||||
<p className="font-mono text-xs tracking-widest text-brand-300 uppercase">On the menu</p>
|
||||
<h2 className="mt-2 font-serif text-3xl font-semibold tracking-tight text-balance">
|
||||
{CUISINES.length} cuisines, from quick weeknight bowls to slow Sunday braises
|
||||
</h2>
|
||||
<ul className="mt-6 flex flex-wrap gap-2">
|
||||
{CUISINES.map((c) => (
|
||||
<li key={c.id} className="rounded-full border border-line-strong bg-surface px-3.5 py-1.5 text-sm text-ink">
|
||||
{c.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
href="/recipes"
|
||||
className="inline-flex items-center gap-1.5 font-medium text-brand-300 hover:underline"
|
||||
>
|
||||
Explore the full {RECIPES.length}-recipe index
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* What's cooking */}
|
||||
<section className="mx-auto grid max-w-6xl gap-10 px-4 py-16 sm:px-6 lg:grid-cols-[1.2fr_0.8fr] lg:items-center lg:py-20">
|
||||
<div>
|
||||
<h2 className="text-display-4-semibold text-balance text-text-primary">
|
||||
{RECIPES.length} recipes from {CUISINES.length} cuisines
|
||||
</h2>
|
||||
<p className="mt-3 max-w-lg text-headline-regular text-ink-muted">
|
||||
Quick weeknight bowls, cozy weekend braises, and everything in between.
|
||||
</p>
|
||||
<ul className="mt-6 flex flex-wrap gap-2">
|
||||
{CUISINES.map((c) => (
|
||||
<li key={c.id} className="rounded-full bg-background-secondary-default px-3.5 py-1.5 text-body-medium text-text-primary">
|
||||
{c.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Heirloom Collection Card */}
|
||||
<div className="paper-grain rounded-xl border border-brand-border bg-brand-950 p-6 sm:p-8">
|
||||
<div className="inline-flex items-center gap-2 font-mono text-xs font-semibold tracking-wider text-brand-300 uppercase">
|
||||
<BookHeart className="size-4" strokeWidth={1.75} aria-hidden />
|
||||
Heirloom Collection
|
||||
</div>
|
||||
<h3 className="mt-2 font-serif text-2xl font-semibold tracking-tight">Cayden's Corner</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-ink-muted">
|
||||
{FAMILY_COUNT} tried-and-true homemade family recipes—holiday sides, comfort mains, and classic bakes—scaled
|
||||
automatically to your table and integrated right into the weekly planner.
|
||||
</p>
|
||||
<Link
|
||||
href="/caydens-corner"
|
||||
className="trim-cap mt-5 inline-flex min-h-10 items-center gap-2 rounded-lg border border-line-strong bg-surface px-4 py-2 text-sm font-semibold text-ink transition-colors hover:bg-raised"
|
||||
>
|
||||
Open family cookbook
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="rounded-3xl border border-brand-border bg-brand-950 p-6 sm:p-8">
|
||||
<RiHeart3Line className="size-6 text-brand-300" aria-hidden />
|
||||
<h3 className="mt-3 text-title-1-semibold text-text-primary">Cayden's Corner</h3>
|
||||
<p className="mt-2 text-body-regular text-ink-muted">
|
||||
{FAMILY_COUNT} favorite family recipes, from holiday sides to Sunday bakes, ready to drop into your week.
|
||||
</p>
|
||||
<Link href="/caydens-corner" className={cx(buttonStyles.base, buttonStyles.variant.secondary, "mt-5", ctaClass)}>
|
||||
Open the family cookbook
|
||||
<RiArrowRightLine className="size-5" aria-hidden />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-line bg-surface">
|
||||
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-4 px-4 py-6 text-xs text-ink-faint sm:px-6">
|
||||
<footer className="border-t border-separator-border">
|
||||
<div className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-4 px-4 py-6 text-caption-1-regular text-ink-muted sm:px-6">
|
||||
<Logo small />
|
||||
<p>Estimates reflect average grocery prices as of October 2026 and vary by store and region.</p>
|
||||
<p>Prices are estimates from October 2026 and vary by store.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue