init
This commit is contained in:
commit
c19b910c36
80 changed files with 16134 additions and 0 deletions
144
README.md
Normal file
144
README.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# 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
|
||||
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:
|
||||
|
||||
```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 `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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue