"use client" import { AnimatePresence, motion } from "motion/react" import { useState, useEffect } from "react" import Link from "next/link" import Image from "next/image" import { Gamepad2Icon, SearchIcon, TrendingUpIcon, SparklesIcon, GaugeIcon, FlagIcon } from "lucide-react" import { useRouter } from "next/navigation" interface GameCard { id: string title: string capsule_image: string | null header_image: string | null playability_status?: string | null activity_score?: number benchmark_count?: number comment_count?: number upvote_count?: number avg_fps?: number report_count?: number release_date?: string | null created_at?: string | null } interface SectionData { trending: GameCard[] bestNewReleases: GameCard[] mostTested: GameCard[] mostReported: GameCard[] } function SkeletonSections() { return ( <> {[1, 2, 3].map((i) => (
{[1, 2, 3, 4].map((j) => (
))}
))} ) } function PlayabilityDot({ status }: { status: string }) { const colors: Record = { great: "bg-green-500", playable: "bg-blue-500", needs_tweaks: "bg-yellow-500", unplayable: "bg-red-500", } const labels: Record = { great: "Plays Great", playable: "Playable", needs_tweaks: "Needs Tweaks", unplayable: "Unplayable", } return ( ) } function GameSection({ title, icon: Icon, games, statKey, statLabel, statFormatter, accentColor = "text-text/50", muted = false, }: { title: string icon: React.ElementType games: GameCard[] statKey: string statLabel: string statFormatter?: (v: unknown) => string accentColor?: string muted?: boolean }) { const router = useRouter() const formatStat = (v: unknown): string => { if (statFormatter) return statFormatter(v) if (typeof v === "number") return `${Math.round(v)} ${statLabel}` return `${v} ${statLabel}` } return (

{title}

{games.map((game, idx) => ( router.push(`/game/${game.id}?sync=1`)} >
{game.capsule_image ? ( {game.title} ) : (
)} {game.playability_status && game.playability_status !== "unknown" && (
)}

{game.title}

{formatStat((game as unknown as Record)[statKey])}

))}
) } export default function Landing() { const router = useRouter() const words = ["benchmarks", "settings", "reviews"] const [currentWord, setCurrentWord] = useState(0) const [searchQuery, setSearchQuery] = useState("") // Landing section state const [sections, setSections] = useState({ trending: [], bestNewReleases: [], mostTested: [], mostReported: [], }) const [sectionsLoading, setSectionsLoading] = useState(true) // Animated words cycle — use useEffect with proper cleanup useEffect(() => { const interval = setInterval(() => { setCurrentWord((prev) => (prev + 1) % words.length) }, 2000) return () => clearInterval(interval) }, [words.length]) // Fetch all 4 sections in parallel on mount useEffect(() => { let cancelled = false async function fetchSections() { try { const [trending, bestNew, mostTested, mostReported] = await Promise.all([ fetch("/api/dashboard/trending").then(r => r.ok ? r.json() : []), fetch("/api/dashboard/best-new-releases").then(r => r.ok ? r.json() : []), fetch("/api/dashboard/most-tested").then(r => r.ok ? r.json() : []), fetch("/api/dashboard/most-reported").then(r => r.ok ? r.json() : []), ]) if (!cancelled) { setSections({ trending: Array.isArray(trending) ? trending : [], bestNewReleases: Array.isArray(bestNew) ? bestNew : [], mostTested: Array.isArray(mostTested) ? mostTested : [], mostReported: Array.isArray(mostReported) ? mostReported : [], }) } } catch { // Silently fail — sections are best-effort } finally { if (!cancelled) setSectionsLoading(false) } } fetchSections() return () => { cancelled = true } }, []) const handleSearchSubmit = () => { if (searchQuery.trim()) { sessionStorage.setItem("focusSearch", "true") router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`) } } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { handleSearchSubmit() } } return ( <> {/* ── Hero Section ── */}
DeckyVault beta {"Find your game".split(" ").map((word, index) => ( {word} ))} {words[currentWord]} setSearchQuery(e.target.value)} onKeyDown={handleKeyDown} className='flex-1 outline-none bg-transparent text-xl min-w-0' /> search See what{"'"}s new. 2026 DeckyVault. Github. v{process.env.NEXT_PUBLIC_APP_VERSION}
{/* ── Landing Sections ── */}
{sectionsLoading ? ( ) : ( <> {sections.trending.length > 0 && ( )} {sections.bestNewReleases.length > 0 && ( `${Math.round(Number(v))} FPS`} accentColor="text-green-400" /> )} {sections.mostTested.length > 0 && ( )} {sections.mostReported.length > 0 && ( )} )}