cardamom
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Viewing readonly version of ft-calendar branch: v13View latest version
Add a calendar view that allows users to plan meals by placing recipes on specific days, then generate shopping lists from selected calendar days. This will be an optional entry point into the shopping list creation process.
Create new tables for meal planning:
meal_plans_v1
table:id
(INTEGER PRIMARY KEY)recipe_id
(INTEGER FK to recipes_v1)date
(DATE)meal_type
(TEXT: 'breakfast', 'lunch', 'dinner', 'snack')notes
(TEXT)created_at
(DATETIME)updated_at
(DATETIME)- Note: NO unique constraint - allows multiple recipes per meal type per day
New routes in /backend/routes/meal-plans.ts
:
GET /api/meal-plans
- Get meal plans for date range- Query params:
startDate
,endDate
- Returns array of meal plans with populated recipe data
- Handles deleted recipes: Returns meal plan with
recipe: null
when recipe is deleted
- Query params:
POST /api/meal-plans
- Add recipe to calendar- Body:
{ recipeId, date, mealType, notes? }
- Allows multiple recipes for same meal type
- Transaction safety: Validates recipe exists before creating meal plan
- Body:
DELETE /api/meal-plans/:id
- Remove specific recipe from calendar- Transaction safety: Single atomic operation
PUT /api/meal-plans/:id
- Update meal plan (change date/meal type)- Transaction safety: Validates new recipe exists if changing recipeId
POST /api/meal-plans/shopping-list
- Create shopping list from date range- Body:
{ startDate, endDate }
or{ dates: [...] }
- Collects all recipes from selected dates
- Transaction safety: Uses atomic batch operations for entire shopping list creation
- Handles deleted recipes: Skips meal plans with deleted recipes
- Body:
-
MealPlanCalendar.tsx
- Main calendar view component- Month view with week rows
- Each day shows meal slots with recipe count badges
- Support multiple recipes per meal type
- Click to view/edit day's meals
- Mobile-responsive grid layout
-
DayMealPlan.tsx
- Single day's meal plan- Shows all recipes for each meal type (as a list)
- Add/remove individual recipes
- Quick view of total ingredients needed
- Collapsible sections for each meal type
-
MealPlanSidebar.tsx
- Recipe selector sidebar- Searchable list of recipes
- Click to add to selected day/meal
- Filter by tags, prep time, etc.
- Mobile: slides in as overlay
App.tsx
- Add 'meal-calendar' view type and navigationShoppingListCreator.tsx
- Add option to create from calendar selection
┌─────────────────────────────────────────────────┐
│ < November 2025 > [Create List] [⚙️] │
├─────────────────────────────────────────────────┤
│ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤ │
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ [R] │
│ B:2 │ B:- │ B:1 │ B:3 │ B:- │ B:- │ B:1 │ [e] │
│ L:1 │ L:2 │ L:- │ L:2 │ L:1 │ L:- │ L:3 │ [c] │
│ D:3 │ D:1 │ D:2 │ D:- │ D:2 │ D:1 │ D:2 │ [i] │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┤ [p] │
│ 8 │ 9 │ ... │ │ [e] │
│ │ │ │ │ [s] │
└─────┴─────┴─────┴─────────────────────────┴───────┘
Legend: B=Breakfast, L=Lunch, D=Dinner
Numbers = Recipe count, - = No recipes
- Stack days vertically
- Swipeable week view
- Collapsible meal sections
- Bottom sheet for recipe selection
- Floating action button for quick add
- Create database migration for
meal_plans_v1
table - Add database queries in
/backend/database/queries.ts
- Support batch operations for multiple recipes
- Use LEFT JOIN for recipe data to handle deleted recipes gracefully
- Implement transaction safety using
sqlite.batch()
for all multi-table operations - Add validation queries to check recipe existence before meal plan operations
- Create
/backend/routes/meal-plans.ts
with CRUD operations- Wrap all create/update operations in transactions
- Return appropriate error messages when recipes don't exist
- Handle orphaned meal plans (where recipe was deleted) in GET operations
- Add route to main backend index
- Create
MealPlanCalendar.tsx
with basic month view - Implement day selection and navigation
- Add meal slot display with recipe counts
- Mobile-responsive CSS Grid layout
- Create
MealPlanSidebar.tsx
for recipe selection - Implement click-to-add functionality
- Add
DayMealPlan.tsx
for detailed day view - Support adding multiple recipes to same meal
- Add calendar selection mode (multi-select days)
- Create "Generate Shopping List from Calendar" flow
- Use atomic transaction for entire shopping list creation
- Filter out meal plans with deleted recipes
- Show warning if some recipes were skipped due to deletion
- Integrate with existing
ShoppingListCreator
- Reuse existing transaction patterns from recipe-based shopping lists
- Show which recipes come from which days
- Handle duplicate recipes across days
- Use CSS Grid with
grid-template-columns: repeat(auto-fit, minmax(...))
- Implement touch gestures for mobile navigation
- Create responsive breakpoints (mobile < 768px, tablet < 1024px)
- Bottom sheet pattern for recipe selection on mobile
- Ensure all interactions work with touch (no hover-only features)
export interface MealPlan {
id?: number;
recipeId: number;
recipe?: Recipe | null; // Populated on fetch, null if recipe was deleted
date: string; // YYYY-MM-DD
mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack';
notes?: string;
createdAt?: string;
updatedAt?: string;
}
export interface CalendarDay {
date: string;
meals: {
breakfast: MealPlan[]; // Array to support multiple recipes
lunch: MealPlan[];
dinner: MealPlan[];
snack: MealPlan[];
};
isSelected?: boolean;
totalRecipeCount: number;
}
export interface MealPlanDateRange {
startDate: string;
endDate: string;
}
- Main nav: Add "Meal Calendar" option
- From Calendar: "Create Shopping List" → Select days → Generate list
- From Shopping List Creator: Tab for "From Calendar" vs "From Recipes"
- Quick actions: "Plan this recipe" button on recipe view
- Touch targets: Minimum 44x44px for all interactive elements
- Swipe navigation: Between months and weeks
- Responsive grid: Days stack on small screens
- Progressive disclosure: Expand/collapse meal details
- Offline support: Cache meal plans locally
- Performance: Lazy load recipes, virtualize long lists
All multi-table operations will use Val Town's sqlite.batch()
method:
// Example: Creating meal plan with validation
const queries = [
// Verify recipe exists
{ sql: 'SELECT id FROM recipes_v1 WHERE id = ?', args: [recipeId] },
// Insert meal plan if recipe exists
{ sql: 'INSERT INTO meal_plans_v1 (recipe_id, date, meal_type) VALUES (?, ?, ?)',
args: [recipeId, date, mealType] }
];
const results = await sqlite.batch(queries);
if (!results[0].rows.length) {
throw new Error('Recipe not found');
}
- Meal Plan Queries: Use LEFT JOIN to include orphaned meal plans:
SELECT mp.*, r.* FROM meal_plans_v1 mp LEFT JOIN recipes_v1 r ON mp.recipe_id = r.id WHERE mp.date BETWEEN ? AND ?
- Frontend Display: Show placeholder for deleted recipes with option to remove
- Shopping List Generation: Filter out null recipes before ingredient aggregation
- Cascade Behavior: Keep meal plans when recipes are deleted (no CASCADE DELETE)
- Visual meal planning interface
- Support for meal prep (multiple recipes per meal)
- Efficient shopping for multiple days
- Flexibility for varying meal sizes/occasions
- Mobile-friendly for grocery shopping
- Data integrity maintained even when recipes are deleted
- Atomic operations ensure consistent state