"use client" import { useEffect, useMemo, useState } from "react" import Link from "next/link" import Image from "next/image" import { motion } from "motion/react" import { Gamepad2Icon, TrendingUpIcon, DatabaseIcon, CheckCircleIcon, ArrowRightIcon, Loader2, RefreshCwIcon, MonitorIcon, } from "lucide-react" import { EChartWrapper, CHART_THEME, getDeviceColor, } from "@/components/charts/EChartWrapper" import type { EChartsOption } from "echarts" interface DeviceInfo { slug: string name: string deviceType: string image: string | null colorIndex: number } interface UpscalerEntry { upscalerType: string count: number avgFps: number } interface DeviceStats { slug: string name: string deviceType: string totalBenchmarks: number avgFps: number | null verifiedCount: number gameCount: number boxplot: Array<{ gameId: string gameTitle: string min: number q1: number median: number q3: number max: number count: number }> historical: Array<{ period: string avgFps: number count: number }> topGames: Array<{ gameId: string gameTitle: string headerImage: string | null avgFps: number benchmarkCount: number }> genreBreakdown: Array<{ genre: string; count: number }> protonBreakdown: Array<{ version: string; count: number }> upscalerBreakdown: UpscalerEntry[] } const deviceTypeLabel: Record = { handheld: "Handheld", console: "Console", } const deviceTypeColor: Record = { handheld: "text-primary bg-primary/10 border-primary/20", console: "text-secondary bg-secondary/10 border-secondary/20", } export function DeviceDetailClient({ device }: { device: DeviceInfo }) { const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const deviceColor = getDeviceColor(device.colorIndex) async function fetchStats() { setLoading(true) setError(null) try { const res = await fetch(`/api/hardware/${device.slug}/stats`) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() setStats(data) } catch (err) { console.error("Failed to fetch device stats:", err) setError("Failed to load device statistics. Please try again.") } finally { setLoading(false) } } useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect fetchStats() }, [device.slug]) // eslint-disable-line react-hooks/exhaustive-deps const historicalOption = useMemo(() => { if (!stats || stats.historical.length === 0) return {} return { tooltip: { trigger: "axis", backgroundColor: "#1a1020", borderColor: CHART_THEME.border, textStyle: { color: CHART_THEME.text }, }, grid: { left: 50, right: 20, top: 10, bottom: 30 }, xAxis: { type: "category", data: stats.historical.map((h) => h.period), axisLine: { lineStyle: { color: CHART_THEME.border } }, axisLabel: { color: CHART_THEME.textMuted, fontSize: 11 }, }, yAxis: { type: "value", axisLine: { lineStyle: { color: CHART_THEME.border } }, splitLine: { lineStyle: { color: CHART_THEME.border, opacity: 0.3 } }, axisLabel: { color: CHART_THEME.textMuted, fontSize: 11 }, }, series: [ { type: "line", data: stats.historical.map((h) => h.avgFps), smooth: true, lineStyle: { color: deviceColor, width: 2 }, areaStyle: { color: { type: "linear", x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: deviceColor + "40" }, { offset: 1, color: deviceColor + "05" }, ], }, }, symbol: "circle", symbolSize: 4, itemStyle: { color: deviceColor }, }, ], } }, [stats, deviceColor]) const boxplotOption = useMemo(() => { if (!stats || stats.boxplot.length === 0) return {} return { tooltip: { trigger: "item", backgroundColor: "#1a1020", borderColor: CHART_THEME.border, textStyle: { color: CHART_THEME.text }, }, grid: { left: 80, right: 20, top: 10, bottom: 40 }, xAxis: { type: "category", data: stats.boxplot.map((b) => b.gameTitle), axisLine: { lineStyle: { color: CHART_THEME.border } }, axisLabel: { color: CHART_THEME.textMuted, fontSize: 10, rotate: 30 }, }, yAxis: { type: "value", name: "FPS", axisLine: { lineStyle: { color: CHART_THEME.border } }, splitLine: { lineStyle: { color: CHART_THEME.border, opacity: 0.3 } }, axisLabel: { color: CHART_THEME.textMuted, fontSize: 11 }, nameTextStyle: { color: CHART_THEME.textMuted, fontSize: 11 }, }, series: [ { type: "boxplot", data: stats.boxplot.map((b) => [b.min, b.q1, b.median, b.q3, b.max]), itemStyle: { color: deviceColor + "30", borderColor: deviceColor }, }, ], } }, [stats, deviceColor]) const genreOption = useMemo(() => { if (!stats || stats.genreBreakdown.length === 0) return {} return { tooltip: { trigger: "item", backgroundColor: "#1a1025", borderColor: CHART_THEME.border, textStyle: { color: CHART_THEME.text }, }, series: [ { type: "pie", radius: ["40%", "70%"], center: ["50%", "50%"], data: stats.genreBreakdown.map((g, i) => ({ name: g.genre, value: g.count, itemStyle: { color: CHART_THEME.deviceColors[i % CHART_THEME.deviceColors.length], }, })), label: { color: CHART_THEME.textMuted, fontSize: 10 }, emphasis: { itemStyle: { shadowBlur: 10, shadowColor: "rgba(0,0,0,0.5)" }, }, }, ], } }, [stats]) return (
{/* Hero Header */}
{device.image ? ( {device.name} ) : ( )}

{device.name}

{deviceTypeLabel[device.deviceType] || device.deviceType} {stats && stats.verifiedCount > 0 && ( {stats.verifiedCount} verified )}

Performance benchmarks and statistics

{/* Overview Stats */} {stats && !loading && (
)} {/* Loading */} {loading && (
)} {/* Error State */} {error && !loading && (

{error}

)} {/* Charts */} {stats && !loading && !error && (
{stats.historical.length > 0 && (

Historical Performance

)}
{stats.boxplot.length > 0 && (

FPS Distribution by Game

)} {stats.genreBreakdown.length > 0 && (

Genre Breakdown

)}
{(stats.protonBreakdown.length > 0 || stats.upscalerBreakdown.length > 0) && (
{stats.protonBreakdown.length > 0 && (

Proton Version Distribution

{stats.protonBreakdown.map((p) => (
{p.version}
{p.count}
))}
)} {stats.upscalerBreakdown.length > 0 && (

Upscaler Performance

{stats.upscalerBreakdown.map((f) => (
{f.upscalerType === "none" ? "Native" : f.upscalerType.toUpperCase()}
{f.avgFps} FPS ({f.count})
))}
)}
)} {stats.topGames.length > 0 && (

Top Games by Average FPS

{stats.topGames.slice(0, 8).map((game, i) => (
{i + 1}

{game.gameTitle}

{game.avgFps} FPS {game.benchmarkCount} runs
))}
)}
)} {/* Empty state */} {stats && !loading && !error && stats.totalBenchmarks === 0 && (

No benchmark data yet for this device

Data will appear as benchmarks are submitted

)}
) } function StatCard({ icon: Icon, label, value }: { icon: React.ElementType; label: string; value: string }) { return (
{label} {value}
) }