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:
@@ -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} />;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { db, collections, collectionFavorites, eq, and, or } from "@epicure/db";
|
||||
import { requireSession } from "@/lib/api-auth";
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function POST(_req: NextRequest, { params }: Params) {
|
||||
const { session, response } = await requireSession();
|
||||
if (response) return response;
|
||||
const { id } = await params;
|
||||
|
||||
const collection = await db.query.collections.findFirst({
|
||||
where: and(eq(collections.id, id), or(eq(collections.isPublic, true), eq(collections.userId, session!.user.id))),
|
||||
columns: { id: true },
|
||||
});
|
||||
if (!collection) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
await db.insert(collectionFavorites)
|
||||
.values({ userId: session!.user.id, collectionId: id })
|
||||
.onConflictDoNothing();
|
||||
return NextResponse.json({ favorited: true });
|
||||
}
|
||||
|
||||
export async function DELETE(_req: NextRequest, { params }: Params) {
|
||||
const { session, response } = await requireSession();
|
||||
if (response) return response;
|
||||
const { id } = await params;
|
||||
|
||||
await db.delete(collectionFavorites)
|
||||
.where(and(eq(collectionFavorites.userId, session!.user.id), eq(collectionFavorites.collectionId, id)));
|
||||
return NextResponse.json({ favorited: false });
|
||||
}
|
||||
Reference in New Issue
Block a user