"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, GaugeIcon, ClockIcon, } from "lucide-react" import { useRouter } from "next/navigation" import { PlayabilityBadge } from "@/components/playability-badge" import { cn } from "@/lib/utils" 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 { recentBenchmarks: GameCard[] trending: GameCard[] mostTested: GameCard[] onSale: SaleGameCard[] } interface SaleGameCard extends GameCard { price_current?: number price_initial?: number price_currency?: string steam_review_score?: number best_fps?: number } function SkeletonSections() { return ( <> {[1, 2, 3].map((i) => (
{[1, 2, 3, 4].map((j) => (
))}
))} ) } 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} ) : (
)} {/* Playability badge pinned at bottom of image */} {game.playability_status && game.playability_status !== "unknown" && (
)}

{game.title}

{/* Performance badges */} {game.avg_fps !== undefined && game.avg_fps !== null && (
{game.avg_fps >= 60 ? ( ⚡ RAW PERFORMER ) : game.avg_fps < 30 ? ( ⚠ POOR PERFORMANCE ) : null} {Math.round(game.avg_fps)}fps
)}

{formatStat( ( game as unknown as Record< string, unknown > )[statKey], )}

))}
) } function SaleSection({ games, }: { games: SaleGameCard[] }) { const router = useRouter() return (
💰

On Sale & Performing Well

{games.map((game, idx) => { const discountPct = game.price_initial && game.price_current ? Math.round((1 - game.price_current / game.price_initial) * 100) : 0 return ( router.push(`/game/${game.id}?sync=1`)} >
{game.capsule_image ? ( {game.title} ) : (
)} {/* Discount badge */} {discountPct > 0 && (
-{discountPct}%
)} {game.playability_status && game.playability_status !== "unknown" && (
)}

{game.title}

{/* Pricing */}
{game.price_current !== undefined && ( {game.price_currency === "USD" ? "$" : ""}{(game.price_current / 100).toFixed(2)} )} {game.price_initial !== undefined && game.price_initial > (game.price_current ?? 0) && ( {(game.price_initial / 100).toFixed(2)} )}
{/* Performance badges */} {game.best_fps !== undefined && game.best_fps !== null && (
{game.best_fps >= 60 ? ( ⚡ RAW PERFORMER ) : game.best_fps < 30 ? ( ⚠ POOR PERFORMANCE ) : null} {Math.round(game.best_fps)}fps
)} {game.steam_review_score !== undefined && game.steam_review_score !== null && (

{game.steam_review_score}% positive

)}
) })}
{/* Disclaimer */}

Prices may vary. Data refreshes weekly.

) } 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({ recentBenchmarks: [], trending: [], mostTested: [], onSale: [], }) 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 [recentBenchmarks, trending, mostTested, onSale] = await Promise.all([ fetch("/api/dashboard/recent-benchmarks").then((r) => r.ok ? r.json() : [], ), fetch("/api/dashboard/trending").then((r) => r.ok ? r.json() : [], ), fetch("/api/dashboard/most-tested").then((r) => r.ok ? r.json() : [], ), fetch("/api/dashboard/on-sale").then((r) => r.ok ? r.json() : [], ), ]) if (!cancelled) { setSections({ recentBenchmarks: Array.isArray(recentBenchmarks) ? recentBenchmarks : [], trending: Array.isArray(trending) ? trending : [], mostTested: Array.isArray(mostTested) ? mostTested : [], onSale: Array.isArray(onSale) ? onSale : [], }) } } 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.recentBenchmarks.length > 0 && ( )} {sections.trending.length > 0 && ( )} {sections.mostTested.length > 0 && ( )} {sections.onSale.length > 0 && ( )} )}