import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { headers } from "next/headers"; import Link from "next/link"; import { Globe } from "lucide-react"; import { auth } from "@/lib/auth/server"; import { db, collections, eq, and, ne } from "@epicure/db"; import { isFollowing } from "@/lib/social-follows"; import { RecipeGridCard } from "@/components/recipe/recipe-grid-card"; import { buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { getMessages } from "@/lib/i18n/server"; type Params = { params: Promise<{ id: string }> }; export async function generateMetadata({ params }: Params): Promise { const { id } = await params; const col = await db.query.collections.findFirst({ where: (c, { and, eq }) => and(eq(c.id, id), eq(c.visibility, "public")), }); if (!col) return {}; return { title: col.name, description: col.description ?? undefined, openGraph: { title: col.name, description: col.description ?? undefined, type: "article" }, }; } export default async function PublicCollectionPage({ params }: Params) { const { id } = await params; const session = await auth.api.getSession({ headers: await headers() }).catch(() => null); const m = getMessages((session?.user as { locale?: string } | undefined)?.locale); const col = await db.query.collections.findFirst({ where: and(eq(collections.id, id), ne(collections.visibility, "private")), with: { user: true, recipes: { orderBy: (t, { asc }) => asc(t.position), with: { recipe: { with: { photos: true } } }, }, }, }); if (!col) notFound(); // Same rationale as /r/[id]: this route has no session-based access // control otherwise, so a "followers"-only collection needs an explicit // gate — the owner, or a signed-in follower of the owner. if (col.visibility === "followers") { const viewerId = session?.user?.id; const allowed = !!viewerId && (viewerId === col.userId || await isFollowing(viewerId, col.userId)); if (!allowed) notFound(); } const viewerId = session?.user?.id; const recipeEntries = await Promise.all( col.recipes.map(async (r) => { const recipe = r.recipe; if (!recipe) return null; if (recipe.visibility === "public" || recipe.visibility === "unlisted") return recipe; if (recipe.visibility === "followers" && viewerId) { if (viewerId === recipe.authorId || await isFollowing(viewerId, recipe.authorId)) return recipe; } // Owner's own private recipes (allowed in their own collection) aren't // shown to anyone else here — this route has no owner-only branch. return null; }) ); const visibleRecipes = recipeEntries.filter((r): r is NonNullable => r !== null); return (
{m.publicCollection.publicCollectionBy} {col.user.name} {session && (
{m.publicCollection.viewInLibrary}
)} {!session && (
{m.publicRecipe.signUpFree} {m.publicRecipe.logIn}
)}

{col.name}

{col.description &&

{col.description}

}

{visibleRecipes.length} recipe{visibleRecipes.length !== 1 ? "s" : ""}

{visibleRecipes.length > 0 && (
{visibleRecipes.map((recipe) => ( ))}
)} {!session && (

{m.publicCollection.wantToSave}

{m.publicCollection.wantToSaveDescription}

{m.publicRecipe.getStartedFree}
)}
); }