# 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 `"-"`, 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 ` `; 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.