feat: add trending/leaderboard for collections

New collection_favorites table lets users star public collections.
/collections/explore mirrors the recipe explore page: trending (star
count in last 7 days) and recently-added public collections. Linked
from the "Discover" button on the collections page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-09 15:33:40 +02:00
parent cd444d4d23
commit b4b964aafb
11 changed files with 4618 additions and 2 deletions
@@ -0,0 +1,113 @@
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.isPublic, true))
.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.isPublic, true))
.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} />;
}