Project/Bite/Week 3 - Meal Planning & Polish

Week 3 - Meal Planning & Polish

2/10/2025

4 min read


The Pillar

Before building anything this week I went and read what real people complain about in meal planning apps. Spent time on r/MealPrepSunday and the pattern was obvious — people hate being locked into an app's recipe database, they hate paying $8/month for a grocery list, and they just want their own recipes on a calendar. Bite already has the "your own recipes" part. So I built the calendar.

Meal Planning Calendar

Weekly view with breakfast, lunch, dinner, and snack slots for each day. Your recipes show up in a sidebar and you drag them onto whatever slot you want.

Used @dnd-kit for drag-and-drop because FullCalendar felt like overkill. @dnd-kit gives you the pieces and you put them together however you want.

For the calendar itself I just used date-fns and Tailwind. No calendar library. The UI is specific enough that any off-the-shelf option would've been more work to customize than building from scratch. Two new tables in the database (meal_plans and meal_plan_recipes) and Drizzle migrations picked them up without touching existing tables.

Meal Planning

Drag recipes onto any day of the week

Weekly grid with breakfast, lunch, dinner, and snack slots. Built with dnd-kit and date-fns — no calendar library needed.

biterecipes.app/dashboard/meal-plan
Meal planning calendar

Recipe Import Enhancements

The URL importer now pulls in servings, yield, cook notes, and macros — calories, fat, carbs, protein. Both per-serving and total where available. The schema.org data usually has this, I just wasn't grabbing it before.

Recipe Import

Enhanced recipe imports with macros

Pulls in servings, yield, cook notes, and macros — calories, fat, carbs, protein. Both per-serving and total where available.

biterecipes.app/dashboard/recipes/import
Import recipe

The nutrition field on a schema.org Recipe looks like this:

JSON
{
  "nutrition": {
    "@type": "NutritionInformation",
    "calories": "320 calories",
    "fatContent": "12g",
    "carbohydrateContent": "45g",
    "proteinContent": "8g"
  }
}

Pulling it out is just a few extra lines on top of the existing parser:

JavaScript
const nutrition = schema.nutrition ?? {};
 
const macros = {
  calories: parseInt(nutrition.calories) || null,
  fat: parseFloat(nutrition.fatContent) || null,
  carbs: parseFloat(nutrition.carbohydrateContent) || null,
  protein: parseFloat(nutrition.proteinContent) || null,
};

Already there in the data — just needed to reach for it.

Also tightened up the image proxy. When you're fetching arbitrary URLs from user input, you want to be careful.

Technical Decisions

@dnd-kit over alternatives: react-beautiful-dnd is deprecated. FullCalendar is too heavy for what I need. @dnd-kit gives you primitives — sensors, collision detection, sortable contexts — and lets you compose them.

Custom calendar over a library: date-fns + Tailwind. The meal plan layout is specific enough that a generic calendar library would need more customization than building it myself.

View modes via URL params: Calendar and list view are switched through search params, not local state. The server component reads the param directly and renders the right view — no useEffect, no hydration mismatch.

tsx
// app/dashboard/meal-plan/page.tsx
export default function MealPlanPage({ searchParams }: { searchParams: { view?: string } }) {
  const view = searchParams.view === "list" ? "list" : "calendar";
 
  return view === "calendar" ? <CalendarView /> : <ListView />;
}

The URL stays the source of truth — shareable, bookmarkable, and one less piece of client state to manage.

Thoughts

Having the recipes in a sidebar, dragging them onto a calendar, with no friction — that's what the Reddit users were asking for. The Reddit research was worth it. Real users telling you what they want is way better than guessing.

Week 4 is grocery lists — the reason you meal planning is required in the first place.