Weekly Meal Planner - Product Requirements Document
1) Summary
Weekly Meal Planner is a single-user web app for one home cook who wants to choose dinners for the week and get one combined grocery list. The user saves recipes with ingredients, drops recipes into a 7-day by 3-slot plan (breakfast, lunch, dinner), and the app totals every ingredient across the plan into a checkable grocery list grouped by aisle. All data lives in the browser on one device.
- Assumption: storage is local only (IndexedDB); one person, one device, no accounts, no server.
- Assumption: "
plan a week" means assigning recipes to named day/slot cells for the 7 days Monday-Sunday of one chosen week, not generating meals automatically. - Assumption: a week starts Monday; the active week is identified by its Monday's local calendar date (
YYYY-MM-DD). - Assumption: caps: recipe name 60 characters, 40 ingredients per recipe, quantity 0.01-9999, servings 1-24, 200 saved recipes.
- Assumption: grocery quantity of an ingredient =
recipe quantity × (planned servings ÷ recipe base servings), summed per ingredient name and unit (formula in 4). - Assumption: ingredients merge only when lowercase trimmed name and unit are identical; no unit conversion (
1 cupand250 mlstay separate lines). - Assumption: the app is a responsive browser app for phone and desktop, not installable and not offline-first beyond local storage.
- Assumption: nothing is automatic: no reminders, no recipe import from URLs, no nutrition data, and editing a plan never silently changes ticked grocery items except as defined in 4.
2) Tech Stack
- Framework → React 18 + Vite → fast static build, no server needed
- Language → TypeScript → typed recipe and plan records
- Storage → IndexedDB via
idb-keyval→ local persistence, no account - Auth → None → single-user local app
- Styling → Tailwind CSS → utility styling, mobile first
- Dates →
date-fns→ week start and day labels - Deployment → Netlify static hosting → single-page app with one route
3) Users & Screens
Single user on one device; no accounts.
Screens:
| Screen | Reached from | Shows | Empty / loading / error | Back / close goes to |
|---|---|---|---|---|
| Plan | Tab Plan (default) | Active week grid, 7 rows × 3 slots | Empty cell shows + Add meal; see 6.2 | n/a (tab) |
| Recipes | Tab Recipes | Searchable recipe list | No recipes: No recipes yet. Add your first one.; see 6.1 | n/a (tab) |
| Recipe form | Add recipe button, or Edit on a recipe row | Name, servings, ingredients, steps | Validation errors inline; see 6.1 | Recipes |
| Recipe picker | Tap an empty or filled plan cell | Recipe list with search and a servings stepper | No recipes: Add a recipe first. with a Go to Recipes button; see 6.2 | Plan |
| Grocery | Tab Grocery | Aisle-grouped checklist for the active week | Empty plan: Plan some meals to build your list.; see 6.3 | n/a (tab) |
Navigation: bottom tab bar with three tabs (Plan, Recipes, Grocery) on screens under 768 px wide; the same three links in a top header at 768 px and above.
4) Data Model & Logic
Recipe
id: string (uuid)name: string (1-60 chars, exampleChicken Curry)baseServings: integer (1-24, default 2, example 4)ingredients: array of Ingredient (1-40)steps: string (0-2000 chars, free text)createdAt: ISO timestamp
Ingredient (embedded in Recipe)
name: string (1-40 chars, exampleonion)quantity: number (0.01-9999, example 2)unit: enumnone | g | kg | ml | l | tsp | tbsp | cup | piece | can | pack(defaultnone)aisle: enumProduce | Dairy | Meat & Fish | Pantry | Frozen | Bakery | Other(defaultOther)
PlanEntry
id: string (uuid)weekStart: string (YYYY-MM-DD, a Monday)day: integer 0-6 (0 = Monday)slot: enumbreakfast | lunch | dinnerrecipeId: stringservings: integer (1-24, defaults to the recipe'sbaseServings)
GroceryCheck
weekStart: stringkey: string (name|unit, lowercase)checked: boolean
Uniqueness, sort and retention: one PlanEntry per weekStart + day + slot; assigning to a filled cell replaces it. Recipes sort by name A-Z, case-insensitive. Plan entries and checks are kept indefinitely; nothing is auto-deleted.
Logic:
monday(date) = startOfWeek(date, { weekStartsOn: 1 })
scale(ing, entry) = ing.quantity * entry.servings / recipe.baseServings
groceryLines(weekStart):
for each entry in plan where entry.weekStart == weekStart:
for each ing in recipe(entry.recipeId).ingredients:
key = lower(trim(ing.name)) + "|" + ing.unit
total[key] += scale(ing, entry) // aisle taken from first ingredient seen
return lines sorted by aisle order, then name A-Z
display quantity = round(total, 2), trailing zeros removed
Aisle order: Produce, Dairy, Meat & Fish, Bakery, Pantry, Frozen, Other.
Worked example (week starting Monday 2024-03-04):
Chicken Curry, base 4:onion2 piece,rice300 g.Veg Omelette, base 2:onion1 piece,egg4 piece.- Plan: Mon dinner, Curry at 2 servings → onion
2 × 2/4 = 1, rice300 × 2/4 = 150. Tue breakfast, Omelette at 2 servings → onion1 × 2/2 = 1, egg4. Sun dinner (2024-03-10), Curry at 8 servings → onion2 × 8/4 = 4, rice300 × 8/4 = 600. - Grocery:
onionpiece1 + 1 + 4 = 6,riceg150 + 600 = 750,eggpiece4. - Crossing midnight: an entry for Sunday
2024-03-10belongs to week2024-03-04. At 00:05 on2024-03-11the app opens on the week of2024-03-11and the Sunday entry now appears only underPrevious week.
Status transitions (GroceryCheck checked):
| From | Event | To | Side effect |
|---|---|---|---|
| unchecked | Tap line | checked | Line moves to the Done group, struck through |
| checked | Tap line | unchecked | Line returns to its aisle group |
| checked | Quantity of that line changes after a plan edit | unchecked | Line reappears in its aisle |
| checked or unchecked | Line total drops to 0 (all sources removed) | removed | GroceryCheck row deleted |
Edits and edge cases:
- Editing a recipe recomputes all grocery lines and plan displays immediately; planned servings stay as chosen.
- Deleting a recipe used in any plan entry asks for confirmation and removes those entries.
- Removing a plan entry recomputes the grocery list; checks stay only on lines whose total is unchanged.
- Duplicate ingredient names inside one recipe are summed.
- Empty week: grocery list is empty. Quantity cap 9999 is enforced at input.
5) External Services
None. All data is local and no third-party API is called.
6) Core Features
6.1 Recipe Library
User Flow:
- User opens
Recipesand tapsAdd recipe. - Recipe form shows fields
Name,Servings(stepper, default2),IngredientsandSteps. - Under
Ingredients, user taps+ Add ingredient; each row hasName,Qty, aUnitselect and anAisleselect, plus a✕remove button. - User taps
Save recipe. The record is written to IndexedDB and the user returns to the list. - List rows show name and
{n} servings, withEditandDeletebuttons. A search box (Search recipes) filters by name as the user types. Deleteopens a confirm dialogDelete "{name}"? This also removes it from your plan.withDeleteandCancel.
UI Copy & Colors:
- Add button:
Add recipe, text#FFFFFFon#16A34A - Save button:
Save recipe, text#FFFFFFon#16A34A - Delete confirm button:
Delete, text#FFFFFFon#DC2626 - Row subtitle:
{n} servings, text#64748Bon#FFFFFF
Error & Empty States:
- No recipes →
No recipes yet. Add your first one.Recovery: tapAdd recipe. - Name empty →
Give your recipe a name.Recovery: type a name. - No ingredient rows →
Add at least one ingredient.Recovery: tap+ Add ingredient. - Quantity outside 0.01-9999 →
Quantity must be between 0.01 and 9999.Recovery: correct the value. - 200 recipes reached →
Recipe limit reached (200). Delete one to add more.Recovery: delete a recipe. - Search with no match →
No recipes match "{text}".Recovery: clear the search.
Acceptance Checks:
- ✓ Saving
Chicken Curry, servings4, withonion 2 pieceandrice 300 gshows a rowChicken Curry/4 servings. - ✓ Saving with an empty name shows
Give your recipe a name.and nothing is stored. - ✓ Typing
currin search showsChicken Curryand hidesVeg Omelette. - ✓ Deleting a recipe used on Monday dinner removes that plan cell's content after confirming.
6.2 Weekly Plan
User Flow:
- User opens
Plan. The header shows the week range (for exampleMar 4 - Mar 10) with‹and›buttons for previous and next week, and aTodaybutton that returns to the current week. - The grid lists Monday-Sunday as rows, each with
Breakfast,LunchandDinnercells. Today's row has a#DBEAFEbackground. - User taps a cell. The recipe picker opens with search and a servings stepper that defaults to the chosen recipe's
baseServings. - User taps a recipe row, adjusts servings if needed, and taps
Add to plan. The cell shows the recipe name and×{servings}. - Tapping a filled cell reopens the picker with the current recipe selected; the picker adds a
Removebutton that clears the cell. - Assigning a recipe to a filled cell replaces the old entry. Every change saves immediately.
UI Copy & Colors:
- Empty cell:
+ Add meal, text#64748Bon#F8FAFC - Filled cell:
{name} ×{servings}, text#0F172Aon#FFFFFF - Confirm button:
Add to plan, text#FFFFFFon#16A34A - Remove button:
Remove, text#DC2626on#FFFFFF - Week range header:
{MMM d} - {MMM d}, text#0F172A
Error & Empty States:
- Picker with zero recipes →
Add a recipe first.Recovery: tapGo to Recipes. - Servings outside 1-24 → stepper stops at the limit; no error text.
- Saving fails (IndexedDB unavailable) →
Couldn't save. Check browser storage settings.Recovery: allow storage and retry.
Acceptance Checks:
- ✓ Tapping Monday
Dinner, choosingChicken Curry, setting servings2showsChicken Curry ×2in that cell. - ✓ Choosing
Veg Omelettefor the same cell replaces the curry; only one entry remains for that cell. - ✓ Tapping
›from the week of2024-03-04showsMar 11 - Mar 17with an empty grid;Todayreturns to the current week. - ✓
Removeempties the cell and shows+ Add meal.
6.3 Grocery List
User Flow:
- User opens
Grocery. The app builds lines for the active week from the plan (formula in 4). - Lines are grouped under aisle headings in the order Produce, Dairy, Meat & Fish, Bakery, Pantry, Frozen, Other; empty aisles are hidden.
- Each line shows a checkbox,
{name}, and{quantity} {unit}(unit omitted fornone). - Tapping a line toggles its checked state; checked lines move to a
Donegroup at the bottom, struck through. - The header shows
{checked} of {total} itemsand aUncheck allbutton. - Changing the week on
Planchanges the list here; checks are stored per week.
UI Copy & Colors:
- Aisle heading:
Produceand so on, text#64748B, 14 px uppercase - Unchecked line: text
#0F172Aon#FFFFFF - Checked line: text
#64748Bon#F8FAFC, strikethrough - Progress:
{checked} of {total} items, text#16A34A - Reset button:
Uncheck all, text#2563EBon#FFFFFF
Error & Empty States:
- No plan entries this week →
Plan some meals to build your list.Recovery: tap thePlantab. - Storage failure on check →
Couldn't save your tick. Try again.Recovery: tap the line again.
Acceptance Checks:
- ✓ With the worked example in 4, the list shows
onion6 piece,rice750 g,egg4 piece, withonionunder Produce. - ✓ Ticking
ricechanges the header from0 of 3 itemsto1 of 3 itemsand movesricetoDone. - ✓ Changing the Sunday curry from 8 to 4 servings changes
ricefrom750 gto450 gand unticks it if it was ticked. - ✓ Removing every plan entry shows
Plan some meals to build your list.
7) UI/UX
Palette:
| Token | Hex | Used for |
|---|---|---|
| primary | #16A34A | Main buttons, progress |
| link | #2563EB | Secondary text buttons, active tab |
| today | #DBEAFE | Today's row highlight |
| danger | #DC2626 | Delete, Remove |
| text | #0F172A | Body text |
| muted | #64748B | Captions, empty cells |
| surface | #F8FAFC | Page and empty cell background |
| card | #FFFFFF | Cards, list rows |
Typography: Inter, 16 px base; screen headings 24 px semibold; week range 18 px; captions 14 px.
Layout: single column, mobile first, max width 480 px centred on phones; at 768 px and above the content is max 720 px wide centred with the tabs in the header. The week grid is the hero element on Plan.
Motion:
| Interaction | Duration | Easing |
|---|---|---|
| Picker open/close | 200 ms | ease-out |
| Line check move to Done | 150 ms | ease-in-out |
| Tab change fade | 120 ms | linear |
Timestamp display: the week range uses date-fns format MMM d for both ends; the day label uses EEE d (for example Mon 4); no other times are shown.
Accessibility: text contrast at least 4.5:1, tap targets at least 44×44 px, visible 2 px focus ring in #2563EB, and aria-label on icon buttons (‹, ›, ✕).
8) Infrastructure Notes
Environment variables: None.
Client vs server: everything runs in the browser; Netlify only serves static files.
Platform limits:
| Limit | Platforms affected | Design answer |
|---|---|---|
| Browser storage can be cleared, and Safari may evict data of sites unused for 7 days | Safari (iPhone and Mac) | Call navigator.storage.persist() on first save; no export in V1 |
| Private browsing blocks or wipes IndexedDB | All browsers | Show Couldn't save. Check browser storage settings. on failure |
| Single route only | Netlify | One page with tab state in memory; no host fallback needed |
9) Out of Scope (V1)
- Recipe import from URLs - tempting for speed, but it needs scraping or a server; recipes are typed by hand.
- Unit conversion between
cup,mlandg- tempting for tidier lists, but it needs density data; lines merge only on identical units. - Nutrition and calorie tracking - needs a food database; the pitch asks only for plans and lists.
- Pantry inventory (subtract what you already have) - a natural next step, but it adds a second data set to maintain.
- Sharing the plan or list with a partner - needs accounts and sync; V1 is one person, one device.
- Automatic meal suggestions - the user plans by choice; no recommendation logic.
- Photos on recipes - heavy storage for a text-first tool.
10) Success Criteria
- ✓ Saving
Chicken Curry(servings4,onion 2 piece,rice 300 g) adds a row readingChicken Curry/4 servings. - ✓ Saving a recipe with an empty name shows
Give your recipe a name.and stores nothing. - ✓ Assigning the curry at
2servings to Monday dinner showsChicken Curry ×2in that cell. - ✓ Assigning a second recipe to the same cell replaces the first.
- ✓ The week of
2024-03-04in the worked example producesonion 6 piece,rice 750 g,egg 4 piece. - ✓ Ticking
ricechanges the header from0 of 3 itemsto1 of 3 items. - ✓ Lowering the Sunday curry from 8 to 4 servings changes
riceto450 gand unticks it. - ✓ After a page reload, recipes, plan entries and ticks are still present.
- ✓ Deleting a recipe used in the plan, after confirming, empties its plan cells and updates the grocery list.
- ✓ No console errors during the flow add recipe → plan a week → tick grocery items.