56 lines
1.6 KiB
TypeScript
56 lines
1.6 KiB
TypeScript
import { inArray, sum } from "drizzle-orm";
|
|
import { apiJson } from "@/lib/api";
|
|
import { db, RadioListenerPoints, User } from "@/lib/db";
|
|
|
|
// Radio listener-points leaderboard: the top 20 users by total points, summed
|
|
// across radio_listener_points and joined to users for username/look. Public
|
|
// (no auth) — mirrors the AtomCMS radio leaderboard widget.
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export async function GET(_req: Request) {
|
|
try {
|
|
// Sum points per user. Sort/slice in JS so we stay adapter-agnostic about
|
|
// aggregate ordering, then resolve the top 20 to usernames/looks.
|
|
const grouped = await db
|
|
.select({
|
|
userId: RadioListenerPoints.userId,
|
|
points: sum(RadioListenerPoints.points),
|
|
})
|
|
.from(RadioListenerPoints)
|
|
.groupBy(RadioListenerPoints.userId);
|
|
|
|
const ranked = grouped
|
|
.map((g) => ({
|
|
userId: Number(g.userId),
|
|
points: Number(g.points ?? 0),
|
|
}))
|
|
.sort((a, b) => b.points - a.points)
|
|
.slice(0, 20);
|
|
|
|
if (ranked.length === 0) {
|
|
return apiJson({ data: [] });
|
|
}
|
|
|
|
const userIds = ranked.map((r) => r.userId);
|
|
const users = await db
|
|
.select({ id: User.id, username: User.username, look: User.look })
|
|
.from(User)
|
|
.where(inArray(User.id, userIds));
|
|
const userById = new Map(users.map((u) => [u.id, u]));
|
|
|
|
const data = ranked.map((r) => {
|
|
const u = userById.get(r.userId);
|
|
return {
|
|
username: u?.username ?? null,
|
|
look: u?.look ?? null,
|
|
points: r.points,
|
|
};
|
|
});
|
|
|
|
return apiJson({ data });
|
|
} catch {
|
|
// DB unavailable — serve an empty leaderboard rather than a 500.
|
|
return apiJson({ data: [] }, { status: 200 });
|
|
}
|
|
}
|