- Delete .cursor/rules/boardui.mdc (rules live in AGENTS.md) and package-lock.json; README now uses bun install/dev/test/build - Remove migrateV1Slots, the v1 localStorage key, the legacy "asian" cuisine expansion, the ownsPantryStaples fallback, and the pre-BoardUI theme-key carryover in the layout init script; drop their tests Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
144 lines
7.7 KiB
Markdown
144 lines
7.7 KiB
Markdown
# Plan my Meals: Budget Meal Planner
|
||
|
||
Plan 1–7 days of meals around a weekly grocery budget and the appliances you actually own.
|
||
Costs are estimated with **whole-package** pricing, so the total is what you'd pay at checkout,
|
||
not a misleading "per-serving" number.
|
||
|
||
## Run it
|
||
|
||
```bash
|
||
bun install
|
||
bun dev # http://localhost:3000 (landing) → /plan (planner)
|
||
bun test # money math, state validation, meal slots
|
||
```
|
||
|
||
Production: `bun run build && bun start`. Deploys to Vercel as-is (no env vars needed).
|
||
|
||
Requires Bun (https://bun.sh); Next.js 16 also runs on Node 20.9+ if you prefer.
|
||
|
||
## How it works
|
||
|
||
```
|
||
app/
|
||
├── page.tsx Landing page (/): what the app is + how it works; links to /plan
|
||
├── plan/page.tsx Planner route (/plan); renders PlannerLoader
|
||
├── recipes/page.tsx Recipe library (/recipes): search + filters
|
||
├── caydens-corner/page.tsx Cayden's Corner (/caydens-corner): family recipes, by section
|
||
├── recipes/[id]/page.tsx One static page per recipe: ingredients, steps, servings scaler
|
||
├── actions.ts Server Action: generateMealPlanAction (validates input → planner)
|
||
├── components/
|
||
│ ├── RecipeDetail.tsx Ingredients (scaled) + instructions; shared by pages, picker and dialog
|
||
│ ├── RecipeDialog.tsx Opens when you click a planned meal: recipe + swap/remove
|
||
│ ├── RecipeBrowser.tsx Client-side search/filter grid for /recipes
|
||
│ ├── SiteHeader.tsx Header for landing + recipe pages
|
||
│ ├── RotatingPreview.tsx Landing-page hero card cycling through example meal sets
|
||
│ ├── PlannerLoader.tsx Loads the dashboard client-only (state lives in localStorage)
|
||
│ ├── PlannerDashboard.tsx Wires state → live summary → children
|
||
│ ├── Navbar.tsx Budget meter + save status
|
||
│ ├── ConfigPanel.tsx Budget, days, meals/day, people, appliances, cuisines, styles
|
||
│ ├── IngredientPanel.tsx Per-ingredient Avoid / Use / Have it switches
|
||
│ ├── MealGrid.tsx Day cards with slots (click to pick, drag to swap on desktop)
|
||
│ ├── MealSelectorModal.tsx Recipe picker sorted by *marginal* cost to your cart
|
||
│ ├── GroceryListDrawer.tsx Cost summary + aisle-grouped list (panel on desktop, sheet on mobile)
|
||
│ ├── BudgetMeter.tsx
|
||
│ └── Chip.tsx The on/off filter pill used by every filter row
|
||
├── lib/
|
||
│ ├── catalog.ts Store catalog (average grocery prices as of October 2026)
|
||
│ ├── recipes/ 421 recipes across 12 cuisines (see "Adding recipes" below)
|
||
│ │ ├── caydens-corner.ts Family recipes (44)
|
||
│ │ └── steps/ Cooking instructions, keyed by recipe id
|
||
│ ├── display.ts Ingredient lines with scaling + kitchen fractions (¾ cup)
|
||
│ ├── sanitize.ts Validates + migrates state (shared by Server Action and localStorage load)
|
||
│ ├── pricing.ts PriceProvider interface: the seam for real prices later
|
||
│ ├── units.ts Recipe units → package units (cups of rice → grams)
|
||
│ ├── grocery.ts Full grocery list, plus Cart: fast "what does this recipe add?"
|
||
│ ├── planner.ts Greedy, overlap-aware auto-fill
|
||
│ ├── plan.ts Slot helpers, labels, cuisines, "can I cook this?" blockers
|
||
│ └── usePlanState.ts Reducer + localStorage persistence
|
||
└── types/index.ts
|
||
```
|
||
|
||
**Where the math runs:** cost recalculation runs in the browser using the same pure functions as
|
||
the server, so every slider tweak updates instantly. Generation runs in a Server Action, so a
|
||
real price API key would stay server-side.
|
||
|
||
**Planner scoring** (lower wins, per slot, in day order):
|
||
|
||
```
|
||
marginal checkout cost + 0.25 × marginal eaten value + $1.50 × times already used + up to $2.50 jitter
|
||
```
|
||
|
||
Hard rules: only recipes your appliances can cook and that use no avoided ingredients; never the
|
||
same dish twice in one day; max 2 uses per week (relaxed only if nothing else fits, with a note
|
||
shown). Preference filters relax style → cuisine if they'd leave slots empty. Appliances and avoided
|
||
ingredients are never relaxed.
|
||
|
||
**Meal slots** are keyed `"<day>-<meal>"`, and meal numbers are fixed: 1 = Breakfast, 2 = Lunch,
|
||
3 = Dinner. "Meals per day" only decides which are visible (2 → Lunch + Dinner), the same way "Days"
|
||
hides days without deleting them. Clear and Shuffle only touch what's visible. Plans saved by older
|
||
versions (which counted from the first visible meal) are converted once on load.
|
||
|
||
## Ingredient preferences
|
||
|
||
Every catalog item has a three-way setting (Ingredients panel, or "Have it" on any grocery line):
|
||
|
||
| Setting | Effect |
|
||
| --- | --- |
|
||
| **Avoid** | Recipes using it are never auto-planned; already-planned ones are flagged |
|
||
| **Use** | Default |
|
||
| **Have it** | Stays on the shopping list but costs $0 (pantry staples start here) |
|
||
|
||
Each aisle also has an **Avoid all** button, e.g. the Seafood aisle for a fish/shellfish allergy.
|
||
|
||
## Seafood
|
||
|
||
`recipes/seafood.ts` holds 13 seafood dishes (plus Shrimp & Grits in the Southern file), all tagged
|
||
with the `seafood` style. Pick **🐟 Seafood** under Cooking style (or in the meal picker) to lean the
|
||
plan toward fish. All fish and shellfish live in one **Seafood** aisle on lists. Budget picks: canned
|
||
tuna/clams, pollock, tilapia, imitation crab. Splurges: shrimp, salmon.
|
||
|
||
## Swapping in real prices
|
||
|
||
1. Fetch prices server-side (in `actions.ts` or a route handler).
|
||
2. Build a `StoreItem[]` snapshot and wrap it: `new SnapshotPriceProvider("Live grocery prices", items)`.
|
||
3. Pass it via the `provider` option to `buildGroceryList` / the planner.
|
||
|
||
Item IDs in `recipes.ts` reference catalog IDs, so a real provider needs a mapping from those IDs to
|
||
store SKUs.
|
||
|
||
## Known limits (v1)
|
||
|
||
- Prices are estimates based on average grocery prices as of October 2026.
|
||
- Drag-to-swap is desktop-only (HTML5 drag & drop doesn't fire on touch); mobile uses tap-to-pick.
|
||
- Greedy planning is fast and predictable but not globally optimal.
|
||
- Leftover carryover isn't carried into *next* week's plan yet.
|
||
|
||
## Cayden's Corner
|
||
|
||
Family recipes: 4 appetizers & sides, 28 mains (one breakfast), 12 desserts.
|
||
Every recipe has the `caydens_corner` style, so it shows up under **🏠 Cayden's Corner** in the
|
||
library filters, the planner's Cooking style chips and the meal picker. Directions are in the
|
||
original wording (`recipes/steps/caydens-corner.ts`); small clarifications are in [brackets].
|
||
|
||
Sides and desserts are their own meal types: they're in the library and can be added to a slot
|
||
by hand, but auto-fill never picks them.
|
||
|
||
## Adding recipes
|
||
|
||
The big library in `app/lib/recipes/extra/` uses a compact one-entry-per-recipe format
|
||
(`recipes/dish.ts`). Ingredients are written like a shopping list:
|
||
|
||
```ts
|
||
{ title: "Beef Stroganoff", mins: 30, tools: ["stovetop"], styles: ["comfort"],
|
||
about: "Creamy mushroom gravy over egg noodles.",
|
||
items: "ground_beef 1 lb; egg_noodles 12 oz; mushrooms 8 oz; sour_cream 0.75 cup" },
|
||
```
|
||
|
||
- Each ingredient is `<catalog id> <amount> <unit>`; units: g, oz, lb, ml, tsp, tbsp, cup, each.
|
||
- These all throw when the app loads (and so fail `bun run build`): unknown catalog ids, units that
|
||
can't convert to how the item is sold, unknown formats, and duplicate ids.
|
||
- Cups/tbsp on an item sold by weight need `gramsPerMl` on that catalog item.
|
||
- Add the recipe's steps to the matching file in `recipes/steps/`, keyed by its id. The app refuses to
|
||
load if any recipe has no steps, or if steps exist for a recipe that doesn't.
|
||
- `id` defaults to a slug of the title. Set it explicitly before renaming a recipe, or saved plans
|
||
that used the old id will drop that meal.
|