diff --git a/app/page.tsx b/app/page.tsx index 0b83a70..a7e67c3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,170 +4,196 @@ 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 { + 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 + 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[] + recentBenchmarks: GameCard[] + trending: GameCard[] + mostTested: GameCard[] } function SkeletonSections() { - return ( - <> - {[1, 2, 3].map((i) => ( -
-
-
- {[1, 2, 3, 4].map((j) => ( -
-
-
-
-
+ 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, + 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 + title: string + icon: React.ElementType + games: GameCard[] + statKey: string + statLabel: string + statFormatter?: (v: unknown) => string + accentColor?: string + muted?: boolean }) { - const router = useRouter() + 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}` - } + 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} - ) : ( -
- + viewport={{ once: true, margin: "-50px" }} + transition={{ duration: 0.4 }} + className={muted ? "opacity-70" : ""} + > +
+
+
+ +

+ {title} +

+
- )} - {game.playability_status && game.playability_status !== "unknown" && ( -
- -
- )}
-
-

- {game.title} -

-

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

+
+ {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], + )} +

+
+
+ ))}
- - ))} -
- - ) + + ) } export default function Landing() { @@ -179,48 +205,54 @@ export default function Landing() { // Landing section state const [sections, setSections] = useState({ - trending: [], - bestNewReleases: [], - mostTested: [], - mostReported: [], + recentBenchmarks: [], + trending: [], + mostTested: [], }) 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) + 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) + let cancelled = false + async function fetchSections() { + try { + const [recentBenchmarks, trending, mostTested] = + 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() : [], + ), + ]) + if (!cancelled) { + setSections({ + recentBenchmarks: Array.isArray(recentBenchmarks) ? recentBenchmarks : [], + trending: Array.isArray(trending) ? trending : [], + mostTested: Array.isArray(mostTested) ? mostTested : [], + }) + } + } catch { + // Silently fail — sections are best-effort + } finally { + if (!cancelled) setSectionsLoading(false) + } + } + fetchSections() + return () => { + cancelled = true } - } - fetchSections() - return () => { cancelled = true } }, []) const handleSearchSubmit = () => { @@ -237,180 +269,170 @@ export default function Landing() { } return ( - <> - {/* ── Hero Section ── */} -
- + {/* ── 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' - /> - + beta + + + + {"Find your game".split(" ").map((word, index) => ( + + {word} + + ))} + - - search - - - - - + {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. - + See what{"'"}s new. + + + - Github. - - v{process.env.NEXT_PUBLIC_APP_VERSION} - -
+ 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 && ( - - )} - - )} -
+ {/* ── Landing Sections ── */} +
+ {sectionsLoading ? ( + + ) : ( + <> + {sections.recentBenchmarks.length > 0 && ( + + )} + {sections.trending.length > 0 && ( + + )} + {sections.mostTested.length > 0 && ( + + )} + + )} +
-