Example app spec: a meal planner

THE WHOLE IDEA WE TYPED

“A recipe app that plans a week of meals and builds the grocery list”

Generated by SpecWise with the Balanced model (3 credits) · 2,730 words · unedited

Want one for your idea?

Describe your idea in a sentence and get a spec like this in about a minute.

Generate my spec

7-day free trial · no card needed

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 cup and 250 ml stay 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:

ScreenReached fromShowsEmpty / loading / errorBack / close goes to
PlanTab Plan (default)Active week grid, 7 rows × 3 slotsEmpty cell shows + Add meal; see 6.2n/a (tab)
RecipesTab RecipesSearchable recipe listNo recipes: No recipes yet. Add your first one.; see 6.1n/a (tab)
Recipe formAdd recipe button, or Edit on a recipe rowName, servings, ingredients, stepsValidation errors inline; see 6.1Recipes
Recipe pickerTap an empty or filled plan cellRecipe list with search and a servings stepperNo recipes: Add a recipe first. with a Go to Recipes button; see 6.2Plan
GroceryTab GroceryAisle-grouped checklist for the active weekEmpty plan: Plan some meals to build your list.; see 6.3n/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, example Chicken 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, example onion)
  • quantity: number (0.01-9999, example 2)
  • unit: enum none | g | kg | ml | l | tsp | tbsp | cup | piece | can | pack (default none)
  • aisle: enum Produce | Dairy | Meat & Fish | Pantry | Frozen | Bakery | Other (default Other)

PlanEntry

  • id: string (uuid)
  • weekStart: string (YYYY-MM-DD, a Monday)
  • day: integer 0-6 (0 = Monday)
  • slot: enum breakfast | lunch | dinner
  • recipeId: string
  • servings: integer (1-24, defaults to the recipe's baseServings)

GroceryCheck

  • weekStart: string
  • key: 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: onion 2 piece, rice 300 g.
  • Veg Omelette, base 2: onion 1 piece, egg 4 piece.
  • Plan: Mon dinner, Curry at 2 servings → onion 2 × 2/4 = 1, rice 300 × 2/4 = 150. Tue breakfast, Omelette at 2 servings → onion 1 × 2/2 = 1, egg 4. Sun dinner (2024-03-10), Curry at 8 servings → onion 2 × 8/4 = 4, rice 300 × 8/4 = 600.
  • Grocery: onion piece 1 + 1 + 4 = 6, rice g 150 + 600 = 750, egg piece 4.
  • Crossing midnight: an entry for Sunday 2024-03-10 belongs to week 2024-03-04. At 00:05 on 2024-03-11 the app opens on the week of 2024-03-11 and the Sunday entry now appears only under Previous week.

Status transitions (GroceryCheck checked):

FromEventToSide effect
uncheckedTap linecheckedLine moves to the Done group, struck through
checkedTap lineuncheckedLine returns to its aisle group
checkedQuantity of that line changes after a plan edituncheckedLine reappears in its aisle
checked or uncheckedLine total drops to 0 (all sources removed)removedGroceryCheck 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:

  1. User opens Recipes and taps Add recipe.
  2. Recipe form shows fields Name, Servings (stepper, default 2), Ingredients and Steps.
  3. Under Ingredients, user taps + Add ingredient; each row has Name, Qty, a Unit select and an Aisle select, plus a ✕ remove button.
  4. User taps Save recipe. The record is written to IndexedDB and the user returns to the list.
  5. List rows show name and {n} servings, with Edit and Delete buttons. A search box (Search recipes) filters by name as the user types.
  6. Delete opens a confirm dialog Delete "{name}"? This also removes it from your plan. with Delete and Cancel.

UI Copy & Colors:

  • Add button: Add recipe, text #FFFFFF on #16A34A
  • Save button: Save recipe, text #FFFFFF on #16A34A
  • Delete confirm button: Delete, text #FFFFFF on #DC2626
  • Row subtitle: {n} servings, text #64748B on #FFFFFF

Error & Empty States:

  • No recipes → No recipes yet. Add your first one. Recovery: tap Add 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, servings 4, with onion 2 piece and rice 300 g shows a row Chicken Curry / 4 servings.
  • ✓ Saving with an empty name shows Give your recipe a name. and nothing is stored.
  • ✓ Typing curr in search shows Chicken Curry and hides Veg Omelette.
  • ✓ Deleting a recipe used on Monday dinner removes that plan cell's content after confirming.

6.2 Weekly Plan

User Flow:

  1. User opens Plan. The header shows the week range (for example Mar 4 - Mar 10) with ‹ and › buttons for previous and next week, and a Today button that returns to the current week.
  2. The grid lists Monday-Sunday as rows, each with Breakfast, Lunch and Dinner cells. Today's row has a #DBEAFE background.
  3. User taps a cell. The recipe picker opens with search and a servings stepper that defaults to the chosen recipe's baseServings.
  4. User taps a recipe row, adjusts servings if needed, and taps Add to plan. The cell shows the recipe name and ×{servings}.
  5. Tapping a filled cell reopens the picker with the current recipe selected; the picker adds a Remove button that clears the cell.
  6. Assigning a recipe to a filled cell replaces the old entry. Every change saves immediately.

UI Copy & Colors:

  • Empty cell: + Add meal, text #64748B on #F8FAFC
  • Filled cell: {name} ×{servings}, text #0F172A on #FFFFFF
  • Confirm button: Add to plan, text #FFFFFF on #16A34A
  • Remove button: Remove, text #DC2626 on #FFFFFF
  • Week range header: {MMM d} - {MMM d}, text #0F172A

Error & Empty States:

  • Picker with zero recipes → Add a recipe first. Recovery: tap Go 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, choosing Chicken Curry, setting servings 2 shows Chicken Curry ×2 in that cell.
  • ✓ Choosing Veg Omelette for the same cell replaces the curry; only one entry remains for that cell.
  • ✓ Tapping › from the week of 2024-03-04 shows Mar 11 - Mar 17 with an empty grid; Today returns to the current week.
  • ✓ Remove empties the cell and shows + Add meal.

6.3 Grocery List

User Flow:

  1. User opens Grocery. The app builds lines for the active week from the plan (formula in 4).
  2. Lines are grouped under aisle headings in the order Produce, Dairy, Meat & Fish, Bakery, Pantry, Frozen, Other; empty aisles are hidden.
  3. Each line shows a checkbox, {name}, and {quantity} {unit} (unit omitted for none).
  4. Tapping a line toggles its checked state; checked lines move to a Done group at the bottom, struck through.
  5. The header shows {checked} of {total} items and a Uncheck all button.
  6. Changing the week on Plan changes the list here; checks are stored per week.

UI Copy & Colors:

  • Aisle heading: Produce and so on, text #64748B, 14 px uppercase
  • Unchecked line: text #0F172A on #FFFFFF
  • Checked line: text #64748B on #F8FAFC, strikethrough
  • Progress: {checked} of {total} items, text #16A34A
  • Reset button: Uncheck all, text #2563EB on #FFFFFF

Error & Empty States:

  • No plan entries this week → Plan some meals to build your list. Recovery: tap the Plan tab.
  • 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 onion 6 piece, rice 750 g, egg 4 piece, with onion under Produce.
  • ✓ Ticking rice changes the header from 0 of 3 items to 1 of 3 items and moves rice to Done.
  • ✓ Changing the Sunday curry from 8 to 4 servings changes rice from 750 g to 450 g and unticks it if it was ticked.
  • ✓ Removing every plan entry shows Plan some meals to build your list.

7) UI/UX

Palette:

TokenHexUsed for
primary#16A34AMain buttons, progress
link#2563EBSecondary text buttons, active tab
today#DBEAFEToday's row highlight
danger#DC2626Delete, Remove
text#0F172ABody text
muted#64748BCaptions, empty cells
surface#F8FAFCPage and empty cell background
card#FFFFFFCards, 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:

InteractionDurationEasing
Picker open/close200 msease-out
Line check move to Done150 msease-in-out
Tab change fade120 mslinear

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:

LimitPlatforms affectedDesign answer
Browser storage can be cleared, and Safari may evict data of sites unused for 7 daysSafari (iPhone and Mac)Call navigator.storage.persist() on first save; no export in V1
Private browsing blocks or wipes IndexedDBAll browsersShow Couldn't save. Check browser storage settings. on failure
Single route onlyNetlifyOne page with tab state in memory; no host fallback needed

9) Out of Scope (V1)

  1. Recipe import from URLs - tempting for speed, but it needs scraping or a server; recipes are typed by hand.
  2. Unit conversion between cup, ml and g - tempting for tidier lists, but it needs density data; lines merge only on identical units.
  3. Nutrition and calorie tracking - needs a food database; the pitch asks only for plans and lists.
  4. Pantry inventory (subtract what you already have) - a natural next step, but it adds a second data set to maintain.
  5. Sharing the plan or list with a partner - needs accounts and sync; V1 is one person, one device.
  6. Automatic meal suggestions - the user plans by choice; no recommendation logic.
  7. Photos on recipes - heavy storage for a text-first tool.

10) Success Criteria

  • ✓ Saving Chicken Curry (servings 4, onion 2 piece, rice 300 g) adds a row reading Chicken Curry / 4 servings.
  • ✓ Saving a recipe with an empty name shows Give your recipe a name. and stores nothing.
  • ✓ Assigning the curry at 2 servings to Monday dinner shows Chicken Curry ×2 in that cell.
  • ✓ Assigning a second recipe to the same cell replaces the first.
  • ✓ The week of 2024-03-04 in the worked example produces onion 6 piece, rice 750 g, egg 4 piece.
  • ✓ Ticking rice changes the header from 0 of 3 items to 1 of 3 items.
  • ✓ Lowering the Sunday curry from 8 to 4 servings changes rice to 450 g and 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.

Spec your own app

Describe your idea in a sentence and get a spec like this in about a minute.

Generate my spec

7-day free trial · no card needed

More examples