planmymeal/app/page.tsx

155 lines
8.1 KiB
TypeScript
Raw Normal View History

import { RiArrowRightLine, RiHeart3Line, RiRestaurantLine, RiShoppingBasket2Line, RiWallet3Line } from "@remixicon/react";
2026-10-31 20:44:29 -05:00
import Link from "next/link";
import { RotatingPreview, type PreviewSet } from "./components/RotatingPreview";
import { Logo, SiteHeader } from "./components/SiteHeader";
import { PANTRY_STAPLE_IDS } from "./lib/catalog";
2026-10-31 20:44:29 -05:00
import { buildGroceryList } from "./lib/grocery";
import { CUISINES } from "./lib/plan";
2026-10-31 20:44:29 -05:00
import { RECIPES } from "./lib/recipes";
import { formatUsd } from "./lib/units";
import { buttonStyles } from "@/components/base/buttons/button";
import { cx } from "@/utils/cx";
2026-10-31 20:44:29 -05:00
// 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.
2026-10-31 20:44:29 -05:00
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"]] },
{ theme: "Shared spice rack", blurb: "High overlap", meals: [["Mon · Dinner", "chicken-curry"], ["Tue · Lunch", "red-lentil-dal"], ["Tue · Dinner", "pad-thai"]] },
{ theme: "Weeknight seafood", blurb: "Budget catch", meals: [["Mon · Dinner", "garlic-butter-shrimp-and-rice"], ["Tue · Lunch", "tuna-chickpea-pitas"], ["Tue · Dinner", "baja-fish-tacos"]] },
{ theme: "Dorm & studio kitchen", blurb: "Microwave + rice cooker", meals: [["Mon · Breakfast", "berry-banana-smoothie"], ["Mon · Lunch", "microwave-buffalo-chicken-rice-bowls"], ["Mon · Dinner", "rice-cooker-arroz-con-pollo"]] },
{ theme: "Plant-forward week", blurb: "Meatless & filling", meals: [["Mon · Breakfast", "shakshuka"], ["Mon · Lunch", "chana-masala"], ["Mon · Dinner", "black-bean-enchiladas"]] },
{ theme: "Sunday batch prep", blurb: "Cook once, eat well", meals: [["Breakfasts", "baked-oatmeal-squares"], ["Lunches", "slow-cooker-salsa-chicken"], ["Dinners", "chicken-burrito-bowls"]] },
{ theme: "Korean pantry week", blurb: "One tub of gochujang", meals: [["Mon · Dinner", "bulgogi-beef-bowls"], ["Tue · Lunch", "kimchi-fried-rice"], ["Tue · Dinner", "dakgalbi-spicy-chicken-stir-fry"]] },
];
const PREVIEW_SETS: PreviewSet[] = PREVIEW_THEMES.map(({ theme, blurb, meals }) => {
const options = { householdSize: 2, onHandItems: PANTRY_STAPLE_IDS };
const recipe = (id: string) => {
const found = RECIPES.find((r) => r.id === id);
if (!found) throw new Error(`Landing preview: unknown recipe id "${id}"`);
return found;
};
const cart = buildGroceryList(meals.map(([, id]) => id), options);
return {
theme,
blurb,
meals: meals.map(([slot, id]) => ({ slot, title: recipe(id).title, cost: formatUsd(buildGroceryList([id], options).usedTotal) })),
total: formatUsd(cart.checkoutTotal),
sharedItems: cart.sharedItemCount,
itemCount: cart.lines.length,
};
});
const STEPS = [
{
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.",
2026-10-31 20:44:29 -05:00
},
{
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.",
2026-10-31 20:44:29 -05:00
},
{
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).",
2026-10-31 20:44:29 -05:00
},
];
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";
2026-10-31 20:44:29 -05:00
export default function LandingPage() {
return (
<div className="flex min-h-dvh flex-col">
<SiteHeader active="home" />
<main className="flex-1">
{/* 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={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>
2026-10-31 20:44:29 -05:00
</div>
</div>
<RotatingPreview sets={PREVIEW_SETS} />
2026-10-31 20:44:29 -05:00
</section>
{/* 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>
2026-10-31 20:44:29 -05:00
</li>
))}
</ol>
</div>
</section>
{/* 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>
2026-10-31 20:44:29 -05:00
<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&apos;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>
2026-10-31 20:44:29 -05:00
</div>
</section>
</main>
<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">
2026-10-31 20:44:29 -05:00
<Logo small />
<p>Prices are estimates from October 2026 and vary by store.</p>
2026-10-31 20:44:29 -05:00
</div>
</footer>
</div>
);
}