No description
Find a file
Cayden Schwechten b02c7a024b Merge branch 'master'
# Conflicts:
#	README.md
2026-10-31 20:46:57 -05:00
.idea init 2026-10-31 20:44:29 -05:00
app init 2026-10-31 20:44:29 -05:00
.gitignore init 2026-10-31 20:44:29 -05:00
.gitlab-ci.yml Configure Secret Detection in .gitlab-ci.yml, creating this file if it does not already exist 2026-10-31 20:45:23 -05:00
AGENTS.md init 2026-10-31 20:44:29 -05:00
bun.lock init 2026-10-31 20:44:29 -05:00
eslint.config.mjs init 2026-10-31 20:44:29 -05:00
next.config.ts init 2026-10-31 20:44:29 -05:00
package-lock.json init 2026-10-31 20:44:29 -05:00
package.json init 2026-10-31 20:44:29 -05:00
README.md Initial commit 2026-10-31 20:45:21 -05:00
tsconfig.json init 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

npm install
npm run dev        # http://localhost:3000 (landing) → /plan (planner)
npm test           # money math, state validation, meal slots (needs Bun: https://bun.sh)

Production: npm run build && npm start. Deploys to Vercel as-is (no env vars needed).

Requires Node 20.9+ (Next.js 16).

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:

{ 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 npm 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.