9da57dd1d0
Replaces collections.isPublic (boolean) with collections.visibility
(private/unlisted/public/followers — same enum recipes use). Two-step
migration (0050 adds+backfills, 0051 drops isPublic) since drizzle-kit's
add+drop-in-one-diff rename heuristic needs an interactive prompt we
can't satisfy here.
New collectionVisibleToViewer(viewerId) in lib/visibility.ts mirrors the
existing recipe helper (author always sees own; public/unlisted visible
to anyone; followers-only via the same user_follows EXISTS pattern) —
used by the collection detail page, its print view, fork, and favorite,
replacing their old `or(isPublic, own)` checks.
Create/edit collection dialogs get the same 4-option visibility select
as the recipe form instead of a public/private checkbox.
Collection PDF export now generates a QR code (qrcode, same as the
recipe PDF) linking to /collections/{id}, shown only when visibility is
public/unlisted — same "would an anonymous scanner actually resolve
this" rule as the recipe QR.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { headers } from "next/headers";
|
|
import { auth } from "@/lib/auth/server";
|
|
import {
|
|
db,
|
|
collections,
|
|
users,
|
|
collectionFavorites,
|
|
collectionRecipes,
|
|
eq,
|
|
desc,
|
|
sql,
|
|
and,
|
|
gte,
|
|
count,
|
|
inArray,
|
|
} from "@epicure/db";
|
|
import { ExploreCollectionsContent } from "@/components/collections/explore-collections-content";
|
|
|
|
export const metadata: Metadata = {};
|
|
|
|
export type CollectionResult = {
|
|
id: string;
|
|
name: string;
|
|
description: string | null;
|
|
authorName: string | null;
|
|
recipeCount: number;
|
|
starCount: number;
|
|
starred: boolean;
|
|
};
|
|
|
|
export default async function ExploreCollectionsPage() {
|
|
const session = await auth.api.getSession({ headers: await headers() });
|
|
if (!session) return null;
|
|
|
|
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
|
|
const trendingRows = await db
|
|
.select({
|
|
id: collections.id,
|
|
name: collections.name,
|
|
description: collections.description,
|
|
authorName: users.name,
|
|
recentStars: count(collectionFavorites.collectionId),
|
|
})
|
|
.from(collections)
|
|
.innerJoin(users, eq(collections.userId, users.id))
|
|
.leftJoin(
|
|
collectionFavorites,
|
|
and(eq(collectionFavorites.collectionId, collections.id), gte(collectionFavorites.createdAt, sevenDaysAgo))
|
|
)
|
|
.where(eq(collections.visibility, "public"))
|
|
.groupBy(collections.id, users.id)
|
|
.orderBy(desc(sql`count(${collectionFavorites.collectionId})`))
|
|
.limit(12);
|
|
|
|
const recentRows = await db
|
|
.select({
|
|
id: collections.id,
|
|
name: collections.name,
|
|
description: collections.description,
|
|
authorName: users.name,
|
|
})
|
|
.from(collections)
|
|
.innerJoin(users, eq(collections.userId, users.id))
|
|
.where(eq(collections.visibility, "public"))
|
|
.orderBy(desc(collections.createdAt))
|
|
.limit(12);
|
|
|
|
const allIds = [...new Set([...trendingRows.map((r) => r.id), ...recentRows.map((r) => r.id)])];
|
|
|
|
const [recipeCounts, totalStars, myStars] = await Promise.all([
|
|
allIds.length > 0
|
|
? db.select({ collectionId: collectionRecipes.collectionId, n: count() })
|
|
.from(collectionRecipes)
|
|
.where(inArray(collectionRecipes.collectionId, allIds))
|
|
.groupBy(collectionRecipes.collectionId)
|
|
: [],
|
|
allIds.length > 0
|
|
? db.select({ collectionId: collectionFavorites.collectionId, n: count() })
|
|
.from(collectionFavorites)
|
|
.where(inArray(collectionFavorites.collectionId, allIds))
|
|
.groupBy(collectionFavorites.collectionId)
|
|
: [],
|
|
allIds.length > 0
|
|
? db.query.collectionFavorites.findMany({
|
|
where: and(eq(collectionFavorites.userId, session.user.id), inArray(collectionFavorites.collectionId, allIds)),
|
|
columns: { collectionId: true },
|
|
})
|
|
: [],
|
|
]);
|
|
|
|
const recipeCountMap = new Map(recipeCounts.map((r) => [r.collectionId, r.n]));
|
|
const starCountMap = new Map(totalStars.map((r) => [r.collectionId, r.n]));
|
|
const starredSet = new Set(myStars.map((s) => s.collectionId));
|
|
|
|
function toResult(row: { id: string; name: string; description: string | null; authorName: string | null }): CollectionResult {
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
description: row.description,
|
|
authorName: row.authorName,
|
|
recipeCount: recipeCountMap.get(row.id) ?? 0,
|
|
starCount: starCountMap.get(row.id) ?? 0,
|
|
starred: starredSet.has(row.id),
|
|
};
|
|
}
|
|
|
|
const trending = trendingRows.map(toResult);
|
|
const recent = recentRows.map(toResult);
|
|
|
|
return <ExploreCollectionsContent trending={trending} recent={recent} />;
|
|
}
|