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 ( <>