diff --git a/app/game/[id]/game-page-client.tsx b/app/game/[id]/game-page-client.tsx
index 8c10444..a782c37 100644
--- a/app/game/[id]/game-page-client.tsx
+++ b/app/game/[id]/game-page-client.tsx
@@ -43,6 +43,7 @@ import { FpsRangeChart } from "@/components/charts/FpsRangeChart"
import { DeviceDonut } from "@/components/charts/DeviceDonut"
import { PerformanceTierChart } from "@/components/charts/PerformanceTierChart"
import { StabilityScatterChart } from "@/components/charts/StabilityScatterChart"
+import { BatteryLifeChart } from "@/components/charts/BatteryLifeChart"
// Comments
import { CommentSection } from "@/components/comments/comment-section"
@@ -122,6 +123,9 @@ interface Preset {
loadTimeSsd: number | null
loadTimeSd: number | null
estimatedBatteryMin: number | null
+ tdpWatts: number | null
+ youtubeVideoId: string | null
+ screenshots: Array<{ id: string; url: string; width: number; height: number; orderIndex: number }> | null
customSystem: boolean
userNotes: string | null
userId: string
@@ -131,6 +135,8 @@ interface Preset {
isPinned: boolean
pinnedAt: string | null
createdAt: string
+ hardwareWattHours: number | null
+ hardwareDeviceType: string | null
}
interface StatsResponse {
@@ -184,6 +190,9 @@ interface StatsResponse {
hardwareSlug: string
hardwareName: string
count: number
+ wattHours: number | null
+ tdpMax: number | null
+ deviceType: string | null
}>
performanceTiers: Array<{
hardwareSlug: string
@@ -199,6 +208,16 @@ interface StatsResponse {
fpsOnePercentLow: number
stabilityRatio: number
}>
+ batteryLife: Array<{
+ id: string
+ hardwareSlug: string
+ tdpWatts: number
+ estimatedBatteryMin: number
+ estimatedBatteryHours: number
+ wattHours: number | null
+ tdpMax: number | null
+ estimatedAtMaxTdpMin: number | null
+ }>
filterOptions: {
protonVersions: string[]
osVersions: string[]
@@ -677,6 +696,7 @@ export function GamePageClient({
deviceBreakdown: filterByDevice(stats.deviceBreakdown),
performanceTiers: filterByDevice(stats.performanceTiers),
stabilityScatter: filterByDevice(stats.stabilityScatter),
+ batteryLife: filterByDevice(stats.batteryLife),
}
}, [stats, selectedDevices, filters])
@@ -1089,6 +1109,11 @@ export function GamePageClient({
+ {preset.youtubeVideoId && (
+
+ )}
{preset.upvotes}
@@ -1132,6 +1157,27 @@ export function GamePageClient({
)}
+ {/* Power / Battery quick-look */}
+ {(() => {
+ const dev = stats?.deviceBreakdown.find((d) => d.hardwareSlug === preset.hardwareSlug)
+ const isHandheld = dev?.deviceType === "handheld"
+ const wh = dev?.wattHours ?? null
+ const tdp = preset.tdpWatts ?? null
+ const estHours = wh && tdp && tdp > 0 ? wh / tdp : null
+
+ if (!isHandheld || (!tdp && !wh)) return null
+
+ return (
+
+ {tdp && ⚡ {Math.round(tdp)}W}
+ {wh && 🔋 {Math.round(wh)}Wh}
+ {estHours !== null && (
+ ⏱ ~{estHours.toFixed(1)}h
+ )}
+
+ )
+ })()}
+
{/* Technology tags */}
{preset.upscalerType &&
@@ -1254,6 +1300,11 @@ export function GamePageClient({
+ {preset.youtubeVideoId && (
+
+ )}
{preset.upvotes}
@@ -1297,6 +1348,27 @@ export function GamePageClient({
)}
+ {/* Power / Battery quick-look */}
+ {(() => {
+ const dev = stats?.deviceBreakdown.find((d) => d.hardwareSlug === preset.hardwareSlug)
+ const isHandheld = dev?.deviceType === "handheld"
+ const wh = dev?.wattHours ?? null
+ const tdp = preset.tdpWatts ?? null
+ const estHours = wh && tdp && tdp > 0 ? wh / tdp : null
+
+ if (!isHandheld || (!tdp && !wh)) return null
+
+ return (
+
+ {tdp && ⚡ {Math.round(tdp)}W}
+ {wh && 🔋 {Math.round(wh)}Wh}
+ {estHours !== null && (
+ ⏱ ~{estHours.toFixed(1)}h
+ )}
+
+ )
+ })()}
+
{/* Technology tags */}
{preset.upscalerType &&
@@ -1491,6 +1563,19 @@ export function GamePageClient({
)}
+ {/* Battery Life Estimates */}
+ {filteredStats && filteredStats.batteryLife && filteredStats.batteryLife.length > 0 && (
+
+
+ Battery Life Estimates
+
+ [d.hardwareSlug, d.hardwareName]))}
+ />
+
+ )}
+
diff --git a/app/game/[id]/page.tsx b/app/game/[id]/page.tsx
index e90223f..d63998a 100644
--- a/app/game/[id]/page.tsx
+++ b/app/game/[id]/page.tsx
@@ -11,9 +11,11 @@ import {
gamePlatformSupport,
hardware,
user,
+ entryScreenshots,
} from "@/lib/db/schema"
import { and, desc, eq, sql } from "drizzle-orm"
import { isSyncStale, syncSteamGame, ensureSteamGame } from "@/lib/steam/sync"
+import { getR2PublicUrl } from "@/lib/storage"
import { GamePageClient } from "./game-page-client"
// This page needs live data — skip static generation at build time
@@ -177,6 +179,8 @@ export default async function GamePage({
loadTimeSsd: performanceEntries.loadTimeSsd,
loadTimeSd: performanceEntries.loadTimeSd,
estimatedBatteryMin: performanceEntries.estimatedBatteryMin,
+ tdpWatts: performanceEntries.tdpWatts,
+ youtubeVideoId: performanceEntries.youtubeVideoId,
customSystem: performanceEntries.customSystem,
userNotes: performanceEntries.userNotes,
verifiedAt: performanceEntries.verifiedAt,
@@ -278,6 +282,11 @@ export default async function GamePage({
loadTimeSsd: p.loadTimeSsd ?? null,
loadTimeSd: p.loadTimeSd ?? null,
estimatedBatteryMin: p.estimatedBatteryMin ?? null,
+ tdpWatts: p.tdpWatts ?? null,
+ youtubeVideoId: p.youtubeVideoId ?? null,
+ screenshots: null,
+ hardwareWattHours: null,
+ hardwareDeviceType: null,
customSystem: p.customSystem ?? false,
userNotes: p.userNotes,
userId: p.userId,
@@ -289,6 +298,42 @@ export default async function GamePage({
pinnedAt: p.pinnedAt ? p.pinnedAt.toISOString() : null,
}))
+ // Fetch screenshots and hardware details for each preset
+ const publicUrl = getR2PublicUrl()
+ for (const preset of serializedPresets) {
+ const screenshots = await db
+ .select({
+ id: entryScreenshots.id,
+ storageKey: entryScreenshots.storageKey,
+ orderIndex: entryScreenshots.orderIndex,
+ width: entryScreenshots.width,
+ height: entryScreenshots.height,
+ })
+ .from(entryScreenshots)
+ .where(eq(entryScreenshots.entryId, preset.id))
+ .orderBy(entryScreenshots.orderIndex)
+
+ preset.screenshots = screenshots.map((ss) => ({
+ id: ss.id,
+ url: `${publicUrl}/${ss.storageKey}`,
+ width: ss.width,
+ height: ss.height,
+ orderIndex: ss.orderIndex,
+ })) as any
+
+ const [hw] = await db
+ .select({
+ wattHours: hardware.wattHours,
+ deviceType: hardware.deviceType,
+ })
+ .from(hardware)
+ .where(eq(hardware.slug, preset.hardwareSlug))
+ .limit(1)
+
+ preset.hardwareWattHours = hw?.wattHours ? Number(hw.wattHours) : null as any
+ preset.hardwareDeviceType = (hw?.deviceType ?? null) as any
+ }
+
return (
<>