"use client" import { useState, useEffect, useRef, useCallback } from "react" import { useRouter } from "next/navigation" import Image from "next/image" import Link from "next/link" import { motion } from "motion/react" import { Gamepad2Icon, SearchIcon, TrendingUpIcon, XIcon, Loader2Icon, } from "lucide-react" import { useGamepadNavigation } from "@/lib/hooks/use-gamepad-navigation" import { AntiCheatBadge } from "@/components/anti-cheat-badge" import { PlayabilityBadge } from "@/components/playability-badge" import { SavedFilters } from "@/components/saved-filters" interface GamesListItem { id: string steamAppId: number | null title: string developer: string | null capsuleImage: string | null headerImage: string | null genres: string[] | null source: string steamReviewScore: number | null playabilityStatus: "great" | "playable" | "needs_tweaks" | "unplayable" | "unknown" | null onlineMultiplayerStatus: "none" | "supported" | "unknown" | null benchmarkCount: number deckStatus: string | null antiCheatRelevant: boolean antiCheatStatus: "none" | "supported" | "unsupported" | "unknown" | null } interface DeviceOption { slug: string name: string } type SortOption = "recent" | "name" | "benchmarks" | "performance" | "popularity" | "release_date" | "steam_reviews" type SortDirection = "asc" | "desc" const DECK_STATUS_CONFIG: Record = { native: { label: "Native", className: "bg-green-500/10 border-green-500/20 text-green-400" }, proton: { label: "Proton", className: "bg-blue-500/10 border-blue-500/20 text-blue-400" }, unsupported: { label: "Unsupported", className: "bg-red-500/10 border-red-500/20 text-red-400" }, unknown: { label: "Unknown", className: "bg-text/5 border-border text-text/40" }, } const SORT_OPTIONS: { value: SortOption; label: string }[] = [ { value: "recent", label: "Recently Added" }, { value: "name", label: "Name A–Z" }, { value: "benchmarks", label: "Most Benchmarks" }, { value: "performance", label: "Best Performance" }, { value: "popularity", label: "Most Popular" }, { value: "release_date", label: "Release Date" }, { value: "steam_reviews", label: "Steam Reviews" }, ] export function GamesPageClient({ initialGames, totalCount, allGenres, allDevices, }: { initialGames: GamesListItem[] totalCount: number allGenres: string[] allDevices: DeviceOption[] }) { const [games, setGames] = useState(initialGames) const [total, setTotal] = useState(totalCount) const [search, setSearch] = useState("") const [selectedGenres, setSelectedGenres] = useState([]) const [selectedDevice, setSelectedDevice] = useState("") const [sort, setSort] = useState("recent") const [sortDirection, setSortDirection] = useState("desc") const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [showFilters, setShowFilters] = useState(false) const [minFps, setMinFps] = useState("") const [maxFps, setMaxFps] = useState("") const [fsrSupport, setFsrSupport] = useState(false) const [protonNative, setProtonNative] = useState("any") const [antiCheatStatus, setAntiCheatStatus] = useState("any") const [playabilityStatus, setPlayabilityStatus] = useState("") const [steamReviewMin, setSteamReviewMin] = useState("") const [isFree, setIsFree] = useState(false) const [hasMultiplayer, setHasMultiplayer] = useState(false) const observerRef = useRef(null) const sentinelRef = useRef(null) const pageRef = useRef(null) const { isGamepadActive } = useGamepadNavigation(pageRef, { onXButton: () => { // Navigate to search page window.location.href = "/search" }, onYButton: () => { // Toggle filter panel setShowFilters((prev) => !prev) }, }) const router = useRouter() const hasMore = games.length < total // ── Initialize filters from URL params on mount ──────────────── /* eslint-disable react-hooks/set-state-in-effect */ useEffect(() => { const params = new URLSearchParams(window.location.search) if (params.get("search")) setSearch(params.get("search")!) if (params.get("device")) setSelectedDevice(params.get("device")!) if (params.get("genre")) { const genres = params.get("genre")!.split(",").filter(Boolean) setSelectedGenres(genres) } if (params.get("minFps")) setMinFps(params.get("minFps")!) if (params.get("maxFps")) setMaxFps(params.get("maxFps")!) if (params.get("fsrSupport") === "true") setFsrSupport(true) if (params.get("protonNative") && params.get("protonNative") !== "any") setProtonNative(params.get("protonNative")!) if (params.get("antiCheatStatus") && params.get("antiCheatStatus") !== "any") setAntiCheatStatus(params.get("antiCheatStatus")!) if (params.get("playabilityStatus")) setPlayabilityStatus(params.get("playabilityStatus")!) if (params.get("steamReviewScore")) setSteamReviewMin(params.get("steamReviewScore")!) if (params.get("isFree") === "true") setIsFree(true) if (params.get("hasMultiplayer") === "true") setHasMultiplayer(true) if (params.get("sort")) { const s = params.get("sort")! if (["recent", "name", "benchmarks", "performance", "popularity", "release_date", "steam_reviews"].includes(s)) { setSort(s as SortOption) } } if (params.get("order")) setSortDirection(params.get("order") as SortDirection) }, []) /* eslint-enable react-hooks/set-state-in-effect */ const buildUrl = useCallback( (offset: number) => { const params = new URLSearchParams() params.set("offset", String(offset)) params.set("limit", "24") params.set("sort", sort) params.set("order", sortDirection) if (search) params.set("search", search) if (selectedDevice) params.set("device", selectedDevice) if (selectedGenres.length > 0) params.set("genre", selectedGenres.join(",")) if (minFps) params.set("minFps", minFps) if (maxFps) params.set("maxFps", maxFps) if (fsrSupport) params.set("fsrSupport", "true") if (protonNative !== "any") params.set("protonNative", protonNative) if (antiCheatStatus !== "any") params.set("antiCheatStatus", antiCheatStatus) if (playabilityStatus) params.set("playabilityStatus", playabilityStatus) if (steamReviewMin) params.set("steamReviewScore", steamReviewMin) if (isFree) params.set("isFree", "true") if (hasMultiplayer) params.set("hasMultiplayer", "true") return `/api/games/listing?${params.toString()}` }, [ sort, sortDirection, search, selectedDevice, selectedGenres, minFps, maxFps, fsrSupport, protonNative, antiCheatStatus, playabilityStatus, steamReviewMin, isFree, hasMultiplayer, ], ) // Load more function for infinite scroll const loadMore = useCallback(async () => { if (loading || !hasMore) return setLoading(true) setError(null) try { const url = buildUrl(games.length) const res = await fetch(url) if (!res.ok) throw new Error("Failed to load games") const data = await res.json() setGames((prev) => [...prev, ...data.data]) setTotal(data.total) } catch (err) { setError("Failed to load more games. Please try again.") console.error(err) } finally { setLoading(false) } }, [loading, hasMore, games.length, buildUrl]) // ── Sync filter state to URL (replace, not push) ─────────────── useEffect(() => { const params = new URLSearchParams() if (search) params.set("search", search) if (selectedDevice) params.set("device", selectedDevice) if (selectedGenres.length > 0) params.set("genre", selectedGenres.join(",")) if (minFps) params.set("minFps", minFps) if (maxFps) params.set("maxFps", maxFps) if (fsrSupport) params.set("fsrSupport", "true") if (protonNative !== "any") params.set("protonNative", protonNative) if (antiCheatStatus !== "any") params.set("antiCheatStatus", antiCheatStatus) if (playabilityStatus) params.set("playabilityStatus", playabilityStatus) if (steamReviewMin) params.set("steamReviewScore", steamReviewMin) if (isFree) params.set("isFree", "true") if (hasMultiplayer) params.set("hasMultiplayer", "true") if (sort !== "recent") params.set("sort", sort) if (sortDirection !== "desc") params.set("order", sortDirection) const qs = params.toString() const url = qs ? `/games?${qs}` : "/games" router.replace(url, { scroll: false }) }, [search, selectedDevice, selectedGenres, minFps, maxFps, fsrSupport, protonNative, antiCheatStatus, playabilityStatus, steamReviewMin, isFree, hasMultiplayer, sort, sortDirection, router]) // Full reload when filters/sort change useEffect(() => { let cancelled = false async function fetchGames() { setLoading(true) setError(null) try { const url = buildUrl(0) const res = await fetch(url) if (!res.ok) throw new Error("Failed to load games") const data = await res.json() if (!cancelled) { setGames(data.data) setTotal(data.total) } } catch (err) { if (!cancelled) setError("Failed to load games. Please try again.") console.error(err) } finally { if (!cancelled) setLoading(false) } } fetchGames() return () => { cancelled = true } }, [buildUrl]) // IntersectionObserver for infinite scroll useEffect(() => { if (observerRef.current) observerRef.current.disconnect() const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasMore && !loading) { loadMore() } }, { rootMargin: "200px" }, ) if (sentinelRef.current) { observer.observe(sentinelRef.current) } observerRef.current = observer return () => observer.disconnect() }, [hasMore, loading, loadMore]) const toggleGenre = (genre: string) => { setSelectedGenres((prev) => prev.includes(genre) ? prev.filter((g) => g !== genre) : [...prev, genre], ) } return (
{/* Header */}

Games

Browse {total.toLocaleString()} games with benchmarks, settings, and performance data

{/* Search & Filter Bar */}
{/* Search + Sort Row */}
{/* Filter Panel (collapsible) */} {showFilters && ( {/* Device filter */}
Device
{allDevices.map((device) => ( ))}
{/* Genre filter */}
Genre
{allGenres.map((genre) => ( ))}
{/* Performance Filters */}

Performance

setMinFps(e.target.value)} className="w-24 rounded-md border border-zinc-700 bg-zinc-800 px-2 py-2.5 text-sm min-h-[44px]" /> setMaxFps(e.target.value)} className="w-24 rounded-md border border-zinc-700 bg-zinc-800 px-2 py-2.5 text-sm min-h-[44px]" />
{/* Compatibility Filters */}

Compatibility

{/* Other Filters */}

Other

setSteamReviewMin(e.target.value)} className="w-full rounded-md border border-zinc-700 bg-zinc-800 px-2 py-1 text-sm" />
{/* Saved Filters */} { setMinFps((filters.minFps as string) || "") setMaxFps((filters.maxFps as string) || "") setFsrSupport((filters.fsrSupport as boolean) || false) setProtonNative((filters.protonNative as string) || "any") setAntiCheatStatus((filters.antiCheatStatus as string) || "any") setPlayabilityStatus((filters.playabilityStatus as string) || "") setSteamReviewMin((filters.steamReviewMin as string) || "") setIsFree((filters.isFree as boolean) || false) setHasMultiplayer((filters.hasMultiplayer as boolean) || false) if (filters.genre) setSelectedGenres((filters.genre as string).split(",").filter(Boolean)) else setSelectedGenres([]) if (filters.device) setSelectedDevice(filters.device as string) else setSelectedDevice("") if (filters.sortBy) setSort(filters.sortBy as SortOption) setShowFilters(false) }} /> {/* Clear filters */} {(selectedDevice || selectedGenres.length > 0 || minFps || maxFps || fsrSupport || protonNative !== "any" || antiCheatStatus !== "any" || playabilityStatus || steamReviewMin || isFree || hasMultiplayer) && ( )}
)}
{/* Error State */} {error && !loading && (

{error}

)} {/* Games Grid */}
{games.map((game) => ( ))}
{/* Empty state */} {!loading && games.length === 0 && !error && (

{search || selectedDevice || selectedGenres.length > 0 || minFps || maxFps || fsrSupport || protonNative !== "any" || antiCheatStatus !== "any" || playabilityStatus || steamReviewMin || isFree || hasMultiplayer ? "No games match your filters" : "No games found"}

{(search || selectedDevice || selectedGenres.length > 0 || minFps || maxFps || fsrSupport || protonNative !== "any" || antiCheatStatus !== "any" || playabilityStatus || steamReviewMin || isFree || hasMultiplayer) && ( )}
)}
{/* Loading indicator for infinite scroll */} {loading && (
)} {/* End of list */} {!loading && !hasMore && games.length > 0 && (

Showing all {games.length} of {total.toLocaleString()} games

)} {/* Infinite scroll sentinel */} {hasMore && !loading &&
}
) } function GameCard({ game }: { game: GamesListItem }) { const [imgError, setImgError] = useState(false) const imageUrl = game.capsuleImage || game.headerImage const deckConfig = game.deckStatus ? DECK_STATUS_CONFIG[game.deckStatus] ?? DECK_STATUS_CONFIG.unknown : null return (
{imageUrl && !imgError ? ( {game.title} setImgError(true)} /> ) : (
)}

{game.title}

{game.playabilityStatus && ( )} {game.antiCheatRelevant && game.antiCheatStatus === "unsupported" && ( )} {game.steamReviewScore != null && ( {game.steamReviewScore}% )}
{game.benchmarkCount > 0 ? ( {game.benchmarkCount} ) : ( No data yet )} {deckConfig && ( {deckConfig.label} )}
) }