2026-10-31 20:44:29 -05:00
|
|
|
|
# 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
|
2026-11-06 20:41:36 -06:00
|
|
|
|
bun install
|
|
|
|
|
|
bun dev # http://localhost:3000 (landing) → /plan (planner)
|
|
|
|
|
|
bun test # money math, state validation, meal slots
|
2026-10-31 20:44:29 -05:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-11-06 20:41:36 -06:00
|
|
|
|
Production: `bun run build && bun start`. Deploys to Vercel as-is (no env vars needed).
|
2026-10-31 20:44:29 -05:00
|
|
|
|
|
2026-11-06 20:41:36 -06:00
|
|
|
|
Requires Bun (https://bun.sh); Next.js 16 also runs on Node 20.9+ if you prefer.
|
2026-10-31 20:44:29 -05:00
|
|
|
|
|
|
|
|
|
|
## 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.
|
2026-11-06 20:41:36 -06:00
|
|
|
|
- These all throw when the app loads (and so fail `bun run build`): unknown catalog ids, units that
|
2026-10-31 20:44:29 -05:00
|
|
|
|
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.
|