No description
Find a file
Cayden Schwechten 2cd683e64a Drop .cursor rules and npm lockfile; standardize on Bun; remove v1 migration code
- 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>
2026-11-06 20:41:36 -06:00
.idea init 2026-10-31 20:44:29 -05:00
app Drop .cursor rules and npm lockfile; standardize on Bun; remove v1 migration code 2026-11-06 20:41:36 -06:00
components Move UI to BoardUI with slate + salmon theme; add PDF plan export 2026-11-06 16:36:33 -06:00
styles Move UI to BoardUI with slate + salmon theme; add PDF plan export 2026-11-06 16:36:33 -06:00
utils Move UI to BoardUI with slate + salmon theme; add PDF plan export 2026-11-06 16:36:33 -06: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 Move UI to BoardUI with slate + salmon theme; add PDF plan export 2026-11-06 16:36:33 -06:00
bun.lock Move UI to BoardUI with slate + salmon theme; add PDF plan export 2026-11-06 16:36:33 -06: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.json Move UI to BoardUI with slate + salmon theme; add PDF plan export 2026-11-06 16:36:33 -06:00
README.md Drop .cursor rules and npm lockfile; standardize on Bun; remove v1 migration code 2026-11-06 20:41:36 -06: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

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:

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