chore: fix tailwind v4 styling & format styling

This commit is contained in:
2026-04-27 06:17:46 +08:00
parent 0a3738a778
commit ea6407cb4f
2 changed files with 1140 additions and 1017 deletions
+296 -178
View File
@@ -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>
))} ))}
+7 -2
View File
@@ -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)