- Clone import: defer FurnitureData.json writes and append all entries in a single batched write instead of one read-modify-write per item, removing the main serialization bottleneck for large batches. - Clone import: raise SSE batch concurrency cap from 5 to 10 and bump the clone client/route default from 2 to 6. - Add a flush hook to runSseBatch so callers can batch deferred work before batch_complete is emitted, and surface flush errors as an error event. - Enable Next.js Cache Components (instant: false opt-out) and silence the related build warnings in next.config.ts. - Switch isomorphic-dompurify to dompurify and refresh dependencies.
55 lines
1.6 KiB
TypeScript
55 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 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 });
|
|
}
|
|
}
|