chore: fix tailwind v4 styling & format styling
This commit is contained in:
+296
-178
@@ -173,7 +173,8 @@ function getFpsColor(preset: Preset): string {
|
|||||||
if (preset.frameGenMethod === "dlss_fg") return "text-blue-400"
|
if (preset.frameGenMethod === "dlss_fg") return "text-blue-400"
|
||||||
return "text-orange-400"
|
return "text-orange-400"
|
||||||
}
|
}
|
||||||
if (preset.fsrVersion && preset.fsrVersion !== "none") return "text-orange-400"
|
if (preset.fsrVersion && preset.fsrVersion !== "none")
|
||||||
|
return "text-orange-400"
|
||||||
return "text-text/60"
|
return "text-text/60"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,8 +243,12 @@ export function GamePageClient({
|
|||||||
const filterUpscaler = (arr: StatsResponse["upscalerStats"]) =>
|
const filterUpscaler = (arr: StatsResponse["upscalerStats"]) =>
|
||||||
arr.filter((item) => {
|
arr.filter((item) => {
|
||||||
if (!deviceSet.has(item.hardwareSlug)) return false
|
if (!deviceSet.has(item.hardwareSlug)) return false
|
||||||
if (filters.fsr !== "all" && item.fsrVersion !== filters.fsr) return false
|
if (filters.fsr !== "all" && item.fsrVersion !== filters.fsr)
|
||||||
if (filters.frameGen !== "all" && item.frameGenMethod !== filters.frameGen)
|
return false
|
||||||
|
if (
|
||||||
|
filters.frameGen !== "all" &&
|
||||||
|
item.frameGenMethod !== filters.frameGen
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -254,7 +259,9 @@ export function GamePageClient({
|
|||||||
historical: stats.historical
|
historical: stats.historical
|
||||||
.map((h) => ({
|
.map((h) => ({
|
||||||
...h,
|
...h,
|
||||||
entries: h.entries.filter((e) => deviceSet.has(e.hardwareSlug)),
|
entries: h.entries.filter((e) =>
|
||||||
|
deviceSet.has(e.hardwareSlug),
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
.filter((h) => h.entries.length > 0),
|
.filter((h) => h.entries.length > 0),
|
||||||
upscalerStats: filterUpscaler(stats.upscalerStats),
|
upscalerStats: filterUpscaler(stats.upscalerStats),
|
||||||
@@ -267,12 +274,20 @@ export function GamePageClient({
|
|||||||
// Filtered presets
|
// Filtered presets
|
||||||
const filteredPresets = useMemo(() => {
|
const filteredPresets = useMemo(() => {
|
||||||
return presets.filter((p) => {
|
return presets.filter((p) => {
|
||||||
if (selectedDevices.length > 0 && !selectedDevices.includes(p.hardwareSlug))
|
if (
|
||||||
|
selectedDevices.length > 0 &&
|
||||||
|
!selectedDevices.includes(p.hardwareSlug)
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
if (filters.proton !== "all" && p.protonVersion !== filters.proton)
|
||||||
return false
|
return false
|
||||||
if (filters.proton !== "all" && p.protonVersion !== filters.proton) return false
|
|
||||||
if (filters.os !== "all" && p.osVersion !== filters.os) return false
|
if (filters.os !== "all" && p.osVersion !== filters.os) return false
|
||||||
if (filters.fsr !== "all" && p.fsrVersion !== filters.fsr) return false
|
if (filters.fsr !== "all" && p.fsrVersion !== filters.fsr)
|
||||||
if (filters.frameGen !== "all" && p.frameGenMethod !== filters.frameGen)
|
return false
|
||||||
|
if (
|
||||||
|
filters.frameGen !== "all" &&
|
||||||
|
p.frameGenMethod !== filters.frameGen
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -289,64 +304,66 @@ export function GamePageClient({
|
|||||||
|
|
||||||
const toggleDevice = useCallback((slug: string) => {
|
const toggleDevice = useCallback((slug: string) => {
|
||||||
setSelectedDevices((prev) =>
|
setSelectedDevices((prev) =>
|
||||||
prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug]
|
prev.includes(slug)
|
||||||
|
? prev.filter((s) => s !== slug)
|
||||||
|
: [...prev, slug],
|
||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full flex flex-col gap-8 pb-16">
|
<section className='w-full flex flex-col gap-8 pb-16'>
|
||||||
{/* Section 1: Hero Header */}
|
{/* Section 1: Hero Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.4 }}
|
transition={{ duration: 0.4 }}
|
||||||
className="px-4 md:px-[10svw] pt-6"
|
className='px-4 md:px-[10svw] pt-6'
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto flex gap-4 sm:gap-6">
|
<div className='max-w-7xl mx-auto flex gap-4 sm:gap-6'>
|
||||||
{/* Cover image */}
|
{/* Cover image */}
|
||||||
<div className="relative shrink-0 aspect-[2/3] w-28 sm:w-32 md:w-36 rounded-lg overflow-hidden border border-border bg-text/5">
|
<div className='relative shrink-0 aspect-2/3 w-28 sm:w-32 md:w-36 rounded-lg overflow-hidden border border-border bg-text/5'>
|
||||||
{coverImage && !imgError ? (
|
{coverImage && !imgError ? (
|
||||||
<Image
|
<Image
|
||||||
src={coverImage}
|
src={coverImage}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className='object-cover'
|
||||||
priority
|
priority
|
||||||
onError={handleImgError}
|
onError={handleImgError}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
<div className='w-full h-full flex items-center justify-center'>
|
||||||
<Gamepad2Icon className="h-10 w-10 text-text/20" />
|
<Gamepad2Icon className='h-10 w-10 text-text/20' />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info */}
|
{/* Info */}
|
||||||
<div className="flex flex-col gap-2 min-w-0">
|
<div className='flex flex-col gap-2 min-w-0'>
|
||||||
{/* Title row */}
|
{/* Title row */}
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold truncate">
|
<h1 className='text-xl sm:text-2xl md:text-3xl font-bold truncate'>
|
||||||
{game.title}
|
{game.title}
|
||||||
</h1>
|
</h1>
|
||||||
{stats?.isRawPerformer && (
|
{stats?.isRawPerformer && (
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-500/10 border border-green-500/20 text-green-400 text-[10px] font-semibold">
|
<span className='inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-500/10 border border-green-500/20 text-green-400 text-[10px] font-semibold'>
|
||||||
<SparklesIcon className="h-3 w-3" />
|
<SparklesIcon className='h-3 w-3' />
|
||||||
Raw Performer
|
Raw Performer
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subtitle */}
|
{/* Subtitle */}
|
||||||
<div className="flex flex-wrap items-center gap-2 text-sm text-text/70">
|
<div className='flex flex-wrap items-center gap-2 text-sm text-text/70'>
|
||||||
{game.developer && <span>{game.developer}</span>}
|
{game.developer && <span>{game.developer}</span>}
|
||||||
{game.developer && game.publisher && (
|
{game.developer && game.publisher && (
|
||||||
<span className="text-text/40">·</span>
|
<span className='text-text/40'>·</span>
|
||||||
)}
|
)}
|
||||||
{game.publisher && <span>{game.publisher}</span>}
|
{game.publisher && <span>{game.publisher}</span>}
|
||||||
{game.genres && game.genres.length > 0 && (
|
{game.genres && game.genres.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<span className="text-text/40">·</span>
|
<span className='text-text/40'>·</span>
|
||||||
<span className="text-text/60">
|
<span className='text-text/60'>
|
||||||
{game.genres.slice(0, 3).join(", ")}
|
{game.genres.slice(0, 3).join(", ")}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
@@ -354,12 +371,25 @@ export function GamePageClient({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats badges */}
|
{/* Stats badges */}
|
||||||
<div className="flex flex-wrap items-center gap-2 mt-1">
|
<div className='flex flex-wrap items-center gap-2 mt-1'>
|
||||||
<Badge icon={TrendingUpIcon} value={counts.benchmarks} label="Benchmarks" />
|
<Badge
|
||||||
<Badge icon={SettingsIcon} value={counts.presets} label="Presets" />
|
icon={TrendingUpIcon}
|
||||||
<Badge icon={MessageSquareIcon} value={counts.comments} label="Comments" />
|
value={counts.benchmarks}
|
||||||
{game.metascore !== undefined && game.metascore !== null && (
|
label='Benchmarks'
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-yellow-500/10 border border-yellow-500/20 text-yellow-400 text-[10px] font-semibold">
|
/>
|
||||||
|
<Badge
|
||||||
|
icon={SettingsIcon}
|
||||||
|
value={counts.presets}
|
||||||
|
label='Presets'
|
||||||
|
/>
|
||||||
|
<Badge
|
||||||
|
icon={MessageSquareIcon}
|
||||||
|
value={counts.comments}
|
||||||
|
label='Comments'
|
||||||
|
/>
|
||||||
|
{game.metascore !== undefined &&
|
||||||
|
game.metascore !== null && (
|
||||||
|
<span className='inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-yellow-500/10 border border-yellow-500/20 text-yellow-400 text-[10px] font-semibold'>
|
||||||
★ {game.metascore}
|
★ {game.metascore}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -367,70 +397,71 @@ export function GamePageClient({
|
|||||||
{session && (
|
{session && (
|
||||||
<Link
|
<Link
|
||||||
href={`/game/${game.id}/submit`}
|
href={`/game/${game.id}/submit`}
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-white text-sm font-semibold hover:bg-primary/90 transition-colors cursor-pointer"
|
className='inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-white text-sm font-semibold hover:bg-primary/90 transition-colors cursor-pointer'
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className='h-4 w-4' />
|
||||||
Add Benchmark
|
Add Benchmark
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* External links */}
|
{/* External links */}
|
||||||
<div className="flex flex-wrap items-center gap-3 mt-1">
|
<div className='flex flex-wrap items-center gap-3 mt-1'>
|
||||||
{game.storeUrl && (
|
{game.storeUrl && (
|
||||||
<a
|
<a
|
||||||
href={game.storeUrl}
|
href={game.storeUrl}
|
||||||
target="_blank"
|
target='_blank'
|
||||||
rel="noopener noreferrer"
|
rel='noopener noreferrer'
|
||||||
className="inline-flex items-center gap-1 text-xs text-text/60 hover:text-primary transition-colors cursor-pointer"
|
className='inline-flex items-center gap-1 text-xs text-text/60 hover:text-primary transition-colors cursor-pointer'
|
||||||
>
|
>
|
||||||
<FaSteam className="h-3.5 w-3.5" />
|
<FaSteam className='h-3.5 w-3.5' />
|
||||||
Steam
|
Steam
|
||||||
<ExternalLinkIcon className="h-3 w-3" />
|
<ExternalLinkIcon className='h-3 w-3' />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{protonDbUrl && (
|
{protonDbUrl && (
|
||||||
<a
|
<a
|
||||||
href={protonDbUrl}
|
href={protonDbUrl}
|
||||||
target="_blank"
|
target='_blank'
|
||||||
rel="noopener noreferrer"
|
rel='noopener noreferrer'
|
||||||
className="inline-flex items-center gap-1 text-xs text-text/60 hover:text-primary transition-colors cursor-pointer"
|
className='inline-flex items-center gap-1 text-xs text-text/60 hover:text-primary transition-colors cursor-pointer'
|
||||||
>
|
>
|
||||||
ProtonDB
|
ProtonDB
|
||||||
<ExternalLinkIcon className="h-3 w-3" />
|
<ExternalLinkIcon className='h-3 w-3' />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{steamDbUrl && (
|
{steamDbUrl && (
|
||||||
<a
|
<a
|
||||||
href={steamDbUrl}
|
href={steamDbUrl}
|
||||||
target="_blank"
|
target='_blank'
|
||||||
rel="noopener noreferrer"
|
rel='noopener noreferrer'
|
||||||
className="inline-flex items-center gap-1 text-xs text-text/60 hover:text-primary transition-colors cursor-pointer"
|
className='inline-flex items-center gap-1 text-xs text-text/60 hover:text-primary transition-colors cursor-pointer'
|
||||||
>
|
>
|
||||||
SteamDB
|
SteamDB
|
||||||
<ExternalLinkIcon className="h-3 w-3" />
|
<ExternalLinkIcon className='h-3 w-3' />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* DB stats row */}
|
{/* DB stats row */}
|
||||||
{stats && (
|
{stats && (
|
||||||
<div className="flex flex-wrap items-center gap-3 mt-1 text-xs text-text/50">
|
<div className='flex flex-wrap items-center gap-3 mt-1 text-xs text-text/50'>
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className='inline-flex items-center gap-1'>
|
||||||
<DatabaseIcon className="h-3 w-3" />
|
<DatabaseIcon className='h-3 w-3' />
|
||||||
{stats.summary.versionCount} versions
|
{stats.summary.versionCount} versions
|
||||||
</span>
|
</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className='inline-flex items-center gap-1'>
|
||||||
<SparklesIcon className="h-3 w-3" />
|
<SparklesIcon className='h-3 w-3' />
|
||||||
{stats.summary.verifiedCount} verified
|
{stats.summary.verifiedCount} verified
|
||||||
</span>
|
</span>
|
||||||
{game.lastSync && (
|
{game.lastSync && (
|
||||||
<>
|
<>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className='inline-flex items-center gap-1'>
|
||||||
<ClockIcon className="h-3 w-3" />
|
<ClockIcon className='h-3 w-3' />
|
||||||
Last sync {formatDate(game.lastSync)}
|
Last sync{" "}
|
||||||
|
{formatDate(game.lastSync)}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -445,38 +476,52 @@ export function GamePageClient({
|
|||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.4, delay: 0.1 }}
|
transition={{ duration: 0.4, delay: 0.1 }}
|
||||||
className="px-4 md:px-[10svw]"
|
className='px-4 md:px-[10svw]'
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto flex flex-col md:flex-row gap-6">
|
<div className='max-w-7xl mx-auto flex flex-col md:flex-row gap-6'>
|
||||||
{/* Left: Description */}
|
{/* Left: Description */}
|
||||||
<div className="flex-[2]">
|
<div className='flex-2'>
|
||||||
<h2 className="text-sm font-medium uppercase tracking-wider text-text/60 mb-3">
|
<h2 className='text-sm font-medium uppercase tracking-wider text-text/60 mb-3'>
|
||||||
About
|
About
|
||||||
</h2>
|
</h2>
|
||||||
{game.description ? (
|
{game.description ? (
|
||||||
<p className="text-sm text-text/80 leading-relaxed whitespace-pre-line">
|
<p className='text-sm text-text/80 leading-relaxed whitespace-pre-line'>
|
||||||
{game.description}
|
{game.description}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-text/40 italic">No description available.</p>
|
<p className='text-sm text-text/40 italic'>
|
||||||
|
No description available.
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: Metadata grid */}
|
{/* Right: Metadata grid */}
|
||||||
<div className="flex-1">
|
<div className='flex-1'>
|
||||||
<h2 className="text-sm font-medium uppercase tracking-wider text-text/60 mb-3">
|
<h2 className='text-sm font-medium uppercase tracking-wider text-text/60 mb-3'>
|
||||||
Details
|
Details
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
<div className='grid grid-cols-2 gap-3 text-sm'>
|
||||||
<MetaItem label="Source" value={game.source} />
|
<MetaItem
|
||||||
|
label='Source'
|
||||||
|
value={game.source}
|
||||||
|
/>
|
||||||
{game.steamAppId !== null && (
|
{game.steamAppId !== null && (
|
||||||
<MetaItem label="Steam AppID" value={String(game.steamAppId)} />
|
<MetaItem
|
||||||
|
label='Steam AppID'
|
||||||
|
value={String(game.steamAppId)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<MetaItem label="Added" value={formatDate(game.createdAt)} />
|
<MetaItem
|
||||||
<MetaItem label="Last Sync" value={formatDate(game.lastSync)} />
|
label='Added'
|
||||||
|
value={formatDate(game.createdAt)}
|
||||||
|
/>
|
||||||
|
<MetaItem
|
||||||
|
label='Last Sync'
|
||||||
|
value={formatDate(game.lastSync)}
|
||||||
|
/>
|
||||||
{game.onlineMultiplayerStatus && (
|
{game.onlineMultiplayerStatus && (
|
||||||
<MetaItem
|
<MetaItem
|
||||||
label="Online Multiplayer"
|
label='Online Multiplayer'
|
||||||
value={game.onlineMultiplayerStatus}
|
value={game.onlineMultiplayerStatus}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -484,20 +529,23 @@ export function GamePageClient({
|
|||||||
|
|
||||||
{/* Platform Support */}
|
{/* Platform Support */}
|
||||||
{platformSupport.length > 0 && (
|
{platformSupport.length > 0 && (
|
||||||
<div className="mt-4">
|
<div className='mt-4'>
|
||||||
<h3 className="text-xs font-medium uppercase tracking-wider text-text/50 mb-2">
|
<h3 className='text-xs font-medium uppercase tracking-wider text-text/50 mb-2'>
|
||||||
Platform Support
|
Platform Support
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-col gap-2">
|
<div className='flex flex-col gap-2'>
|
||||||
{platformSupport.map((ps) => (
|
{platformSupport.map((ps) => (
|
||||||
<div
|
<div
|
||||||
key={ps.id}
|
key={ps.id}
|
||||||
className="flex items-center justify-between p-2 rounded-md border border-border bg-text/[0.03]"
|
className='flex items-center justify-between p-2 rounded-md border border-border bg-text/3'
|
||||||
>
|
>
|
||||||
<span className="text-xs font-medium capitalize">
|
<span className='text-xs font-medium capitalize'>
|
||||||
{ps.hardwareSlug.replace(/-/g, " ")}
|
{ps.hardwareSlug.replace(
|
||||||
|
/-/g,
|
||||||
|
" ",
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<span
|
<span
|
||||||
className={`text-[10px] px-1.5 py-0.5 rounded-full ${
|
className={`text-[10px] px-1.5 py-0.5 rounded-full ${
|
||||||
ps.isSupported
|
ps.isSupported
|
||||||
@@ -505,9 +553,11 @@ export function GamePageClient({
|
|||||||
: "bg-red-500/20 text-red-400"
|
: "bg-red-500/20 text-red-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{ps.isSupported ? "Supported" : "Unsupported"}
|
{ps.isSupported
|
||||||
|
? "Supported"
|
||||||
|
: "Unsupported"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-text/50 capitalize">
|
<span className='text-[10px] text-text/50 capitalize'>
|
||||||
{ps.protonStatus}
|
{ps.protonStatus}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -525,18 +575,22 @@ export function GamePageClient({
|
|||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.4, delay: 0.15 }}
|
transition={{ duration: 0.4, delay: 0.15 }}
|
||||||
className="px-4 md:px-[10svw]"
|
className='px-4 md:px-[10svw]'
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto flex flex-col gap-4">
|
<div className='max-w-7xl mx-auto flex flex-col gap-4'>
|
||||||
{/* Device ribbon */}
|
{/* Device ribbon */}
|
||||||
{stats && stats.deviceBreakdown.length > 0 && (
|
{stats && stats.deviceBreakdown.length > 0 && (
|
||||||
<div className="flex items-center gap-2 overflow-x-auto pb-1 scrollbar-hide">
|
<div className='flex items-center gap-2 overflow-x-auto pb-1 scrollbar-hide'>
|
||||||
{stats.deviceBreakdown.map((device) => {
|
{stats.deviceBreakdown.map((device) => {
|
||||||
const active = selectedDevices.includes(device.hardwareSlug)
|
const active = selectedDevices.includes(
|
||||||
|
device.hardwareSlug,
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={device.hardwareSlug}
|
key={device.hardwareSlug}
|
||||||
onClick={() => toggleDevice(device.hardwareSlug)}
|
onClick={() =>
|
||||||
|
toggleDevice(device.hardwareSlug)
|
||||||
|
}
|
||||||
className={`shrink-0 px-3 py-1.5 rounded-full text-xs font-medium border transition-colors cursor-pointer ${
|
className={`shrink-0 px-3 py-1.5 rounded-full text-xs font-medium border transition-colors cursor-pointer ${
|
||||||
active
|
active
|
||||||
? "bg-primary/20 border-primary text-primary"
|
? "bg-primary/20 border-primary text-primary"
|
||||||
@@ -544,7 +598,9 @@ export function GamePageClient({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{device.hardwareName}
|
{device.hardwareName}
|
||||||
<span className="ml-1 text-text/40">({device.count})</span>
|
<span className='ml-1 text-text/40'>
|
||||||
|
({device.count})
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -552,32 +608,46 @@ export function GamePageClient({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Fine filters */}
|
{/* Fine filters */}
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
<FilterSelect
|
<FilterSelect
|
||||||
label="Proton Version"
|
label='Proton Version'
|
||||||
value={filters.proton}
|
value={filters.proton}
|
||||||
options={["all", ...(stats?.filterOptions.protonVersions ?? [])]}
|
options={[
|
||||||
onChange={(v) => setFilters((f) => ({ ...f, proton: v }))}
|
"all",
|
||||||
|
...(stats?.filterOptions.protonVersions ?? []),
|
||||||
|
]}
|
||||||
|
onChange={(v) =>
|
||||||
|
setFilters((f) => ({ ...f, proton: v }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<FilterSelect
|
<FilterSelect
|
||||||
label="SteamOS Version"
|
label='SteamOS Version'
|
||||||
value={filters.os}
|
value={filters.os}
|
||||||
options={["all", ...(stats?.filterOptions.osVersions ?? [])]}
|
options={[
|
||||||
onChange={(v) => setFilters((f) => ({ ...f, os: v }))}
|
"all",
|
||||||
|
...(stats?.filterOptions.osVersions ?? []),
|
||||||
|
]}
|
||||||
|
onChange={(v) =>
|
||||||
|
setFilters((f) => ({ ...f, os: v }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<FilterSelect
|
<FilterSelect
|
||||||
label="FSR Version"
|
label='FSR Version'
|
||||||
value={filters.fsr}
|
value={filters.fsr}
|
||||||
options={FSR_OPTIONS.map((o) => o.toLowerCase())}
|
options={FSR_OPTIONS.map((o) => o.toLowerCase())}
|
||||||
onChange={(v) => setFilters((f) => ({ ...f, fsr: v }))}
|
onChange={(v) =>
|
||||||
|
setFilters((f) => ({ ...f, fsr: v }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<FilterSelect
|
<FilterSelect
|
||||||
label="Frame Gen Method"
|
label='Frame Gen Method'
|
||||||
value={filters.frameGen}
|
value={filters.frameGen}
|
||||||
options={FRAMEGEN_OPTIONS.map((o) =>
|
options={FRAMEGEN_OPTIONS.map((o) =>
|
||||||
o.toLowerCase().replace(" ", "_")
|
o.toLowerCase().replace(" ", "_"),
|
||||||
)}
|
)}
|
||||||
onChange={(v) => setFilters((f) => ({ ...f, frameGen: v }))}
|
onChange={(v) =>
|
||||||
|
setFilters((f) => ({ ...f, frameGen: v }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -588,38 +658,48 @@ export function GamePageClient({
|
|||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.4, delay: 0.2 }}
|
transition={{ duration: 0.4, delay: 0.2 }}
|
||||||
className="px-4 md:px-[10svw]"
|
className='px-4 md:px-[10svw]'
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto flex flex-col gap-6">
|
<div className='max-w-7xl mx-auto flex flex-col gap-6'>
|
||||||
{/* Row 1 — Featured */}
|
{/* Row 1 — Featured */}
|
||||||
{filteredStats && (
|
{filteredStats && (
|
||||||
<div className="flex flex-col lg:flex-row gap-4">
|
<div className='flex flex-col lg:flex-row gap-4'>
|
||||||
<div className="flex-[3] rounded-xl border border-border bg-text/[0.03] p-4">
|
<div className='flex-3 rounded-xl border border-border bg-text/3 p-4'>
|
||||||
<h3 className="text-sm font-medium text-text/80 mb-2">
|
<h3 className='text-sm font-medium text-text/80 mb-2'>
|
||||||
Historical Performance
|
Historical Performance
|
||||||
</h3>
|
</h3>
|
||||||
{filteredStats.historical.length > 0 ? (
|
{filteredStats.historical.length > 0 ? (
|
||||||
<HistoricalAreaChart data={filteredStats.historical} />
|
<HistoricalAreaChart
|
||||||
|
data={filteredStats.historical}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-[280px] flex items-center justify-center text-sm text-text/40">
|
<div className='h-70 flex items-center justify-center text-sm text-text/40'>
|
||||||
No historical data
|
No historical data
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col gap-3">
|
<div className='flex-1 flex flex-col gap-3'>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Overall Avg FPS"
|
label='Overall Avg FPS'
|
||||||
value={filteredStats.summary.avgFps?.toFixed(1) ?? "—"}
|
value={
|
||||||
|
filteredStats.summary.avgFps?.toFixed(
|
||||||
|
1,
|
||||||
|
) ?? "—"
|
||||||
|
}
|
||||||
icon={TrendingUpIcon}
|
icon={TrendingUpIcon}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Best Device"
|
label='Best Device'
|
||||||
value={filteredStats.summary.bestDevice ?? "—"}
|
value={
|
||||||
|
filteredStats.summary.bestDevice ?? "—"
|
||||||
|
}
|
||||||
icon={Gamepad2Icon}
|
icon={Gamepad2Icon}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Total Entries"
|
label='Total Entries'
|
||||||
value={String(filteredStats.summary.totalEntries)}
|
value={String(
|
||||||
|
filteredStats.summary.totalEntries,
|
||||||
|
)}
|
||||||
icon={DatabaseIcon}
|
icon={DatabaseIcon}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -628,47 +708,54 @@ export function GamePageClient({
|
|||||||
|
|
||||||
{/* Loading state */}
|
{/* Loading state */}
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="flex items-center justify-center py-12 text-sm text-text/40">
|
<div className='flex items-center justify-center py-12 text-sm text-text/40'>
|
||||||
<ClockIcon className="h-4 w-4 animate-spin mr-2" />
|
<ClockIcon className='h-4 w-4 animate-spin mr-2' />
|
||||||
Loading statistics...
|
Loading statistics...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Row 2 — Upscaler Bar */}
|
{/* Row 2 — Upscaler Bar */}
|
||||||
{filteredStats && filteredStats.upscalerStats.length > 0 && (
|
{filteredStats &&
|
||||||
<div className="rounded-xl border border-border bg-text/[0.03] p-4">
|
filteredStats.upscalerStats.length > 0 && (
|
||||||
<h3 className="text-sm font-medium text-text/80 mb-2">
|
<div className='rounded-xl border border-border bg-text/3 p-4'>
|
||||||
|
<h3 className='text-sm font-medium text-text/80 mb-2'>
|
||||||
Upscaler Performance
|
Upscaler Performance
|
||||||
</h3>
|
</h3>
|
||||||
<UpscalerBarChart data={filteredStats.upscalerStats} />
|
<UpscalerBarChart
|
||||||
|
data={filteredStats.upscalerStats}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Row 3 — 3-column grid */}
|
{/* Row 3 — 3-column grid */}
|
||||||
{filteredStats && (
|
{filteredStats && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className='grid grid-cols-1 md:grid-cols-3 gap-4'>
|
||||||
{filteredStats.boxplot.length > 0 && (
|
{filteredStats.boxplot.length > 0 && (
|
||||||
<div className="rounded-xl border border-border bg-text/[0.03] p-4">
|
<div className='rounded-xl border border-border bg-text/3 p-4'>
|
||||||
<h3 className="text-sm font-medium text-text/80 mb-2">
|
<h3 className='text-sm font-medium text-text/80 mb-2'>
|
||||||
FPS Distribution
|
FPS Distribution
|
||||||
</h3>
|
</h3>
|
||||||
<FpsBoxplot data={filteredStats.boxplot} />
|
<FpsBoxplot data={filteredStats.boxplot} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{filteredStats.fpsRange.length > 0 && (
|
{filteredStats.fpsRange.length > 0 && (
|
||||||
<div className="rounded-xl border border-border bg-text/[0.03] p-4">
|
<div className='rounded-xl border border-border bg-text/3 p-4'>
|
||||||
<h3 className="text-sm font-medium text-text/80 mb-2">
|
<h3 className='text-sm font-medium text-text/80 mb-2'>
|
||||||
FPS Range
|
FPS Range
|
||||||
</h3>
|
</h3>
|
||||||
<FpsRangeChart data={filteredStats.fpsRange} />
|
<FpsRangeChart
|
||||||
|
data={filteredStats.fpsRange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{filteredStats.deviceBreakdown.length > 0 && (
|
{filteredStats.deviceBreakdown.length > 0 && (
|
||||||
<div className="rounded-xl border border-border bg-text/[0.03] p-4">
|
<div className='rounded-xl border border-border bg-text/3 p-4'>
|
||||||
<h3 className="text-sm font-medium text-text/80 mb-2">
|
<h3 className='text-sm font-medium text-text/80 mb-2'>
|
||||||
Device Breakdown
|
Device Breakdown
|
||||||
</h3>
|
</h3>
|
||||||
<DeviceDonut data={filteredStats.deviceBreakdown} />
|
<DeviceDonut
|
||||||
|
data={filteredStats.deviceBreakdown}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -676,8 +763,8 @@ export function GamePageClient({
|
|||||||
|
|
||||||
{/* Row 4 — Trust Bar */}
|
{/* Row 4 — Trust Bar */}
|
||||||
{filteredStats && filteredStats.trust.length > 0 && (
|
{filteredStats && filteredStats.trust.length > 0 && (
|
||||||
<div className="rounded-xl border border-border bg-text/[0.03] p-4">
|
<div className='rounded-xl border border-border bg-text/3 p-4'>
|
||||||
<h3 className="text-sm font-medium text-text/80 mb-2">
|
<h3 className='text-sm font-medium text-text/80 mb-2'>
|
||||||
Community Trust
|
Community Trust
|
||||||
</h3>
|
</h3>
|
||||||
<TrustBar data={filteredStats.trust} />
|
<TrustBar data={filteredStats.trust} />
|
||||||
@@ -691,23 +778,28 @@ export function GamePageClient({
|
|||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.4, delay: 0.25 }}
|
transition={{ duration: 0.4, delay: 0.25 }}
|
||||||
className="px-4 md:px-[10svw]"
|
className='px-4 md:px-[10svw]'
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto flex flex-col gap-4">
|
<div className='max-w-7xl mx-auto flex flex-col gap-4'>
|
||||||
<div className="flex items-center justify-between">
|
<div className='flex items-center justify-between'>
|
||||||
<h2 className="text-lg font-semibold">Community Presets</h2>
|
<h2 className='text-lg font-semibold'>
|
||||||
<span className="text-xs text-text/50">
|
Community Presets
|
||||||
{filteredPresets.length} preset{filteredPresets.length !== 1 ? "s" : ""}
|
</h2>
|
||||||
|
<span className='text-xs text-text/50'>
|
||||||
|
{filteredPresets.length} preset
|
||||||
|
{filteredPresets.length !== 1 ? "s" : ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredPresets.length === 0 ? (
|
{filteredPresets.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-16 gap-3 rounded-xl border border-border bg-text/[0.02]">
|
<div className='flex flex-col items-center justify-center py-16 gap-3 rounded-xl border border-border bg-text/2'>
|
||||||
<SettingsIcon className="h-10 w-10 text-text/20" />
|
<SettingsIcon className='h-10 w-10 text-text/20' />
|
||||||
<p className="text-sm text-text/40">No presets match the selected filters</p>
|
<p className='text-sm text-text/40'>
|
||||||
|
No presets match the selected filters
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4'>
|
||||||
{filteredPresets.map((preset) => {
|
{filteredPresets.map((preset) => {
|
||||||
const raw = isRawPerformerPreset(preset)
|
const raw = isRawPerformerPreset(preset)
|
||||||
const fpsColor = getFpsColor(preset)
|
const fpsColor = getFpsColor(preset)
|
||||||
@@ -717,69 +809,85 @@ export function GamePageClient({
|
|||||||
className={`flex flex-col gap-3 p-4 rounded-xl border transition-colors ${
|
className={`flex flex-col gap-3 p-4 rounded-xl border transition-colors ${
|
||||||
raw
|
raw
|
||||||
? "border-green-500/30 bg-green-500/5"
|
? "border-green-500/30 bg-green-500/5"
|
||||||
: "border-border bg-text/[0.03]"
|
: "border-border bg-text/3"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className='flex items-start justify-between gap-2'>
|
||||||
<div className="min-w-0">
|
<div className='min-w-0'>
|
||||||
<div className="flex items-center gap-2">
|
<div className='flex items-center gap-2'>
|
||||||
<h3 className="font-semibold text-sm truncate">
|
<h3 className='font-semibold text-sm truncate'>
|
||||||
{preset.name}
|
{preset.name}
|
||||||
</h3>
|
</h3>
|
||||||
{raw && (
|
{raw && (
|
||||||
<span className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded-full bg-green-500/10 border border-green-500/20 text-green-400 text-[9px] font-semibold">
|
<span className='inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded-full bg-green-500/10 border border-green-500/20 text-green-400 text-[9px] font-semibold'>
|
||||||
<SparklesIcon className="h-2.5 w-2.5" />
|
<SparklesIcon className='h-2.5 w-2.5' />
|
||||||
Raw
|
Raw
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-text/50 mt-0.5">
|
<p className='text-xs text-text/50 mt-0.5'>
|
||||||
{preset.hardwareName}
|
{preset.hardwareName}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 text-xs text-text/60 shrink-0">
|
<div className='flex items-center gap-1 text-xs text-text/60 shrink-0'>
|
||||||
<TrendingUpIcon className="h-3 w-3" />
|
<TrendingUpIcon className='h-3 w-3' />
|
||||||
{preset.upvotes}
|
{preset.upvotes}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Settings count */}
|
{/* Settings count */}
|
||||||
<div className="text-xs text-text/50">
|
<div className='text-xs text-text/50'>
|
||||||
{preset.settingsCount} settings
|
{preset.settingsCount} settings
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* FPS */}
|
{/* FPS */}
|
||||||
{preset.fpsAvg !== null && (
|
{preset.fpsAvg !== null && (
|
||||||
<div className={`text-sm tabular-nums ${fpsColor}`}>
|
<div
|
||||||
<span className="font-semibold">{preset.fpsAvg}</span>
|
className={`text-sm tabular-nums ${fpsColor}`}
|
||||||
<span className="text-text/40"> avg</span>
|
>
|
||||||
|
<span className='font-semibold'>
|
||||||
|
{preset.fpsAvg}
|
||||||
|
</span>
|
||||||
|
<span className='text-text/40'>
|
||||||
|
{" "}
|
||||||
|
avg
|
||||||
|
</span>
|
||||||
{preset.fpsLow !== null && (
|
{preset.fpsLow !== null && (
|
||||||
<span className="text-text/40">
|
<span className='text-text/40'>
|
||||||
{" "}({preset.fpsLow}–{preset.fpsHigh})
|
{" "}
|
||||||
|
({preset.fpsLow}–
|
||||||
|
{preset.fpsHigh})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Technology tags */}
|
{/* Technology tags */}
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className='flex flex-wrap items-center gap-1.5'>
|
||||||
{preset.fsrVersion && preset.fsrVersion !== "none" && (
|
{preset.fsrVersion &&
|
||||||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-purple-500/10 text-purple-400 border border-purple-500/20">
|
preset.fsrVersion !==
|
||||||
|
"none" && (
|
||||||
|
<span className='px-1.5 py-0.5 rounded text-[10px] font-medium bg-purple-500/10 text-purple-400 border border-purple-500/20'>
|
||||||
{preset.fsrVersion.toUpperCase()}
|
{preset.fsrVersion.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{preset.frameGenMethod && preset.frameGenMethod !== "none" && (
|
{preset.frameGenMethod &&
|
||||||
|
preset.frameGenMethod !==
|
||||||
|
"none" && (
|
||||||
<span
|
<span
|
||||||
className={`px-1.5 py-0.5 rounded text-[10px] font-medium border ${
|
className={`px-1.5 py-0.5 rounded text-[10px] font-medium border ${
|
||||||
preset.frameGenMethod === "dlss_fg"
|
preset.frameGenMethod ===
|
||||||
|
"dlss_fg"
|
||||||
? "bg-blue-500/10 text-blue-400 border-blue-500/20"
|
? "bg-blue-500/10 text-blue-400 border-blue-500/20"
|
||||||
: "bg-orange-500/10 text-orange-400 border-orange-500/20"
|
: "bg-orange-500/10 text-orange-400 border-orange-500/20"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{preset.frameGenMethod === "fsr_fg"
|
{preset.frameGenMethod ===
|
||||||
|
"fsr_fg"
|
||||||
? "FSR FG"
|
? "FSR FG"
|
||||||
: preset.frameGenMethod === "dlss_fg"
|
: preset.frameGenMethod ===
|
||||||
|
"dlss_fg"
|
||||||
? "DLSS FG"
|
? "DLSS FG"
|
||||||
: preset.frameGenMethod}
|
: preset.frameGenMethod}
|
||||||
</span>
|
</span>
|
||||||
@@ -787,12 +895,17 @@ export function GamePageClient({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Proton + OS */}
|
{/* Proton + OS */}
|
||||||
<div className="flex flex-wrap items-center gap-2 text-[10px] text-text/40">
|
<div className='flex flex-wrap items-center gap-2 text-[10px] text-text/40'>
|
||||||
{preset.protonVersion && (
|
{preset.protonVersion && (
|
||||||
<span>Proton {preset.protonVersion}</span>
|
<span>
|
||||||
|
Proton{" "}
|
||||||
|
{preset.protonVersion}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
{preset.osVersion && (
|
{preset.osVersion && (
|
||||||
<span>SteamOS {preset.osVersion}</span>
|
<span>
|
||||||
|
SteamOS {preset.osVersion}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -816,8 +929,8 @@ function Badge({
|
|||||||
label: string
|
label: string
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-500/10 border border-green-500/20 text-green-400 text-[10px] font-semibold">
|
<span className='inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-500/10 border border-green-500/20 text-green-400 text-[10px] font-semibold'>
|
||||||
<Icon className="h-3 w-3" />
|
<Icon className='h-3 w-3' />
|
||||||
{value} {label}
|
{value} {label}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -825,11 +938,11 @@ function Badge({
|
|||||||
|
|
||||||
function MetaItem({ label, value }: { label: string; value: string }) {
|
function MetaItem({ label, value }: { label: string; value: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className='flex flex-col gap-1'>
|
||||||
<span className="text-[10px] text-text/50 uppercase tracking-wider">
|
<span className='text-[10px] text-text/50 uppercase tracking-wider'>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-text/80 text-sm">{value}</span>
|
<span className='text-text/80 text-sm'>{value}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -844,13 +957,15 @@ function StatCard({
|
|||||||
icon: React.ElementType
|
icon: React.ElementType
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 p-4 rounded-xl border border-border bg-text/[0.03]">
|
<div className='flex items-center gap-3 p-4 rounded-xl border border-border bg-text/3'>
|
||||||
<div className="flex items-center justify-center h-9 w-9 rounded-lg bg-primary/10 text-primary">
|
<div className='flex items-center justify-center h-9 w-9 rounded-lg bg-primary/10 text-primary'>
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className='h-4 w-4' />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className='flex flex-col'>
|
||||||
<span className="text-xs text-text/50">{label}</span>
|
<span className='text-xs text-text/50'>{label}</span>
|
||||||
<span className="text-lg font-semibold tabular-nums">{value}</span>
|
<span className='text-lg font-semibold tabular-nums'>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -868,15 +983,18 @@ function FilterSelect({
|
|||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className='flex items-center gap-1.5'>
|
||||||
<label className="text-xs text-text/50 shrink-0">{label}</label>
|
<label className='text-xs text-text/50 shrink-0'>{label}</label>
|
||||||
<select
|
<select
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
className="text-xs bg-background border border-border rounded-md px-2 py-1 text-text/80 focus:outline-none focus:border-primary min-w-[100px]"
|
className='text-xs bg-background border border-border rounded-md px-2 py-1 text-text/80 focus:outline-none focus:border-primary min-w-25'
|
||||||
>
|
>
|
||||||
{options.map((opt) => (
|
{options.map((opt) => (
|
||||||
<option key={opt} value={opt}>
|
<option
|
||||||
|
key={opt}
|
||||||
|
value={opt}
|
||||||
|
>
|
||||||
{opt === "all" ? "Any" : opt}
|
{opt === "all" ? "Any" : opt}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -131,8 +131,13 @@ export default async function GamePage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Fetch related data in parallel ──────────────────────────────
|
// ── Fetch related data in parallel ──────────────────────────────
|
||||||
const [benchmarkCount, presetCount, commentCount, platformSupport, presetRows] =
|
const [
|
||||||
await Promise.all([
|
benchmarkCount,
|
||||||
|
presetCount,
|
||||||
|
commentCount,
|
||||||
|
platformSupport,
|
||||||
|
presetRows,
|
||||||
|
] = await Promise.all([
|
||||||
db
|
db
|
||||||
.select({ count: sql<number>`count(*)::int` })
|
.select({ count: sql<number>`count(*)::int` })
|
||||||
.from(performanceEntries)
|
.from(performanceEntries)
|
||||||
|
|||||||
Reference in New Issue
Block a user