Skip to main content
Urbicon UI
Back to Recipes

Meal Planner

Weekly meal plan built on Planner — meals bucketed by day, sorted by meal type, with an add affordance that lives on every day including empty ones.

Live Preview

Week 25 – June 2026
Week 25 – June 2026
Today
Mon 15Tue 16Wed 17Thu 18Fri 19Sat 20Sun 21
Mon 15
Overnight Oats
Lentil Curry
Tue 16
Ramen Bowl
Wed 17
Avocado Toast
Margherita Pizza
Thu 18
Fri 19
Tacos al Pastor
Sat 20
Sun 21
Shakshuka

Key Features

  • Planner buckets a typed MealEntry[] by local day via getDate — no manual date math
  • sort orders meals within a day (breakfast → lunch → dinner)
  • The cell snippet renders your own markup and runs for empty days, so "Add" is always reachable
  • onNavigate hands you the visible week range to load data per week
  • bind:selectedDate tracks the active day; clicking a cell body selects it
  • Server and client agree on the week via the Svelte-free @urbicon-ui/blocks/date subpath

Code

Meal Planner Recipe

<script lang="ts">
  import { Planner, Button, Badge, PlusIcon } from '@urbicon-ui/blocks';
  import { endOfWeek, toIso } from '@urbicon-ui/blocks/date';

  type MealType = 'breakfast' | 'lunch' | 'dinner';
  interface MealEntry { id: string; date: string; mealType: MealType; title: string; emoji: string; }

  const MEAL_ORDER: Record<MealType, number> = { breakfast: 0, lunch: 1, dinner: 2 };

  let entries = $state<MealEntry[]>(initialEntries);
  let referenceDate = $state(new Date());
  let selectedDate = $state<Date | undefined>();

  // Fetch a week's worth of entries whenever navigation moves the window.
  async function loadWeek(start: Date) {
    entries = await db.meals.between(toIso(start), toIso(endOfWeek(start, 1)));
  }

  function addMeal(isoDate: string) {
    entries.push({ id: crypto.randomUUID(), date: isoDate, mealType: 'lunch', title: '', emoji: '🍽️' });
  }
</script>

<Planner
  view="week"
  items={entries}
  getDate={(e) => e.date}
  sort={(a, b) => MEAL_ORDER[a.mealType] - MEAL_ORDER[b.mealType]}
  bind:value={referenceDate}
  bind:selectedDate
  onNavigate={(_, range) => loadWeek(range.start)}
>
  {#snippet cell({ items, isoDate })}
    {#each items as meal (meal.id)}
      <div class="bg-surface-subtle flex items-center gap-2 rounded-md px-2 py-1.5">
        <span aria-hidden="true">{meal.emoji}</span>
        <span class="text-text-secondary truncate text-sm">{meal.title}</span>
      </div>
    {/each}
    <!-- cell runs for empty days too → Add is reachable everywhere -->
    <Button variant="ghost" size="sm" class="mt-auto justify-start" onclick={() => addMeal(isoDate)}>
      <PlusIcon size={14} /> Add
    </Button>
  {/snippet}
</Planner>