"use client" import { useCallback, useState, useEffect, useMemo, useRef } from "react" import { useRouter, useSearchParams } from "next/navigation" import Image from "next/image" import { Gamepad2Icon, MessageSquareIcon, SettingsIcon, TrendingUpIcon, ExternalLinkIcon, ClockIcon, SparklesIcon, DatabaseIcon, Plus, ThumbsUpIcon, ThumbsDownIcon, GaugeIcon, ChevronDownIcon, PencilIcon, } from "lucide-react" import Link from "next/link" import { useSession } from "@/lib/auth-client" import { FaSteam } from "react-icons/fa" import { motion, AnimatePresence } from "motion/react" import type { GameSettingCategory } from "@/lib/db/schema/performanceEntries" import { PresetDetailModal } from "./preset-detail-modal" import { BookmarkButton } from "@/components/saved-games/bookmark-button" import { WindowsIcon, MacIcon, LinuxIcon } from "@/app/components/PlatformIcons" import { AntiCheatBadge } from "@/components/anti-cheat-badge" import { PlayabilityBadge } from "@/components/playability-badge" import { SteamReviews } from "@/components/steam-reviews" import { CommunitySuggestionForm } from "@/components/community-suggestion-form" // Chart imports import { HistoricalAreaChart } from "@/components/charts/HistoricalAreaChart" import { UpscalerBarChart } from "@/components/charts/UpscalerBarChart" import { FpsBoxplot } from "@/components/charts/FpsBoxplot" import { FpsRangeChart } from "@/components/charts/FpsRangeChart" import { DeviceDonut } from "@/components/charts/DeviceDonut" import { PerformanceTierChart } from "@/components/charts/PerformanceTierChart" import { StabilityScatterChart } from "@/components/charts/StabilityScatterChart" // Comments import { CommentSection } from "@/components/comments/comment-section" // Types interface Game { id: string steamAppId: number | null title: string description: string | null developer: string | null publisher: string | null genres: string[] | null headerImage: string | null capsuleImage: string | null storeUrl: string | null source: string lastSync: string | null syncStatus: string | null createdAt: string metascore?: number | null onlineMultiplayerStatus?: string | null systemRequirements: { minimum: string | null; recommended: string | null } | null metacriticScore: number | null metacriticUrl: string | null recommendationsTotal: number | null priceCurrent: number | null priceInitial: number | null priceCurrency: string | null isFree: boolean releaseDate: string | null categories: string[] | null platforms: { windows: boolean; mac: boolean; linux: boolean } | null playabilityStatus?: "great" | "playable" | "needs_tweaks" | "unplayable" | "unknown" | null steamReviewScore?: number | null steamReviewSentiment?: string | null steamReviewCount?: number | null } interface Counts { benchmarks: number presets: number comments: number } interface PlatformSupport { id: string gameId: string hardwareSlug: string isSupported: boolean protonStatus: string playabilityStatus: "great" | "playable" | "needs_tweaks" | "unplayable" | "unknown" | null antiCheatRelevant: boolean antiCheatStatus: "none" | "supported" | "unsupported" | "unknown" | null antiCheatName: string | null } interface Preset { id: string gameId?: string hardwareSlug: string hardwareName: string upvotes: number downvotes: number settingsJson: GameSettingCategory[] | null settingsCount: number fpsAvg: number | null fpsLow: number | null fpsHigh: number | null fpsOnePercentLow: number | null upscalerType: string | null upscalerVersion: string | null frameGenMethod: string | null protonVersion: string | null osVersion: string | null launchOptions: string | null loadTimeSsd: number | null loadTimeSd: number | null estimatedBatteryMin: number | null customSystem: boolean userNotes: string | null userId: string userName: string | null userImage: string | null verifiedAt: string | null isPinned: boolean pinnedAt: string | null createdAt: string } interface StatsResponse { summary: { totalEntries: number avgFps: number bestDevice: string verifiedCount: number versionCount: number avgStability: number | null bestOnePercentLow: number | null } isRawPerformer: boolean isPoorPerformance: boolean boxplot: Array<{ hardwareSlug: string hardwareName: string min: number q1: number median: number onePercentLow?: number q3: number max: number }> historical: Array<{ period: string entries: Array<{ hardwareSlug: string avgFps: number count: number }> }> upscalerStats: Array<{ upscalerType: string upscalerVersion?: string frameGenMethod: string hardwareSlug: string avgFps: number count: number }> fpsRange: Array<{ id: string hardwareSlug: string fpsLow: number fpsAvg: number fpsHigh: number fpsOnePercentLow: number | null isRawPerformer: boolean }> deviceBreakdown: Array<{ hardwareSlug: string hardwareName: string count: number }> performanceTiers: Array<{ hardwareSlug: string unplayable: number playable: number smooth: number excellent: number }> stabilityScatter: Array<{ id: string hardwareSlug: string fpsAvg: number fpsOnePercentLow: number stabilityRatio: number }> filterOptions: { protonVersions: string[] osVersions: string[] } } interface Props { game: Game counts: Counts platformSupport: PlatformSupport[] presets: Preset[] gameId: string } const UPSCALER_OPTIONS = ["Any", "None", "FSR", "DLSS", "XeSS", "LSFG", "Other"] const FRAMEGEN_OPTIONS = ["Any", "None", "FSR FG", "DLSS FG", "LSFG", "Other"] function formatDate(value: string | null): string { if (!value) return "—" return new Date(value).toLocaleDateString() } function isRawPerformerPreset(preset: Preset): boolean { return ( preset.fpsAvg !== null && preset.fpsAvg >= 60 && preset.upscalerType === "none" && preset.frameGenMethod === "none" ) } function isPoorPerformancePreset(preset: Preset): boolean { return preset.fpsAvg !== null && preset.fpsAvg < 30 } function getFpsColor(preset: Preset): string { if (preset.fpsAvg === null) return "text-text/60" if (isRawPerformerPreset(preset)) return "text-green-400" if (preset.frameGenMethod && preset.frameGenMethod !== "none") { if (preset.frameGenMethod === "dlss_fg") return "text-blue-400" return "text-orange-400" } if (preset.upscalerType && preset.upscalerType !== "none") return "text-orange-400" return "text-text/60" } function generatePresetName(preset: Preset): string { const parts = [preset.hardwareName] if (preset.fpsAvg !== null) parts.push(`${Math.round(preset.fpsAvg)}fps`) return parts.join(" · ") } export function GamePageClient({ game, counts, platformSupport, presets, gameId, }: Props) { const router = useRouter() const searchParams = useSearchParams() const { data: session } = useSession() const [imgError, setImgError] = useState(false) const [stats, setStats] = useState(null) const [selectedDevices, setSelectedDevices] = useState([]) const [filters, setFilters] = useState({ proton: "all", os: "all", upscaler: "all", frameGen: "all", }) const [loading, setLoading] = useState(true) const [selectedPresetId, setSelectedPresetId] = useState(null) const [showSystemReq, setShowSystemReq] = useState(true) const [reportedPresets, setReportedPresets] = useState>(new Set()) const presetsRef = useRef(null) // On mount, auto-open preset from URL useEffect(() => { const presetId = searchParams.get("preset") queueMicrotask(() => { if (presetId) { setSelectedPresetId(presetId) } else { setSelectedPresetId(null) } }) }, [searchParams]) const handlePresetOpen = (presetId: string) => { setSelectedPresetId(presetId) const url = new URL(window.location.href) url.searchParams.set("preset", presetId) router.replace(url.toString(), { scroll: false }) } const handlePresetClose = () => { setSelectedPresetId(null) const url = new URL(window.location.href) url.searchParams.delete("preset") router.replace(url.toString(), { scroll: false }) } const handleDeletePreset = async (presetId: string) => { try { const res = await fetch(`/api/performance/${presetId}/user-delete`, { method: "DELETE", headers: { "Content-Type": "application/json" }, }) if (res.ok) { setSelectedPresetId(null) router.refresh() } } catch (err) { console.error("Failed to delete preset:", err) } } const handleReportPreset = async (presetId: string, reason: "inaccurate" | "spam" | "inappropriate" | "other", details?: string) => { try { const res = await fetch(`/api/performance/${presetId}/report`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ reason, details }), }) if (res.ok) { setReportedPresets((prev) => new Set(prev).add(presetId)) } } catch (err) { console.error("Failed to report preset:", err) } } const handleImgError = useCallback(() => setImgError(true), []) // Fetch stats on mount useEffect(() => { let cancelled = false async function fetchStats() { setLoading(true) try { const res = await fetch(`/api/games/${gameId}/stats`) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() if (!cancelled) setStats(data) } catch (err) { console.error("Failed to fetch stats:", err) } finally { if (!cancelled) setLoading(false) } } fetchStats() return () => { cancelled = true } }, [gameId]) // Initialize selected devices when stats load const didInitDevices = useRef(false) useEffect(() => { if (stats && !didInitDevices.current) { didInitDevices.current = true const slugs = stats.deviceBreakdown.map((d) => d.hardwareSlug) queueMicrotask(() => setSelectedDevices(slugs)) } }, [stats]) // Filtered stats const filteredStats = useMemo(() => { if (!stats) return null const deviceSet = new Set(selectedDevices) const filterByDevice = (arr: T[] | undefined) => arr?.filter((item) => deviceSet.has(item.hardwareSlug)) ?? [] const filterUpscaler = (arr: StatsResponse["upscalerStats"] | undefined) => (arr ?? []).filter((item) => { if (!deviceSet.has(item.hardwareSlug)) return false if (filters.upscaler !== "all" && item.upscalerType !== filters.upscaler) return false if ( filters.frameGen !== "all" && item.frameGenMethod !== filters.frameGen ) return false return true }) return { ...stats, boxplot: filterByDevice(stats.boxplot), historical: (stats.historical ?? []) .map((h) => ({ ...h, entries: h.entries.filter((e) => deviceSet.has(e.hardwareSlug), ), })) .filter((h) => h.entries.length > 0), upscalerStats: filterUpscaler(stats.upscalerStats), fpsRange: filterByDevice(stats.fpsRange), deviceBreakdown: filterByDevice(stats.deviceBreakdown), performanceTiers: filterByDevice(stats.performanceTiers), stabilityScatter: filterByDevice(stats.stabilityScatter), } }, [stats, selectedDevices, filters]) // Filtered presets const filteredPresets = useMemo(() => { return presets.filter((p) => { if ( selectedDevices.length > 0 && !selectedDevices.includes(p.hardwareSlug) ) return false if (filters.proton !== "all" && p.protonVersion !== filters.proton) return false if (filters.os !== "all" && p.osVersion !== filters.os) return false if (filters.upscaler !== "all" && p.upscalerType !== filters.upscaler) return false if ( filters.frameGen !== "all" && p.frameGenMethod !== filters.frameGen ) return false return true }) }, [presets, selectedDevices, filters]) const pinnedPresets = useMemo(() => filteredPresets.filter((p) => p.isPinned), [filteredPresets]) const regularPresets = useMemo(() => filteredPresets.filter((p) => !p.isPinned), [filteredPresets]) const coverImage = game.capsuleImage || game.headerImage const protonDbUrl = game.steamAppId ? `https://www.protondb.com/app/${game.steamAppId}` : null const steamDbUrl = game.steamAppId ? `https://steamdb.info/app/${game.steamAppId}` : null const toggleDevice = useCallback((slug: string) => { setSelectedDevices((prev) => prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug], ) }, []) return ( <>
{/* Section 1: Hero Header */}
{/* Cover image */}
{coverImage && !imgError ? ( {game.title} ) : (
)}
{/* Info */}
{/* Title row */}

{game.title}

{stats?.isRawPerformer && ( Raw Performer )} {stats?.isPoorPerformance && ( ⚠ Poor Performance )} {/* Playability badge */} {game.playabilityStatus && ( )} {/* Anti-cheat badge */} {platformSupport?.some((p) => p.antiCheatRelevant) && ( p.antiCheatRelevant)?.antiCheatStatus ?? "unknown" } antiCheatName={ platformSupport.find((p) => p.antiCheatRelevant)?.antiCheatName } /> )} {/* Steam review score */} {game.steamReviewScore != null && ( {game.steamReviewScore}% Positive {game.steamReviewSentiment && ( ({game.steamReviewSentiment.replace(/_/g, ' ')}) )} )}
{/* Subtitle */}
{game.developer && {game.developer}} {game.developer && game.publisher && ( · )} {game.publisher && {game.publisher}} {game.genres && game.genres.length > 0 && ( <> · {game.genres.slice(0, 3).join(", ")} )}
{/* Stats badges */}
{game.metascore !== undefined && game.metascore !== null && ( ★ {game.metascore} )}
{/* Metadata pills */}
{game.releaseDate && ( {game.releaseDate} )} {game.isFree && ( Free to Play )} {game.priceCurrent != null && !game.isFree && game.priceCurrency && ( {new Intl.NumberFormat("en-US", { style: "currency", currency: game.priceCurrency }).format(game.priceCurrent / 100)} )} {game.priceCurrent != null && !game.isFree && game.priceInitial != null && game.priceInitial > game.priceCurrent && game.priceCurrency && ( {new Intl.NumberFormat("en-US", { style: "currency", currency: game.priceCurrency }).format(game.priceInitial / 100)} )} {game.metacriticScore != null && ( ★ {game.metacriticScore}/100 )} {game.onlineMultiplayerStatus && ( {game.onlineMultiplayerStatus} )} {game.platforms && ( {game.platforms.windows && } {game.platforms.mac && } {game.platforms.linux && } )} {game.steamAppId !== null && ( AppID {game.steamAppId} )}
{/* External links */}
{game.storeUrl && ( Steam )} {protonDbUrl && ( ProtonDB )} {steamDbUrl && ( SteamDB )}
{/* DB stats row */} {stats && (
{stats.summary.versionCount} versions · {stats.summary.verifiedCount} verified {game.lastSync && ( <> · Last sync{" "} {formatDate(game.lastSync)} )}
)} {stats && (
{session?.user && game.source !== "steam" && ( Edit Game )} {session && ( Add Benchmark )} {game.source !== "steam" && ( )}
)}
{/* Section 2: Overview */}
{/* Description */}

About

{game.description ? (

{game.description}

) : (

No description available.

)} {game.systemRequirements && (game.systemRequirements.minimum || game.systemRequirements.recommended) && (
{showSystemReq && (
{game.systemRequirements.minimum && (

Minimum

)} {game.systemRequirements.recommended && (

Recommended

)}
)}
)}
{/* Platforms */} {game.platforms && (

Platforms

)} {platformSupport.length > 0 && (

Platform Support

{platformSupport.map((ps) => (
{ps.hardwareSlug.replace( /-/g, " ", )}
{ps.isSupported ? "Supported" : "Unsupported"} {ps.protonStatus} {/* Per-device playability */} {ps.playabilityStatus && ( )} {/* Per-device anti-cheat */} {ps.antiCheatRelevant && ( )}
))}
)}
{/* Section 3: Device Selector + Filters */}
{/* Device ribbon */} {stats && stats.deviceBreakdown.length > 0 && (
{stats.deviceBreakdown.map((device) => { const active = selectedDevices.includes( device.hardwareSlug, ) return ( ) })}
)} {/* Fine filters */}
setFilters((f) => ({ ...f, proton: v })) } /> setFilters((f) => ({ ...f, os: v })) } /> o.toLowerCase())} onChange={(v) => setFilters((f) => ({ ...f, upscaler: v })) } /> o.toLowerCase().replace(" ", "_"), )} onChange={(v) => setFilters((f) => ({ ...f, frameGen: v })) } />
{/* Section 4a: Pinned Presets */} {pinnedPresets.length > 0 && (

📌 Pinned Presets

{pinnedPresets.length} pinned
{pinnedPresets.map((preset) => { const raw = isRawPerformerPreset(preset) const fpsColor = getFpsColor(preset) return ( handlePresetOpen(preset.id)} className={`flex flex-col gap-3 p-4 rounded-xl border transition-colors cursor-pointer hover:border-primary/30 ${ raw ? "border-green-500/30 bg-green-500/5" : "border-yellow-500/30 bg-yellow-500/5" }`} data-gamepad-focusable > {/* Header */}

{generatePresetName(preset)}

{raw && ( Raw )} {isPoorPerformancePreset(preset) && preset.fpsAvg !== null && ( ⚠ Slow )}

{preset.hardwareName}

{preset.upvotes} {preset.downvotes}
{/* Settings count */}
{preset.settingsCount} settings
{/* FPS */} {preset.fpsAvg !== null && (
{preset.fpsAvg} {" "} avg {preset.fpsOnePercentLow !== null && ( {" "} · {preset.fpsOnePercentLow} 1% low )} {preset.fpsLow !== null && preset.fpsHigh !== null && !preset.fpsOnePercentLow && ( {" "} ({preset.fpsLow}–{preset.fpsHigh}) )}
)} {/* Technology tags */}
{preset.upscalerType && preset.upscalerType !== "none" && ( {preset.upscalerType.toUpperCase()} {preset.upscalerVersion ? ` ${preset.upscalerVersion}` : ""} )} {preset.frameGenMethod && preset.frameGenMethod !== "none" && ( {preset.frameGenMethod === "fsr_fg" ? "FSR FG" : preset.frameGenMethod === "dlss_fg" ? "DLSS FG" : preset.frameGenMethod} )}
{/* Proton + OS */}
{preset.protonVersion && ( Proton{" "} {preset.protonVersion} )} {preset.osVersion && ( {preset.osVersion} )}
) })}
)} {/* Section 4b: Community Presets */}

Community Presets

{regularPresets.length} preset {regularPresets.length !== 1 ? "s" : ""}
{regularPresets.length === 0 ? (

No presets match the selected filters

) : (
{regularPresets.map((preset) => { const raw = isRawPerformerPreset(preset) const fpsColor = getFpsColor(preset) return ( handlePresetOpen(preset.id)} className={`flex flex-col gap-3 p-4 rounded-xl border transition-colors cursor-pointer hover:border-primary/30 ${ raw ? "border-green-500/30 bg-green-500/5" : "border-border bg-text/3" }`} data-gamepad-focusable > {/* Header */}

{generatePresetName(preset)}

{raw && ( Raw )} {isPoorPerformancePreset(preset) && preset.fpsAvg !== null && ( ⚠ Slow )}

{preset.hardwareName}

{preset.upvotes} {preset.downvotes}
{/* Settings count */}
{preset.settingsCount} settings
{/* FPS */} {preset.fpsAvg !== null && (
{preset.fpsAvg} {" "} avg {preset.fpsOnePercentLow !== null && ( {" "} · {preset.fpsOnePercentLow} 1% low )} {preset.fpsLow !== null && preset.fpsHigh !== null && !preset.fpsOnePercentLow && ( {" "} ({preset.fpsLow}–{preset.fpsHigh}) )}
)} {/* Technology tags */}
{preset.upscalerType && preset.upscalerType !== "none" && ( {preset.upscalerType.toUpperCase()} {preset.upscalerVersion ? ` ${preset.upscalerVersion}` : ""} )} {preset.frameGenMethod && preset.frameGenMethod !== "none" && ( {preset.frameGenMethod === "fsr_fg" ? "FSR FG" : preset.frameGenMethod === "dlss_fg" ? "DLSS FG" : preset.frameGenMethod} )}
{/* Proton + OS */}
{preset.protonVersion && ( Proton{" "} {preset.protonVersion} )} {preset.osVersion && ( {preset.osVersion} )}
) })}
)}
{/* Section 5: Statistics Dashboard */}
{/* Row 1 — Featured */} {filteredStats && (

Historical Performance

{filteredStats.historical.length > 0 ? ( ) : (
No historical data
)}
{filteredStats.summary.avgStability !== null && ( )}
)} {/* Loading state */} {loading && (
Loading statistics...
)} {/* Row 2 — Upscaler Bar */} {filteredStats && filteredStats.upscalerStats.length > 0 && (

Upscaler Performance

)} {/* Row 3 — 3-column grid */} {filteredStats && (
{filteredStats.boxplot.length > 0 && (

FPS Distribution

)} {filteredStats.fpsRange.length > 0 && (

FPS Range

)} {filteredStats.deviceBreakdown.length > 0 && (

Device Breakdown

)}
)} {/* Row 4 — Performance Tiers + Stability Scatter */} {filteredStats && (
{filteredStats.performanceTiers.length > 0 && (

Performance Tiers

)} {filteredStats.stabilityScatter.length > 0 && (

Avg FPS vs 1% Low (Stability)

[d.hardwareSlug, d.hardwareName]))} />
)}
)}
{/* Steam Reviews */} {game.steamAppId && (
)} {/* Section 6: Comments */}
{/* Mobile FAB for Add Benchmark */} {session && ( )}
{selectedPresetId && (() => { const preset = filteredPresets.find((p) => p.id === selectedPresetId) if (!preset) return null return ( ) })()} ) } function Badge({ icon: Icon, value, label, }: { icon: React.ElementType value: number label: string }) { return ( {value} {label} ) } function StatCard({ label, value, icon: Icon, }: { label: string value: string icon: React.ElementType }) { return (
{label} {value}
) } function FilterSelect({ label, value, options, onChange, }: { label: string value: string options: string[] onChange: (value: string) => void }) { return (
) }