feat: add stats fetching, preset joins, and sync=1 param to game page

This commit is contained in:
2026-04-26 15:49:00 +08:00
parent 64cf13636c
commit 016d585e48
+74 -8
View File
@@ -8,6 +8,7 @@ import {
communityPresets, communityPresets,
gameComments, gameComments,
gamePlatformSupport, gamePlatformSupport,
hardware,
} from "@/lib/db/schema" } from "@/lib/db/schema"
import { eq, sql } from "drizzle-orm" import { eq, sql } from "drizzle-orm"
import { isSyncStale, syncSteamGame } from "@/lib/steam/sync" import { isSyncStale, syncSteamGame } from "@/lib/steam/sync"
@@ -38,7 +39,9 @@ async function createGameStub(steamAppId: number) {
if (res.ok) { if (res.ok) {
const data = (await res.json()) as Record< const data = (await res.json()) as Record<
string, string,
{ success: boolean; data: { {
success: boolean
data: {
type?: string type?: string
name: string name: string
developers?: string[] developers?: string[]
@@ -46,11 +49,11 @@ async function createGameStub(steamAppId: number) {
genres?: { description: string }[] genres?: { description: string }[]
header_image?: string header_image?: string
short_description?: string short_description?: string
} } }
}
> >
const entry = data[String(steamAppId)] const entry = data[String(steamAppId)]
if (entry?.success && entry.data) { if (entry?.success && entry.data) {
// Reject non-games (DLCs, soundtracks, demos, etc.)
if (entry.data.type && entry.data.type !== "game") { if (entry.data.type && entry.data.type !== "game") {
notFound() notFound()
} }
@@ -87,11 +90,15 @@ async function createGameStub(steamAppId: number) {
export default async function GamePage({ export default async function GamePage({
params, params,
searchParams,
}: { }: {
params: Promise<{ id: string }> params: Promise<{ id: string }>
searchParams: Promise<{ sync?: string }>
}) { }) {
const { id } = await params const { id } = await params
const { sync } = await searchParams
const isNumeric = /^\d+$/.test(id) const isNumeric = /^\d+$/.test(id)
const forceSync = sync === "1"
// ── Resolve game ──────────────────────────────────────────────── // ── Resolve game ────────────────────────────────────────────────
let game let game
@@ -111,7 +118,6 @@ export default async function GamePage({
game = rows[0] game = rows[0]
} }
// Auto-create stub for missing Steam games
if (!game && isNumeric) { if (!game && isNumeric) {
try { try {
game = await createGameStub(Number(id)) game = await createGameStub(Number(id))
@@ -124,8 +130,8 @@ export default async function GamePage({
notFound() notFound()
} }
// ── Fetch related counts ──────────────────────────────────────── // ── Fetch related data in parallel ──────────────────────────────
const [benchmarkCount, presetCount, commentCount, platformSupport] = const [benchmarkCount, presetCount, commentCount, platformSupport, presetRows] =
await Promise.all([ await Promise.all([
db db
.select({ count: sql<number>`count(*)::int` }) .select({ count: sql<number>`count(*)::int` })
@@ -151,10 +157,44 @@ export default async function GamePage({
.from(gamePlatformSupport) .from(gamePlatformSupport)
.where(eq(gamePlatformSupport.gameId, game.id)) .where(eq(gamePlatformSupport.gameId, game.id))
.then((r) => r), .then((r) => r),
db
.select({
id: communityPresets.id,
name: communityPresets.name,
description: communityPresets.description,
hardwareSlug: communityPresets.hardwareSlug,
hardwareName: hardware.name,
upvotes: communityPresets.upvotes,
settingsJson: communityPresets.settingsJson,
createdAt: communityPresets.createdAt,
fpsAvg: performanceEntries.fpsAvg,
fpsLow: performanceEntries.fpsLow,
fpsHigh: performanceEntries.fpsHigh,
fsrVersion: performanceEntries.fsrVersion,
frameGenMethod: performanceEntries.frameGenMethod,
protonVersion: performanceEntries.protonVersion,
osVersion: performanceEntries.osVersion,
})
.from(communityPresets)
.leftJoin(
performanceEntries,
eq(communityPresets.performanceEntryId, performanceEntries.id),
)
.innerJoin(
hardware,
eq(communityPresets.hardwareSlug, hardware.slug),
)
.where(eq(communityPresets.gameId, game.id))
.orderBy(sql`${communityPresets.upvotes} DESC`),
]) ])
// ── Stale-While-Revalidate: schedule background sync ──────────── // ── Sync logic: force or stale-while-revalidate ─────────────────
if (game.source === "steam" && game.steamAppId && isSyncStale(game.lastSync)) { const shouldSync =
game.source === "steam" &&
game.steamAppId &&
(forceSync || isSyncStale(game.lastSync))
if (shouldSync) {
after(async () => { after(async () => {
await syncSteamGame(game.steamAppId!) await syncSteamGame(game.steamAppId!)
}) })
@@ -178,6 +218,30 @@ export default async function GamePage({
createdAt: game.createdAt.toISOString(), createdAt: game.createdAt.toISOString(),
} }
const serializedPresets = presetRows.map((p) => ({
id: p.id,
name: p.name,
description: p.description,
hardwareSlug: p.hardwareSlug,
hardwareName: p.hardwareName,
upvotes: p.upvotes,
settingsCount: Array.isArray(p.settingsJson)
? p.settingsJson.reduce(
(sum: number, cat: { settings: unknown[] }) =>
sum + cat.settings.length,
0,
)
: 0,
fpsAvg: p.fpsAvg,
fpsLow: p.fpsLow,
fpsHigh: p.fpsHigh,
fsrVersion: p.fsrVersion,
frameGenMethod: p.frameGenMethod,
protonVersion: p.protonVersion,
osVersion: p.osVersion,
createdAt: p.createdAt.toISOString(),
}))
return ( return (
<GamePageClient <GamePageClient
game={serializedGame} game={serializedGame}
@@ -187,6 +251,8 @@ export default async function GamePage({
comments: commentCount, comments: commentCount,
}} }}
platformSupport={platformSupport} platformSupport={platformSupport}
presets={serializedPresets}
gameId={game.id}
/> />
) )
} }