diff --git a/docs/superpowers/plans/2026-04-27-devices-admin.md b/docs/superpowers/plans/2026-04-27-devices-admin.md index 6389ab8..060b78a 100644 --- a/docs/superpowers/plans/2026-04-27-devices-admin.md +++ b/docs/superpowers/plans/2026-04-27-devices-admin.md @@ -230,4 +230,1354 @@ git commit -m "refactor: remove broken Community Trust chart (TrustBar) - Remove trust data from game-stats API response - Remove trust section from game detail page - DB columns and vote endpoints preserved for future voting system" +``` + +--- + +### Task 4: Redesign Devices List Page + +**Files:** +- Modify: `app/devices/page.tsx` +- Modify: `app/devices/page-client.tsx` + +- [ ] **Step 1: Rewrite the server component to fetch data and pass as props** + +Replace the entire content of `app/devices/page.tsx` with: + +```tsx +import { db } from "@/lib/db/index" +import { hardware, performanceEntries, gameVersions, games } from "@/lib/db/schema" +import { eq, and, sql, desc } from "drizzle-orm" +import { DevicesPageClient } from "./page-client" +import type { Metadata } from "next" + +export const metadata: Metadata = { + title: "Devices — DeckyVault", + description: + "Browse benchmark data for handheld and console gaming devices. Compare FPS, performance stats, and community benchmarks on DeckyVault.", + keywords: ["steam deck", "handheld", "console", "benchmarks", "FPS", "performance", "devices"], + alternates: { canonical: "https://deckyvault.xyz/devices" }, + openGraph: { + title: "Devices — DeckyVault", + description: + "Browse benchmark data for handheld and console gaming devices on DeckyVault.", + url: "https://deckyvault.xyz/devices", + siteName: "DeckyVault", + type: "website", + }, +} + +export default async function DevicesPage() { + // Fetch all hardware devices ordered by sortOrder + const deviceRows = await db + .select({ + slug: hardware.slug, + name: hardware.name, + deviceType: hardware.deviceType, + image: hardware.image, + sortOrder: hardware.sortOrder, + }) + .from(hardware) + .orderBy(hardware.sortOrder) + + // Get aggregated stats per device + const statsPerDevice = await db + .select({ + hardwareSlug: performanceEntries.hardwareSlug, + totalBenchmarks: sql`count(*)::int`, + avgFps: sql`round(avg(${performanceEntries.fpsAvg})::numeric, 1)`, + verifiedCount: sql`count(*) filter (where ${performanceEntries.verifiedAt} is not null)::int`, + gameCount: sql`count(distinct ${gameVersions.gameId})::int`, + }) + .from(performanceEntries) + .innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id)) + .innerJoin(games, eq(gameVersions.gameId, games.id)) + .innerJoin(hardware, eq(performanceEntries.hardwareSlug, hardware.slug)) + .where(eq(performanceEntries.isRemoved, false)) + .groupBy(performanceEntries.hardwareSlug) + + const statsMap = new Map(statsPerDevice.map((s) => [s.hardwareSlug, s])) + + // Best game per device (highest avg FPS) + const bestGames = await db + .select({ + hardwareSlug: performanceEntries.hardwareSlug, + gameId: games.id, + gameTitle: games.title, + gameHeaderImage: games.headerImage, + fpsAvg: sql`round(avg(${performanceEntries.fpsAvg})::numeric, 1)`, + }) + .from(performanceEntries) + .innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id)) + .innerJoin(games, eq(gameVersions.gameId, games.id)) + .where(eq(performanceEntries.isRemoved, false)) + .groupBy( + performanceEntries.hardwareSlug, + games.id, + games.title, + games.headerImage, + ) + .orderBy(desc(sql`avg(${performanceEntries.fpsAvg})`)) + + const bestGameMap = new Map< + string, + { id: string; title: string; headerImage: string | null; fpsAvg: number } + >() + for (const bg of bestGames) { + if (!bestGameMap.has(bg.hardwareSlug)) { + bestGameMap.set(bg.hardwareSlug, { + id: bg.gameId, + title: bg.gameTitle, + headerImage: bg.gameHeaderImage, + fpsAvg: Number(bg.fpsAvg), + }) + } + } + + // Build device data with stats + const devices = deviceRows.map((device, index) => { + const stats = statsMap.get(device.slug) + const bestGame = bestGameMap.get(device.slug) + return { + slug: device.slug, + name: device.name, + deviceType: device.deviceType, + image: device.image, + sortOrder: device.sortOrder, + colorIndex: index, + totalBenchmarks: stats?.totalBenchmarks ?? 0, + avgFps: stats?.avgFps ? Number(stats.avgFps) : null, + gameCount: stats?.gameCount ?? 0, + verifiedCount: stats?.verifiedCount ?? 0, + bestGame: bestGame ?? null, + } + }) + + // JSON-LD structured data + const jsonLd = { + "@context": "https://schema.org", + "@type": "ItemList", + itemListElement: devices.map((d, i) => ({ + "@type": "ListItem", + position: i + 1, + name: d.name, + url: `https://deckyvault.xyz/devices/${d.slug}`, + })), + } + + return ( + <> +