From 2f4f0250dfe72d902343debe064abd1e5104a92b Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sat, 9 May 2026 13:07:52 +0800 Subject: [PATCH] feat: add public dashboard API routes for trending, best new, most tested, most reported --- lib/api/app.ts | 2 + lib/api/dashboard-public.ts | 147 ++++++++++++++++++++++++++++++++++++ lib/api/index.ts | 1 + 3 files changed, 150 insertions(+) create mode 100644 lib/api/dashboard-public.ts diff --git a/lib/api/app.ts b/lib/api/app.ts index b94c1db..ecacb18 100644 --- a/lib/api/app.ts +++ b/lib/api/app.ts @@ -32,6 +32,7 @@ import { communitySuggestionRoutes } from "@/lib/api/community-suggestions" import { gamesListingRoutes } from "@/lib/api/games-listing" import { steamgridProxyRoutes } from "@/lib/api/steamgrid-proxy" import { dashboardRoutes } from "@/lib/api/dashboard" +import { dashboardPublicRoutes } from "@/lib/api/dashboard-public" import { savedFilterRoutes } from "@/lib/api/saved-filters" import { cronRoutes } from "@/lib/api/cron" import { profilePhotoRoutes } from "@/lib/api/profile-photo" @@ -122,6 +123,7 @@ export const app = new Elysia({ prefix: "/api" }) .use(cronRoutes) // Dashboard .use(dashboardRoutes) + .use(dashboardPublicRoutes) // Root .get("/", () => ({ name: "DeckyVault API", diff --git a/lib/api/dashboard-public.ts b/lib/api/dashboard-public.ts new file mode 100644 index 0000000..af7d865 --- /dev/null +++ b/lib/api/dashboard-public.ts @@ -0,0 +1,147 @@ +import { Elysia, t } from "elysia" +import { db } from "@/lib/db/index" +import { + games, + gameVersions, + performanceEntries, + gameComments, + reports, +} from "@/lib/db/schema" +import { eq, and, gte, sql, desc, count } from "drizzle-orm" + +const SEVEN_DAYS_AGO = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) +const NINETY_DAYS_AGO = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) + +export const dashboardPublicRoutes = new Elysia({ prefix: "/dashboard" }) + + // ── Trending Games (7-day activity) ──────────────────────────────── + .get( + "/trending", + async () => { + const trending = await db.execute(sql` + WITH recent_benchmarks AS ( + SELECT gv.game_id, COUNT(*) AS cnt + FROM performance_entries pe + JOIN game_versions gv ON pe.version_id = gv.id + WHERE pe.is_removed = false + AND pe.created_at >= ${SEVEN_DAYS_AGO} + GROUP BY gv.game_id + ), + recent_comments AS ( + SELECT gc.game_id, COUNT(*) AS cnt + FROM game_comments gc + WHERE gc.is_removed = false + AND gc.created_at >= ${SEVEN_DAYS_AGO} + GROUP BY gc.game_id + ), + recent_upvotes AS ( + SELECT gv.game_id, SUM(pe.upvotes) AS total_upvotes + FROM performance_entries pe + JOIN game_versions gv ON pe.version_id = gv.id + WHERE pe.is_removed = false + AND pe.updated_at >= ${SEVEN_DAYS_AGO} + GROUP BY gv.game_id + ) + SELECT + g.id, + g.title, + g.capsule_image, + g.header_image, + g.playability_status, + COALESCE(rb.cnt, 0) AS benchmark_count, + COALESCE(rc.cnt, 0) AS comment_count, + COALESCE(ru.total_upvotes, 0) AS upvote_count, + (COALESCE(rb.cnt, 0) * 3 + COALESCE(rc.cnt, 0) * 2 + COALESCE(ru.total_upvotes, 0) * 1) AS activity_score + FROM games g + LEFT JOIN recent_benchmarks rb ON rb.game_id = g.id + LEFT JOIN recent_comments rc ON rc.game_id = g.id + LEFT JOIN recent_upvotes ru ON ru.game_id = g.id + WHERE (rb.cnt IS NOT NULL OR rc.cnt IS NOT NULL OR ru.total_upvotes IS NOT NULL) + ORDER BY activity_score DESC + LIMIT 10 + `) + + return trending.rows + }, + ) + + // ── Best Performing New Releases ───────────────────────────────── + .get( + "/best-new-releases", + async () => { + const results = await db.execute(sql` + SELECT + g.id, + g.title, + g.capsule_image, + g.header_image, + g.release_date, + g.created_at, + g.playability_status, + AVG(pe.fps_avg) AS avg_fps, + COUNT(pe.id) AS benchmark_count + FROM games g + JOIN game_versions gv ON gv.game_id = g.id + JOIN performance_entries pe ON pe.version_id = gv.id + WHERE pe.is_removed = false + AND (g.created_at >= ${new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)} + OR g.release_date IS NOT NULL) + GROUP BY g.id, g.title, g.capsule_image, g.header_image, g.release_date, g.created_at, g.playability_status + HAVING COUNT(pe.id) >= 3 + ORDER BY avg_fps DESC + LIMIT 10 + `) + + return results.rows + }, + ) + + // ── Most Tested Games ───────────────────────────────────────────── + .get( + "/most-tested", + async () => { + const results = await db.execute(sql` + SELECT + g.id, + g.title, + g.capsule_image, + g.header_image, + g.playability_status, + COUNT(pe.id) AS benchmark_count + FROM games g + JOIN game_versions gv ON gv.game_id = g.id + JOIN performance_entries pe ON pe.version_id = gv.id + WHERE pe.is_removed = false + GROUP BY g.id, g.title, g.capsule_image, g.header_image, g.playability_status + ORDER BY benchmark_count DESC + LIMIT 10 + `) + + return results.rows + }, + ) + + // ── Most Reported Games ──────────────────────────────────────────── + .get( + "/most-reported", + async () => { + const results = await db.execute(sql` + SELECT + g.id, + g.title, + g.capsule_image, + g.header_image, + COUNT(DISTINCT r.id) AS report_count + FROM games g + JOIN game_versions gv ON gv.game_id = g.id + JOIN performance_entries pe ON pe.version_id = gv.id + JOIN reports r ON r.entry_id = pe.id + WHERE r.status = 'open' + GROUP BY g.id, g.title, g.capsule_image, g.header_image + ORDER BY report_count DESC + LIMIT 10 + `) + + return results.rows + }, + ) \ No newline at end of file diff --git a/lib/api/index.ts b/lib/api/index.ts index 4db5f6a..13e0561 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -22,5 +22,6 @@ export { steamReviewRoutes } from "./steam-reviews" export { communitySuggestionRoutes } from "./community-suggestions" export { savedFilterRoutes } from "./saved-filters" export { dashboardRoutes } from "./dashboard" +export { dashboardPublicRoutes } from "./dashboard-public" export { cronRoutes } from "./cron" export { profilePhotoRoutes } from "./profile-photo"