import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { headers } from "next/headers"; import Link from "next/link"; import { Clock, Users, Globe, Lock, Link2, Pencil, ChefHat, ExternalLink, Play } from "lucide-react"; import { VariationsButton } from "@/components/recipe/variations-button"; import { TranslateButton } from "@/components/recipe/translate-button"; import { AddToShoppingListButton } from "@/components/recipe/add-to-shopping-list-button"; import { MealPairingButton } from "@/components/recipe/meal-pairing-button"; import { DrinkPairingButton } from "@/components/recipe/drink-pairing-button"; import { AdaptRecipeButton } from "@/components/recipe/adapt-recipe-button"; import { PrintButton } from "@/components/recipe/print-button"; import { VersionHistoryButton } from "@/components/recipe/version-history-button"; import { DeleteRecipeButton } from "@/components/recipe/delete-recipe-button"; import { NutritionPanel } from "@/components/recipe/nutrition-panel"; import { auth } from "@/lib/auth/server"; import { db, recipes, ratings, favorites, avg } from "@epicure/db"; import { and, eq, count } from "@epicure/db"; import { Badge } from "@/components/ui/badge"; import { buttonVariants } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { ServingScaler } from "@/components/recipe/serving-scaler"; import { FavoriteButton } from "@/components/social/favorite-button"; import { RatingStars } from "@/components/social/rating-stars"; import { CommentsSection } from "@/components/social/comments-section"; import { getPublicUrl } from "@/lib/storage"; import { cn } from "@/lib/utils"; type Params = { params: Promise<{ id: string }> }; export async function generateMetadata({ params }: Params): Promise { const { id } = await params; const recipe = await db.query.recipes.findFirst({ where: eq(recipes.id, id) }); return { title: recipe?.title ?? "Recipe" }; } const VISIBILITY_LABEL = { private: "Private", unlisted: "Unlisted", public: "Public" }; const VISIBILITY_ICON = { private: Lock, unlisted: Link2, public: Globe }; const DIFFICULTY_COLOR = { easy: "default", medium: "secondary", hard: "destructive" } as const; const DIETARY_LABELS: Record = { vegan: "Vegan", vegetarian: "Vegetarian", glutenFree: "Gluten-free", dairyFree: "Dairy-free", nutFree: "Nut-free", halal: "Halal", kosher: "Kosher", }; export default async function RecipePage({ params }: Params) { const { id } = await params; const session = await auth.api.getSession({ headers: await headers() }); if (!session) return null; const [recipe, ratingData, favoriteData] = await Promise.all([ 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) }, photos: { orderBy: (t, { asc }) => asc(t.order) }, }, }), db.select({ avgScore: avg(ratings.score), total: count() }).from(ratings).where(eq(ratings.recipeId, id)), db.query.favorites.findFirst({ where: and(eq(favorites.userId, session.user.id), eq(favorites.recipeId, id)) }), ]); if (!recipe) notFound(); const avgScore = ratingData[0]?.avgScore ? parseFloat(ratingData[0].avgScore) : null; const ratingCount = ratingData[0]?.total ?? 0; const isFavorited = !!favoriteData; const cover = recipe.photos.find((p) => p.isCover) ?? recipe.photos[0]; const totalMins = (recipe.prepMins ?? 0) + (recipe.cookMins ?? 0); const activeDietaryTags = Object.entries(recipe.dietaryTags ?? {}) .filter(([, v]) => v) .map(([k]) => DIETARY_LABELS[k]) .filter(Boolean); const VisibilityIcon = VISIBILITY_ICON[recipe.visibility]; return (
{/* Header */}

{recipe.title}

{recipe.visibility === "public" && ( )} {recipe.ingredients.length > 0 && ( ({ rawName: ing.rawName, quantity: ing.quantity, unit: ing.unit, }))} /> )} {recipe.ingredients.length > 0 && ( ({ rawName: ing.rawName }))} /> )} ({ rawName: ing.rawName, quantity: ing.quantity, unit: ing.unit, note: ing.note, order: ing.order, }))} steps={recipe.steps.map((s) => ({ instruction: s.instruction, timerSeconds: s.timerSeconds, order: s.order, }))} /> {recipe.steps.length > 0 && ( Cook )} Edit
{avgScore !== null && (
{avgScore.toFixed(1)} ({ratingCount})
)} {recipe.description && (

{recipe.description}

)}
{recipe.difficulty && ( {recipe.difficulty} )} {recipe.baseServings} servings {recipe.prepMins && ( {recipe.prepMins}m prep )} {recipe.cookMins && ( {recipe.cookMins}m cook )} {totalMins > 0 && recipe.prepMins && recipe.cookMins && ( ({totalMins}m total) )} {VISIBILITY_LABEL[recipe.visibility]}
{activeDietaryTags.length > 0 && (
{activeDietaryTags.map((tag) => ( {tag} ))}
)}
{/* Cover photo */} {cover && (
{recipe.title}
)} {/* Serving scaler + ingredients */} {recipe.ingredients.length > 0 && (

Ingredients

({ id: ing.id, rawName: ing.rawName, quantity: ing.quantity, unit: ing.unit, note: ing.note, order: ing.order, }))} />
)} {recipe.steps.length > 0 && ( <>

Instructions

    {recipe.steps.map((step, i) => (
  1. {i + 1}

    {step.instruction}

    {step.timerSeconds && (

    {step.timerSeconds >= 60 ? `${Math.floor(step.timerSeconds / 60)}m ${step.timerSeconds % 60 > 0 ? `${step.timerSeconds % 60}s` : ""}` : `${step.timerSeconds}s`}

    )}
  2. ))}
)} {recipe.photos.length > 1 && ( <>

Photos

{recipe.photos.map((photo) => (
))}
)} {recipe.visibility !== "private" && ( <>
)}
); }