Close the web-feasible 100% gaps: REST write/token API, tickets, draw-badge, /me, sanitisation, dusk, radio SSE
Final parity push (web-tier only): - REST API write + token auth: POST /api/tokens (issue a personal_access_token for the session user), Bearer auth via src/lib/api-auth.ts, POST /api/articles/[slug]/comment, GET/DELETE /api/me/tokens, full tickets API (/api/tickets +[id] +[id]/reply), radio current-dj/points/points-leaderboard/ embed-config + POST shouts, and a real-time /api/radio/stream (SSE). 31 public API routes total. - Pages: /draw-badge (buy a custom profile badge → credits + RCON), /me dashboard (stats + online friends + referral claim). Wired into the nav. - HTML sanitisation (sanitize-html) — the HTMLPurifier equivalent — applied to writeable boxes + article bodies before dangerouslySetInnerHTML. - "Dusk" dark theme preset + a default-dark site option honoured by the no-flash boot script. Verified live (prod, amx_test): token issue → Bearer endpoint 200, no-token 401; /api/me/tokens lists it; current-dj/leaderboard JSON; /me + /draw-badge 200; reverted the test user + tokens. tsc 0, vitest 49/49, next build 0.
This commit is contained in:
1 parent
8cedf5614e
commit
f7b3845131
30 files changed
+1734
-10
No files matched your search
@@ -0,0 +1,48 @@
|
||||
import { apiJson } from "@/lib/api";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
// 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 prisma.radioListenerPoints.groupBy({
|
||||
by: ["userId"],
|
||||
_sum: { points: true },
|
||||
});
|
||||
|
||||
const ranked = grouped
|
||||
.map((g) => ({ userId: g.userId, points: g._sum.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 prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true, look: true },
|
||||
});
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user