diff --git a/app/games/games-page-client.tsx b/app/games/games-page-client.tsx index 9db4037..14a4c0f 100644 --- a/app/games/games-page-client.tsx +++ b/app/games/games-page-client.tsx @@ -8,7 +8,6 @@ import { Gamepad2Icon, SearchIcon, TrendingUpIcon, - ChevronDownIcon, XIcon, Loader2Icon, } from "lucide-react" @@ -108,10 +107,10 @@ export function GamesPageClient({ // Full reload when filters/sort change useEffect(() => { let cancelled = false - setLoading(true) - setError(null) async function fetchGames() { + setLoading(true) + setError(null) try { const url = buildUrl(0) const res = await fetch(url) diff --git a/app/games/page.tsx b/app/games/page.tsx index b8b1532..dc7be58 100644 --- a/app/games/page.tsx +++ b/app/games/page.tsx @@ -33,14 +33,7 @@ export const metadata: Metadata = { } export default async function GamesPage() { - // Fetch initial 24 games with benchmark counts - const benchmarkCountSql = sql`( - SELECT count(*)::int FROM ${performanceEntries} - INNER JOIN ${gameVersions} ON ${performanceEntries.versionId} = ${gameVersions.id} - WHERE ${gameVersions.gameId} = ${games.id} - AND ${performanceEntries.isRemoved} = false - )` - + // Fetch initial 24 games const gamesData = await db .select({ id: games.id, @@ -52,7 +45,6 @@ export default async function GamesPage() { genres: games.genres, source: games.source, createdAt: games.createdAt, - benchmarkCount: benchmarkCountSql, }) .from(games) .orderBy(desc(games.createdAt)) @@ -63,8 +55,32 @@ export default async function GamesPage() { .select({ count: sql`count(*)::int` }) .from(games) - // Fetch platform support for initial games (prioritise Steam Deck) + // Get benchmark counts for the initial games const gameIds = gamesData.map((g) => g.id) + + const benchmarkCounts = gameIds.length > 0 + ? await db + .select({ + gameId: gameVersions.gameId, + count: sql`count(*)::int`, + }) + .from(performanceEntries) + .innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id)) + .where( + and( + inArray(gameVersions.gameId, gameIds), + eq(performanceEntries.isRemoved, false), + ), + ) + .groupBy(gameVersions.gameId) + : [] + + const benchmarkMap = new Map() + for (const row of benchmarkCounts) { + benchmarkMap.set(row.gameId, row.count) + } + + // Fetch platform support for initial games (prioritise Steam Deck) const platformRows = gameIds.length > 0 ? await db .select({ @@ -116,7 +132,7 @@ export default async function GamesPage() { headerImage: g.headerImage, genres: g.genres, source: g.source, - benchmarkCount: g.benchmarkCount, + benchmarkCount: benchmarkMap.get(g.id) ?? 0, deckStatus: platformMap.get(g.id) ?? null, })) @@ -149,4 +165,4 @@ export default async function GamesPage() { /> ) -} +} \ No newline at end of file diff --git a/app/sitemap.ts b/app/sitemap.ts index 3056c53..07b50a7 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -15,7 +15,7 @@ export default async function sitemap(): Promise { lastModified: game.updatedAt, changeFrequency: "weekly" as const, priority: 0.8, - images: game.capsuleImage ? [{ url: game.capsuleImage }] : undefined, + images: game.capsuleImage ? [game.capsuleImage] : undefined, })) const deviceEntries: MetadataRoute.Sitemap = allDevices.map((device) => ({ diff --git a/lib/api/games-listing.ts b/lib/api/games-listing.ts index 5512083..63affea 100644 --- a/lib/api/games-listing.ts +++ b/lib/api/games-listing.ts @@ -14,7 +14,7 @@ const PAGE_SIZE = 24 export const gamesListingRoutes = new Elysia({ prefix: "/games/listing" }).get( "/", - async ({ query, set }) => { + async ({ query }) => { const offset = Math.min(Number(query.offset) || 0, MAX_OFFSET) const limit = Math.min(Number(query.limit) || PAGE_SIZE, 100) const search = query.search || "" @@ -59,55 +59,6 @@ export const gamesListingRoutes = new Elysia({ prefix: "/games/listing" }).get( const where = conditions.length > 0 ? and(...conditions) : undefined - // Subquery for benchmark count — referenced in both SELECT and ORDER BY - const benchmarkCountSql = sql`( - SELECT count(*)::int FROM ${performanceEntries} - INNER JOIN ${gameVersions} ON ${performanceEntries.versionId} = ${gameVersions.id} - WHERE ${gameVersions.gameId} = ${games.id} - AND ${performanceEntries.isRemoved} = false - )` - - // Determine sort order - let orderBy - switch (sort) { - case "name": - orderBy = asc(games.title) - break - case "benchmarks": - orderBy = desc(benchmarkCountSql) - break - case "recent": - default: - orderBy = desc(games.createdAt) - break - } - - // Fetch games with benchmark counts - const gamesQuery = db - .select({ - id: games.id, - steamAppId: games.steamAppId, - title: games.title, - developer: games.developer, - capsuleImage: games.capsuleImage, - headerImage: games.headerImage, - genres: games.genres, - source: games.source, - createdAt: games.createdAt, - benchmarkCount: benchmarkCountSql, - }) - .from(games) - .where(where) - .orderBy(orderBy) - .limit(limit) - .offset(offset) - - // Count total - const countQuery = db - .select({ count: sql`count(*)::int` }) - .from(games) - .where(where) - // Fetch all genres (for filter options) const genreRows = await db .select({ genres: games.genres }) @@ -129,13 +80,71 @@ export const gamesListingRoutes = new Elysia({ prefix: "/games/listing" }).get( .from(hardware) .orderBy(hardware.sortOrder) - const [data, countResult] = await Promise.all([gamesQuery, countQuery]) + // Count total games matching filters + const countQuery = db + .select({ count: sql`count(*)::int` }) + .from(games) + .where(where) + + // Determine sort order + let orderBy + switch (sort) { + case "name": + orderBy = asc(games.title) + break + case "benchmarks": + case "recent": + default: + orderBy = desc(games.createdAt) + break + } + + // Fetch games page + const data = await db + .select({ + id: games.id, + steamAppId: games.steamAppId, + title: games.title, + developer: games.developer, + capsuleImage: games.capsuleImage, + headerImage: games.headerImage, + genres: games.genres, + source: games.source, + createdAt: games.createdAt, + }) + .from(games) + .where(where) + .orderBy(orderBy) + .limit(limit) + .offset(offset) - // Fetch platform support for the returned games - // Prioritise Steam Deck entries (slug starts with "steamdeck") for deckStatus. - // If no Steam Deck entry exists, fall back to the first available device. const gameIds = data.map((g) => g.id) - let platformMap = new Map() + + // Fetch benchmark counts for the returned games (separate query to avoid subquery ambiguity) + const benchmarkCounts = gameIds.length > 0 + ? await db + .select({ + gameId: gameVersions.gameId, + count: sql`count(*)::int`, + }) + .from(performanceEntries) + .innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id)) + .where( + and( + inArray(gameVersions.gameId, gameIds), + eq(performanceEntries.isRemoved, false), + ), + ) + .groupBy(gameVersions.gameId) + : [] + + const benchmarkMap = new Map() + for (const row of benchmarkCounts) { + benchmarkMap.set(row.gameId, row.count) + } + + // Fetch platform support for the returned games (prioritise Steam Deck) + const platformMap = new Map() if (gameIds.length > 0) { const platformRows = await db .select({ @@ -149,7 +158,6 @@ export const gamesListingRoutes = new Elysia({ prefix: "/games/listing" }).get( for (const row of platformRows) { const isSteamDeck = row.hardwareSlug.startsWith("steamdeck") const existing = platformMap.get(row.gameId) - // Prefer Steam Deck entries; if we already have a non-Deck entry, replace it if (!existing || (!existing.startsWith("steamdeck") && isSteamDeck)) { platformMap.set(row.gameId, row.protonStatus) } @@ -165,13 +173,20 @@ export const gamesListingRoutes = new Elysia({ prefix: "/games/listing" }).get( headerImage: g.headerImage, genres: g.genres, source: g.source, - benchmarkCount: g.benchmarkCount, + benchmarkCount: benchmarkMap.get(g.id) ?? 0, deckStatus: platformMap.get(g.id) ?? null, })) + // If sorting by benchmarks, re-sort the enriched data + if (sort === "benchmarks") { + enrichedData.sort((a, b) => b.benchmarkCount - a.benchmarkCount) + } + + const [{ count: total }] = await countQuery + return { data: enrichedData, - total: countResult[0]?.count ?? 0, + total, limit, offset, genres: Array.from(genreSet).sort(), @@ -188,4 +203,4 @@ export const gamesListingRoutes = new Elysia({ prefix: "/games/listing" }).get( sort: t.Optional(t.String()), }), }, -) +) \ No newline at end of file