import type { Metadata } from "next"; import { headers } from "next/headers"; import Link from "next/link"; import { ChevronLeft, ChevronRight, ShoppingCart, Printer, CalendarDays } from "lucide-react"; import { auth } from "@/lib/auth/server"; import { db, mealPlans, mealPlanMembers, recipes, userNutritionGoals, eq, and, desc } from "@epicure/db"; import { buttonVariants } from "@/components/ui/button"; import { MealPlanner } from "@/components/meal-plan/meal-planner"; import { ShareMealPlanButton } from "@/components/meal-plan/share-meal-plan-button"; import { NewShoppingListButton } from "@/components/meal-plan/new-shopping-list-button"; import { WeeklyNutritionBar } from "@/components/nutrition/weekly-nutrition-bar"; import { cn } from "@/lib/utils"; import { ExportMarkdownButton } from "@/components/shared/export-markdown-button"; import { mealPlanToMarkdown } from "@/lib/markdown/meal-plan"; import { getMessages, formatMessage } from "@/lib/i18n/server"; export const metadata: Metadata = {}; function getMonday(dateStr?: string): Date { // new Date("YYYY-MM-DD") parses as UTC midnight, but getDay() below reads // local time — in negative UTC-offset zones that's still "yesterday", // shifting the resolved Monday back by a full week. Parse the y/m/d parts // directly into local time instead. const d = dateStr ? (() => { const [y, m, day] = dateStr.split("-").map(Number); return new Date(y!, m! - 1, day!); })() : new Date(); const dow = d.getDay(); const diff = (dow === 0 ? -6 : 1 - dow); d.setDate(d.getDate() + diff); d.setHours(0, 0, 0, 0); return d; } function toDateStr(d: Date): string { // Read local y/m/d, not toISOString() (which converts to UTC and can // shift the date by a day depending on the server's timezone offset). const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } function addWeeks(d: Date, n: number): Date { const copy = new Date(d); copy.setDate(copy.getDate() + n * 7); return copy; } export default async function MealPlanPage({ searchParams, }: { searchParams: Promise<{ week?: string }>; }) { const { week } = await searchParams; const session = await auth.api.getSession({ headers: await headers() }); if (!session) return null; const msgs = getMessages((session.user as { locale?: string }).locale); const monday = getMonday(week); const weekStart = toDateStr(monday); const prevWeek = toDateStr(addWeeks(monday, -1)); const nextWeek = toDateStr(addWeeks(monday, 1)); const sunday = addWeeks(monday, 1); sunday.setDate(sunday.getDate() - 1); const [plan, userRecipes, sharedMemberships, nutritionGoals] = await Promise.all([ db.query.mealPlans.findFirst({ where: and(eq(mealPlans.userId, session.user.id), eq(mealPlans.weekStart, weekStart)), with: { entries: { with: { recipe: { with: { photos: true } }, batchDish: true }, }, }, }), db.query.recipes.findMany({ where: eq(recipes.authorId, session.user.id), orderBy: desc(recipes.updatedAt), columns: { id: true, title: true, isBatchCook: true }, with: { batchDishes: { columns: { id: true, name: true }, orderBy: (t, { asc }) => asc(t.order) } }, }), db.query.mealPlanMembers.findMany({ where: eq(mealPlanMembers.userId, session.user.id), with: { mealPlan: { with: { user: true } } }, }), db.query.userNutritionGoals.findFirst({ where: eq(userNutritionGoals.userId, session.user.id), }), ]); const hasNutritionGoals = !!( nutritionGoals && (nutritionGoals.caloriesKcal || nutritionGoals.proteinG || nutritionGoals.carbsG || nutritionGoals.fatG) ); const entries = (plan?.entries ?? []).map((e) => ({ id: e.id, day: e.day, mealType: e.mealType, servings: e.servings, note: e.note, recipe: e.recipe ? { id: e.recipe.id, title: e.recipe.title } : null, batchDish: e.batchDish ? { id: e.batchDish.id, name: e.batchDish.name } : null, })); const label = `${monday.toLocaleDateString("en-US", { month: "short", day: "numeric" })} – ${sunday.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}`; return (

{msgs.mealPlan.title}

{label}

{msgs.mealPlan.shoppingLists} {msgs.common.print} {msgs.mealPlan.exportCalendar}
{sharedMemberships.length > 0 && (

{msgs.mealPlan.sharedWithYou}

{sharedMemberships.map((membership) => (

{formatMessage(msgs.mealPlan.sharedPlanOf, { name: membership.mealPlan.user?.name ?? "?" })}

{formatMessage(msgs.mealPlan.weekOf, { date: membership.mealPlan.weekStart })} · {msgs.shareDialog[membership.role]}

))}
)}
); }