A mobile-first dinner planning, shopping list, and recurring tasks app for families. Built with Next.js 14, deployed on Vercel.
- Create and manage dinner recipes with ingredients
- Free-text units (stk, kg, dl, etc.)
- Auto-complete suggestions based on history
- Custom emoji icons for each dinner
- Notes field for cooking instructions
- 7-day grid view (Monday–Sunday)
- Drag-free tap-to-assign interface
- Todos integration: Each day shows dinner + todos
- Copy entire week to the next
- Historical snapshots preserved even if dinners are deleted
- Recurring templates: Define tasks that repeat weekly or bi-weekly
- Ad-hoc todos: Add one-time tasks directly to any day
- Responsible assignment: Magnus, Nansy, or Begge (Both)
- Time scheduling: Optional HH:MM time for each task
- Hidden/Restore: "Tøm liste" hides items, restorable later
- Clipboard export: ISO format for iOS Shortcuts integration
- Auto-populate from week plan ingredients
- Manual add with smart suggestions
- Conservative merge: Only items with matching name AND unit are combined
- Hide/Restore: Completed items move to a collapsible "hidden" section
- Session reset: "Tøm og send uke" clears hidden memory for a fresh start
- Clipboard export: Copies as
* {qty} {unit} {name}for Apple Reminders
Both Todos and Shopping List support deep linking to iOS Shortcuts:
- Todos shortcut: Must be named exactly
"Gjøremål til Påminnelser" - Shopping List shortcut: Must be named exactly
"Middager til Påminnelser"
How it works:
- Tap "Kopier liste" to copy items to clipboard
- Tap "Åpne Påminnelser" / "Send til Påminnelser" to launch the shortcut
- The shortcut should split clipboard by newline and create reminders
Clipboard format (Todos):
2026-01-07 14:30 | Handle mat | Magnus
2026-01-08 | Rydde garasjen | Begge (ferdig)
- Theme: Light, Dark, or System
- Logout: End session and redirect to login
- Export JSON: Full backup (schemaVersion 3)
- Import JSON: Validated restore with UUID checks and content validation
- Delete all dinners: Preserves week plan snapshots
- Reset app: 2-step confirmation, wipes all data
| Layer | Technology |
|---|---|
| Framework | Next.js 14 (App Router) |
| Language | TypeScript |
| Database | Vercel Postgres |
| ORM | Drizzle |
| Styling | Tailwind CSS |
| Data Fetching | SWR (revalidate on focus, no polling) |
- Node.js 18+
- Vercel account (for Postgres)
# Clone
git clone https://github.com/your-repo/ukesplan.git
cd ukesplan
# Install dependencies
npm install
# Set up environment
cp .env.local.example .env.local
# Edit .env.local with your Vercel Postgres credentials and ACCESS_SECRET
# Push schema to database
npx drizzle-kit push
# Run dev server
npm run dev| Variable | Description |
|---|---|
POSTGRES_URL |
Vercel Postgres connection string |
POSTGRES_URL_NON_POOLING |
Direct connection for migrations |
ACCESS_SECRET |
Shared password for the access gate |
├── app/
│ ├── api/ # API routes
│ │ ├── auth/ # Login/logout
│ │ ├── data/ # Export, import, reset
│ │ ├── dinners/ # CRUD for dinners
│ │ ├── events/ # Event log API
│ │ ├── shopping-list/# Shopping list operations
│ │ ├── todo-templates/# Recurring todo templates
│ │ ├── todos/ # Todo CRUD + clear-week
│ │ └── week-plans/ # Week plan operations
│ ├── gjoremal/ # Todos page + recurring templates
│ ├── handleliste/ # Shopping list page
│ ├── innstillinger/ # Settings page
│ ├── middager/ # Dinner list + detail pages
│ ├── ukeplan/ # Week planner page
│ └── login/ # Login page
├── components/
│ ├── dinners/ # Dinner-related UI
│ ├── layout/ # Navigation, headers, MoreMenu
│ ├── shopping-list/ # Shopping list UI
│ ├── todos/ # Todos UI (list, actions, templates)
│ ├── ui/ # Shared components (Button, Modal, Toast)
│ └── week-plan/ # Week planner UI (DayCell, DayTodosBlock)
├── lib/
│ ├── db/ # Drizzle client, schema, migrations
│ ├── domain/ # Business logic (dinners, shopping, weekPlans, todos)
│ └── utils/ # Helpers (date, toast)
└── middleware.ts # Auth gate
All API routes are protected by the auth middleware and return JSON. Unauthorized requests receive 401 { error: 'unauthorized' }.
GET /api/dinners— List all dinnersPOST /api/dinners— Create dinnerGET /api/dinners/[id]— Get dinner by IDPUT /api/dinners/[id]— Update dinnerDELETE /api/dinners/[id]— Delete dinnerDELETE /api/dinners— Delete all dinners
GET /api/week-plans?year=&week=— Get or create week plan (includes todos)POST /api/week-plans/assign— Assign dinner to dayPOST /api/week-plans/clear-day— Clear day assignmentPOST /api/week-plans/copy— Copy week to next
GET /api/todos?year=&week=&includeHidden=— Get todos for a weekPOST /api/todos— Create ad-hoc todoPATCH /api/todos/[id]— Update todo (completion, hidden, etc.)DELETE /api/todos/[id]— Delete todoPOST /api/todos/clear-week— Hide all todos for a week
GET /api/todo-templates— List all recurring templatesPOST /api/todo-templates— Create templatePATCH /api/todo-templates/[id]— Update templateDELETE /api/todo-templates/[id]— Delete template
GET /api/shopping-list— Get active and hidden itemsPOST /api/shopping-list/add— Add items manuallyPATCH /api/shopping-list/[id]— Update itemPATCH /api/shopping-list/[id]/hide— Hide/restore itemDELETE /api/shopping-list— Clear sessionPOST /api/shopping-list/clear-send— Atomic clear + send weekPOST /api/shopping-list/send-day— Send day ingredientsPOST /api/shopping-list/send-week— Send week ingredients
GET /api/data/export— Download JSON backup (schemaVersion 3)POST /api/data/import— Restore from JSON (validates content)POST /api/data/reset— Wipe all data
GET /api/events?limit=&offset=&type=&from=&to=— Paginated event log
- Truthful UI: No fake data, no placeholders, clear error states
- Conservative merge: Name + Unit must match for quantity merge
- Hidden exclusion: Hidden items stay hidden within a session
- Session reset: Clear deletes ALL items (fresh start)
- No polling: SWR revalidates on focus only
- Snapshot preservation: Week plans store
dinnerNameSnapshotanddinnerIconSnapshot - JSON 401: All unauthorized API requests return JSON, never redirect
- Dinner name: max 120 chars
- Ingredient name: max 120 chars
- Unit: max 20 chars
- Notes: max 1000 chars
- Todo title: max 120 chars
- Time format: HH:MM (24-hour)
- Responsible:
he,she, orboth - intervalWeeks: 1–52
- Cookie-based auth with
httpOnly,Secure, andsameSite: strictflags - Timing-safe password comparison (
crypto.timingSafeEqual) - 1-second delay on failed login attempts (brute force mitigation)
- JSON 401 responses for API routes (no redirects)
- Transactional import with full validation before commit
dinners— Dinner recipes with name, notes, iconingredients— Linked to dinners with quantity and unitweek_plans— Year/week indexed plansweek_plan_days— 7 days per plan with dinner snapshotstodo_templates— Recurring task definitionstodos— Task occurrences with hidden flag, unique on (template_id, week_plan_day_id)shopping_list_items— Items with normalized key for mergingevent_log— Audit trail for all mutations
Located in lib/db/migrations/:
0000_right_giant_man.sql— Initial schema0001_add_todos.sql— Todos and templates tables0002_add_hidden_to_todos.sql— Hidden column for soft clear
The app logs all major user actions to an event_log table:
dinner_created,dinner_updated,dinner_deleteddinner_assigned,day_clearedshopping_items_added,shopping_item_removed,shopping_item_restoredshopping_list_cleared,shopping_list_reset_with_weekweek_copiedtodo_template_created,todo_template_updated,todo_template_deletedtodo_created,todo_completed,todo_uncompleted,todo_deletedtodo_hidden,todo_restored,week_todos_clearedrecurring_todo_generateddata_imported
Hidden .ai-suggestion placeholders in DayCell provide context for future AI features.
Private project. All rights reserved.