import { notFound } from "next/navigation"; import { headers } from "next/headers"; import QRCode from "qrcode"; import { auth } from "@/lib/auth/server"; import { db, recipes, eq, and } from "@epicure/db"; import { PrintTrigger } from "@/components/recipe/print-trigger"; import { formatIngredientQuantity } from "@/lib/unit-conversion"; import { getMessages, formatMessage } from "@/lib/i18n/server"; type Params = { params: Promise<{ id: string }> }; export default async function RecipePrintPage({ params }: Params) { const { id } = await params; const session = await auth.api.getSession({ headers: await headers() }); if (!session) return null; const m = getMessages((session.user as { locale?: string }).locale); const unitPref = (session.user as { unitPref?: string }).unitPref === "imperial" ? "imperial" : "metric"; const recipe = await db.query.recipes.findFirst({ where: and(eq(recipes.id, id), eq(recipes.authorId, session.user.id)), with: { ingredients: { orderBy: (t, { asc }) => asc(t.order) }, steps: { orderBy: (t, { asc }) => asc(t.order) }, batchDishes: { orderBy: (t, { asc }) => asc(t.order) }, }, }); if (!recipe) notFound(); // /r/[id] resolves anonymously for "public" and "unlisted" only (see // app/r/[id]/page.tsx) — "private" and "followers" have no link a random // scanner of this QR code could actually open. const shareUrl = recipe.visibility === "public" || recipe.visibility === "unlisted" ? `${process.env["BETTER_AUTH_URL"] ?? "http://localhost:3000"}/r/${id}` : null; const qrDataUrl = shareUrl ? await QRCode.toDataURL(shareUrl, { margin: 1, width: 120 }) : null; const totalMins = (recipe.prepMins ?? 0) + (recipe.cookMins ?? 0); const stepGroups: Array<{ label: string; steps: typeof recipe.steps }> = []; if (recipe.isBatchCook) { for (const step of recipe.steps) { const label = step.appliesTo.length === 0 ? m.recipe.batchCookPrep : step.appliesTo.join(" + "); const last = stepGroups[stepGroups.length - 1]; if (last && last.label === label) last.steps.push(step); else stepGroups.push({ label, steps: [step] }); } } const activeTags = Object.entries(recipe.dietaryTags ?? {}) .filter(([, v]) => v) .map(([k]) => m.recipe.dietary[k as keyof typeof m.recipe.dietary]) .filter(Boolean); return ( <>

{recipe.title}

{qrDataUrl && (
{/* eslint-disable-next-line @next/next/no-img-element -- a data: URL generated server-side, not an optimizable remote image */} {m.recipe.qrCodeAlt} {m.recipe.qrCodeCaption}
)}
{recipe.description && (

{recipe.description}

)}
{recipe.baseServings && {formatMessage(m.recipe.servings, { count: recipe.baseServings })}} {recipe.prepMins && {formatMessage(m.recipe.prep, { mins: recipe.prepMins })}} {recipe.cookMins && {formatMessage(m.recipe.cook, { mins: recipe.cookMins })}} {totalMins > 0 && {formatMessage(m.recipe.total, { mins: totalMins })}} {recipe.difficulty && {recipe.difficulty.charAt(0).toUpperCase() + recipe.difficulty.slice(1)}}
{activeTags.length > 0 && (
{activeTags.map((tag) => ( {tag} ))}
)} {recipe.ingredients.length > 0 && ( <>

{m.recipe.ingredients}

)} {recipe.steps.length > 0 && ( <>

{m.recipe.instructions}

{recipe.isBatchCook ? ( stepGroups.map((group, gi) => (

{group.label}

    {group.steps.map((step) => (
  1. {step.instruction}
  2. ))}
)) ) : (
    {recipe.steps.map((step) => (
  1. {step.instruction} {step.timerSeconds && ( ⏱ {Math.floor(step.timerSeconds / 60)} min )}
  2. ))}
)} )} {recipe.isBatchCook && recipe.batchDishes.length > 0 && ( <>

{m.recipe.batchCookDishesTitle}

{recipe.batchDishes.map((dish) => (

{dish.name}

{formatMessage(m.recipe.batchCookFridgeDays, { days: dish.fridgeDays })} {dish.freezerFriendly && ` · ${m.recipe.batchCookFreezerFriendly}`} {dish.freezerNote && ` — ${dish.freezerNote}`}

{m.recipe.batchCookDayOf}: {dish.dayOfInstructions}

))} )}
); }