feat: updates page and lint fixes

This commit is contained in:
2026-05-01 10:15:13 +08:00
parent 7bcd580397
commit ca08024cb5
25 changed files with 188 additions and 314 deletions
+2 -2
View File
@@ -169,7 +169,7 @@ export function GamesClient() {
setSyncCompleted(true) setSyncCompleted(true)
setSelectedIds(new Set()) setSelectedIds(new Set())
} }
} catch (e) { } catch {
// Ignore parse errors // Ignore parse errors
} }
} }
@@ -255,7 +255,7 @@ export function GamesClient() {
})) }))
setSyncCompleted(true) setSyncCompleted(true)
} }
} catch (e) { } catch {
// Ignore parse errors // Ignore parse errors
} }
} }
+11 -11
View File
@@ -485,18 +485,18 @@ export function GamesPageClient({
sortBy: sort, sortBy: sort,
}} }}
onLoad={(filters) => { onLoad={(filters) => {
setMinFps(filters.minFps || "") setMinFps((filters.minFps as string) || "")
setMaxFps(filters.maxFps || "") setMaxFps((filters.maxFps as string) || "")
setFsrSupport(filters.fsrSupport || false) setFsrSupport((filters.fsrSupport as boolean) || false)
setProtonNative(filters.protonNative || "any") setProtonNative((filters.protonNative as string) || "any")
setAntiCheatStatus(filters.antiCheatStatus || "any") setAntiCheatStatus((filters.antiCheatStatus as string) || "any")
setPlayabilityStatus(filters.playabilityStatus || "") setPlayabilityStatus((filters.playabilityStatus as string) || "")
setSteamReviewMin(filters.steamReviewMin || "") setSteamReviewMin((filters.steamReviewMin as string) || "")
setIsFree(filters.isFree || false) setIsFree((filters.isFree as boolean) || false)
setHasMultiplayer(filters.hasMultiplayer || false) setHasMultiplayer((filters.hasMultiplayer as boolean) || false)
if (filters.genre) setSelectedGenres([filters.genre]) if (filters.genre) setSelectedGenres([filters.genre as string])
else setSelectedGenres([]) else setSelectedGenres([])
if (filters.device) setSelectedDevice(filters.device) if (filters.device) setSelectedDevice(filters.device as string)
else setSelectedDevice("") else setSelectedDevice("")
if (filters.sortBy) setSort(filters.sortBy as SortOption) if (filters.sortBy) setSort(filters.sortBy as SortOption)
}} }}
+6 -148
View File
@@ -1,4 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "@tailwindcss/typography";
@theme inline { @theme inline {
/* Colors */ /* Colors */
@@ -9,7 +10,11 @@
--color-accent: #fb793c; --color-accent: #fb793c;
--color-text: #ebe4f1; --color-text: #ebe4f1;
--color-border: color-mix(in srgb, var(--color-text) 20%, transparent); --color-border: color-mix(in srgb, var(--color-text) 20%, transparent);
--color-border-active: color-mix(in srgb, var(--color-text) 60%, transparent); --color-border-active: color-mix(
in srgb,
var(--color-text) 60%,
transparent
);
/* Defaults */ /* Defaults */
--default-transition-duration: 0.3s; --default-transition-duration: 0.3s;
@@ -17,150 +22,3 @@
/* Fonts */ /* Fonts */
--font-sans: var(--font-lexend); --font-sans: var(--font-lexend);
} }
.scrollbar-hide {
scrollbar-width: none;
-ms-overflow-style: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Markdown content styles */
.update-content {
color: var(--color-text);
}
.update-content h2 {
font-size: 1.5rem;
font-weight: 700;
margin-top: 2.5rem;
margin-bottom: 1rem;
color: var(--color-text);
}
.update-content h3 {
font-size: 1.125rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: var(--color-text);
}
.update-content p {
margin-bottom: 0.75rem;
line-height: 1.7;
color: rgba(235, 228, 241, 0.8);
}
.update-content ul {
list-style-type: disc;
padding-left: 1.5rem;
margin-bottom: 1rem;
}
.update-content ol {
list-style-type: decimal;
padding-left: 1.5rem;
margin-bottom: 1rem;
}
.update-content li {
margin-bottom: 0.375rem;
line-height: 1.6;
}
.update-content li > ul,
.update-content li > ol {
margin-top: 0.25rem;
margin-bottom: 0.5rem;
}
.update-content a {
color: var(--color-primary);
text-decoration: none;
transition: opacity 0.2s;
}
.update-content a:hover {
text-decoration: underline;
opacity: 0.8;
}
/* Hide anchor link icons from rehype-autolink-headings by default */
.update-content a[aria-hidden="true"] {
text-decoration: none;
margin-right: 0.375rem;
opacity: 0;
transition: opacity 0.2s;
}
.update-content h2:hover a[aria-hidden="true"],
.update-content h3:hover a[aria-hidden="true"] {
opacity: 0.4;
}
.update-content a[aria-hidden="true"]::before {
content: "#";
font-size: 0.75rem;
font-weight: 400;
}
.update-content code {
color: var(--color-accent);
background: rgba(235, 228, 241, 0.06);
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.update-content pre {
background: rgba(235, 228, 241, 0.06);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin-bottom: 1rem;
}
.update-content pre code {
background: transparent;
padding: 0;
}
.update-content strong {
font-weight: 700;
color: var(--color-text);
}
.update-content hr {
border: none;
border-top: 1px solid var(--color-border);
margin: 2rem 0;
}
.update-content blockquote {
border-left: 3px solid var(--color-primary);
padding-left: 1rem;
margin: 1rem 0;
color: rgba(235, 228, 241, 0.7);
font-style: italic;
}
.update-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
}
.update-content th,
.update-content td {
border: 1px solid var(--color-border);
padding: 0.5rem 0.75rem;
text-align: left;
}
.update-content th {
font-weight: 600;
background: rgba(235, 228, 241, 0.05);
}
+41 -24
View File
@@ -33,16 +33,18 @@ export default function Landing() {
return ( return (
<section <section
id="hero" id='hero'
className="w-full h-[calc(100vh-3.6rem)] flex flex-col items-center justify-center relative p-4" className='w-full h-[calc(100vh-3.6rem)] flex flex-col items-center justify-center relative p-4'
> >
<motion.h1 <motion.h1
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
className="font-bold text-3xl md:text-5xl text-center flex flex-row gap-2 items-center justify-center" className='font-bold text-3xl md:text-5xl text-center flex flex-row gap-2 items-center justify-center'
> >
DeckyVault DeckyVault
<span className="border border-border text-xs md:text-base px-2 py-1 rounded-md bg-primary/10 font-medium">beta</span> <span className='border border-border text-xs md:text-base px-2 py-1 rounded-md bg-primary/10 font-medium'>
beta
</span>
</motion.h1> </motion.h1>
<motion.h2 <motion.h2
layout layout
@@ -50,18 +52,18 @@ export default function Landing() {
opacity: 0, opacity: 0,
}} }}
animate={{ opacity: 0.8, transition: { delay: 0.5 } }} animate={{ opacity: 0.8, transition: { delay: 0.5 } }}
className="mt-4 flex flex-row flex-wrap items-center justify-center gap-x-1 md:gap-x-2 text-base md:text-xl" className='mt-4 flex flex-row flex-wrap items-center justify-center gap-x-1 md:gap-x-2 text-base md:text-xl'
> >
{"Find your game".split(" ").map((word, index) => ( {"Find your game".split(" ").map((word, index) => (
<motion.span <motion.span
key={index} key={index}
className="text-center" className='text-center'
> >
{word} {word}
</motion.span> </motion.span>
))} ))}
<AnimatePresence <AnimatePresence
mode="wait" mode='wait'
initial={false} initial={false}
> >
<motion.span <motion.span
@@ -69,7 +71,7 @@ export default function Landing() {
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="text-primary font-bold" className='text-primary font-bold'
> >
{words[currentWord]} {words[currentWord]}
</motion.span> </motion.span>
@@ -77,27 +79,27 @@ export default function Landing() {
</motion.h2> </motion.h2>
<AnimatePresence> <AnimatePresence>
<motion.label <motion.label
key="search-bar" key='search-bar'
layoutId="search-bar" layoutId='search-bar'
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
className="mt-8 flex flex-row items-center gap-4 bg-text/5 px-2 py-2 rounded-md border border-border placeholder:text-text/60 group hover:border-border-active transition-colors focus-within:border-primary/80! focus-within:ring-2 focus-within:ring-primary/50! focus-within:ring-offset-2 focus-within:ring-offset-background cursor-text w-full max-w-md" className='mt-8 flex flex-row items-center gap-4 bg-text/5 px-2 py-2 rounded-md border border-border placeholder:text-text/60 group hover:border-border-active transition-colors focus-within:border-primary/80! focus-within:ring-2 focus-within:ring-primary/50! focus-within:ring-offset-2 focus-within:ring-offset-background cursor-text w-full max-w-md'
> >
<Gamepad2Icon className="h-6 w-6 group-focus-within:stroke-accent transition-colors shrink-0" /> <Gamepad2Icon className='h-6 w-6 group-focus-within:stroke-accent transition-colors shrink-0' />
<input <input
type="text" type='text'
placeholder="search by game or appid..." placeholder='search by game or appid...'
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className="flex-1 outline-none bg-transparent text-xl min-w-0" className='flex-1 outline-none bg-transparent text-xl min-w-0'
/> />
<motion.button <motion.button
onClick={handleSearchSubmit} onClick={handleSearchSubmit}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
className="text-sm flex flex-row gap-1 items-center bg-text text-background px-2 py-1 rounded-sm cursor-pointer hover:opacity-60 transition-opacity shrink-0" className='text-sm flex flex-row gap-1 items-center bg-text text-background px-2 py-1 rounded-sm cursor-pointer hover:opacity-60 transition-opacity shrink-0'
> >
<SearchIcon className="h-3 w-3" /> <SearchIcon className='h-3 w-3' />
search search
</motion.button> </motion.button>
</motion.label> </motion.label>
@@ -105,26 +107,41 @@ export default function Landing() {
<motion.small <motion.small
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 1.5 } }} animate={{ opacity: 1, transition: { delay: 1.5 } }}
className="mt-8 text-center text-xs flex flex-row gap-1" className='mt-8 text-center text-xs flex flex-row gap-1'
> >
<span className="opacity-60">2026 DeckyVault.</span>
<Link <Link
title="Visit our Github Repository" title='Visit our Github Repository'
href="https://github.com/AdrianBonpin/deckyvault" href='/updates'
className="text-accent opacity-60 hover:opacity-100 transition-opacity cursor-pointer" className='text-accent opacity-60 hover:opacity-100 transition-opacity cursor-pointer'
>
See what{"'"}s new.
</Link>
</motion.small>
<motion.small
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 1.5 } }}
className='mt-2 text-center text-xs flex flex-row gap-1'
>
<span className='opacity-60'>2026 DeckyVault.</span>
<Link
title='Visit our Github Repository'
href='https://github.com/AdrianBonpin/deckyvault'
className='text-accent opacity-60 hover:opacity-100 transition-opacity cursor-pointer'
> >
Github. Github.
</Link> </Link>
<span className="opacity-60">v{process.env.NEXT_PUBLIC_APP_VERSION}</span>
</motion.small> </motion.small>
<script <script
type="application/ld+json" type='application/ld+json'
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: JSON.stringify({ __html: JSON.stringify({
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "WebSite", "@type": "WebSite",
name: "DeckyVault", name: "DeckyVault",
url: "https://deckyvault.xyz", url: "https://deckyvault.xyz",
description: "Steam Deck benchmarks, settings, and performance guides", description:
"Steam Deck benchmarks, settings, and performance guides",
potentialAction: { potentialAction: {
"@type": "SearchAction", "@type": "SearchAction",
target: "https://deckyvault.xyz/search?q={search_term_string}", target: "https://deckyvault.xyz/search?q={search_term_string}",
+1 -1
View File
@@ -43,7 +43,7 @@ export default async function UpdatePage({
return ( return (
<main className="w-full"> <main className="w-full">
<div className="w-full max-w-4xl mx-auto px-4 pt-4"> <div className="w-full max-w-7xl mx-auto px-4 pt-4">
<Link <Link
href="/updates" href="/updates"
className="inline-flex items-center gap-1 text-sm text-text/60 hover:text-primary transition-colors" className="inline-flex items-center gap-1 text-sm text-text/60 hover:text-primary transition-colors"
+18 -12
View File
@@ -6,32 +6,38 @@ import { ArrowLeftIcon } from "lucide-react"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Updates", title: "Updates",
description: "See what's new in DeckyVault — version release notes, features, and improvements.", description:
"See what's new in DeckyVault — version release notes, features, and improvements.",
} }
export default function UpdatesPage() { export default function UpdatesPage() {
const updates = getAllUpdates() const updates = getAllUpdates()
return ( return (
<main className="w-full max-w-2xl mx-auto px-4 py-8"> <main className='w-full max-w-7xl mx-auto px-4 py-8'>
<div className="flex flex-row items-center gap-2 mb-6"> <div className='flex flex-row items-center gap-2 mb-6'>
<Link <Link
href="/" href='/'
className="text-text/60 hover:text-primary transition-colors" className='text-text/60 hover:text-primary transition-colors'
> >
<ArrowLeftIcon className="w-4 h-4" /> <ArrowLeftIcon className='w-4 h-4' />
</Link> </Link>
<h1 className="text-2xl font-bold">Updates</h1> <h1 className='text-2xl font-bold'>Updates</h1>
</div> </div>
<p className="text-text/60 mb-8"> <p className='text-text/60 mb-8'>
Release notes and changelogs for every DeckyVault version. Release notes, changelogs, and the like for DeckyVault.
</p> </p>
{updates.length === 0 ? ( {updates.length === 0 ? (
<p className="text-text/40 text-center py-16">No updates yet.</p> <p className='text-text/40 text-center py-16'>
No updates yet.
</p>
) : ( ) : (
<div className="flex flex-col gap-3"> <div className='flex flex-col gap-3'>
{updates.map((update) => ( {updates.map((update) => (
<UpdateCard key={update.slug} update={update} /> <UpdateCard
key={update.slug}
update={update}
/>
))} ))}
</div> </div>
)} )}
+9
View File
@@ -39,6 +39,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^20", "@types/node": "^20",
"@types/pg": "^8.20.0", "@types/pg": "^8.20.0",
"@types/react": "^19", "@types/react": "^19",
@@ -599,6 +600,8 @@
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.4", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "postcss": "^8.5.6", "tailwindcss": "4.2.4" } }, "sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg=="], "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.4", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "postcss": "^8.5.6", "tailwindcss": "4.2.4" } }, "sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg=="],
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="],
"@tiptap/core": ["@tiptap/core@3.22.4", "", { "peerDependencies": { "@tiptap/pm": "3.22.4" } }, "sha512-vGIGm/HpqLg8EAAQXQ+koV+/S828OEpzocfWcPOwo1u2QUVf9dQG47Yy6JJ8zFFaJwfv4dBcOXli+7BrJwsxDQ=="], "@tiptap/core": ["@tiptap/core@3.22.4", "", { "peerDependencies": { "@tiptap/pm": "3.22.4" } }, "sha512-vGIGm/HpqLg8EAAQXQ+koV+/S828OEpzocfWcPOwo1u2QUVf9dQG47Yy6JJ8zFFaJwfv4dBcOXli+7BrJwsxDQ=="],
"@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-7/61kNPbGFhMgM//zMknD0pSb69rGdRIkpulXOWS1JBrFHkH6hjZDfrOETNzgKkO+NlmzVl9rXSTv0xauS3lzA=="], "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.22.4", "", { "peerDependencies": { "@tiptap/core": "3.22.4" } }, "sha512-7/61kNPbGFhMgM//zMknD0pSb69rGdRIkpulXOWS1JBrFHkH6hjZDfrOETNzgKkO+NlmzVl9rXSTv0xauS3lzA=="],
@@ -867,6 +870,8 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
@@ -1385,6 +1390,8 @@
"postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
"postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
@@ -1639,6 +1646,8 @@
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+1
View File
@@ -31,6 +31,7 @@ export function CommentSection({ gameId, initialCount }: CommentSectionProps) {
const limit = 20 const limit = 20
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true) setMounted(true)
}, []) }, [])
+4 -4
View File
@@ -1,18 +1,18 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Bookmark, Plus, Trash2, Check } from "lucide-react"; import { Bookmark, Plus, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface SavedFilter { interface SavedFilter {
id: string; id: string;
name: string; name: string;
filters: Record<string, any>; filters: Record<string, unknown>;
} }
interface SavedFiltersProps { interface SavedFiltersProps {
currentFilters: Record<string, any>; currentFilters: Record<string, unknown>;
onLoad: (filters: Record<string, any>) => void; onLoad: (filters: Record<string, unknown>) => void;
className?: string; className?: string;
} }
+5 -4
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect, useCallback } from "react";
import { ThumbsUp, ThumbsDown, ExternalLink, MessageSquare } from "lucide-react"; import { ThumbsUp, ThumbsDown, ExternalLink, MessageSquare } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -42,7 +42,7 @@ export function SteamReviews({ gameId, steamAppId, className }: SteamReviewsProp
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [offset, setOffset] = useState(0); const [offset, setOffset] = useState(0);
const fetchReviews = async (newOffset: number) => { const fetchReviews = useCallback(async (newOffset: number) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
@@ -64,11 +64,12 @@ export function SteamReviews({ gameId, steamAppId, className }: SteamReviewsProp
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [gameId, setData, setLoading, setError, setOffset]);
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
fetchReviews(0); fetchReviews(0);
}, [gameId]); }, [gameId, fetchReviews]);
// Loading skeleton // Loading skeleton
if (loading && !data) { if (loading && !data) {
+10 -6
View File
@@ -10,17 +10,21 @@ export function UpdateCard({ update }: { update: UpdateMeta }) {
return ( return (
<Link href={`/updates/${update.slug}`}> <Link href={`/updates/${update.slug}`}>
<article className="group flex flex-col gap-2 p-4 rounded-lg border border-border hover:border-border-active hover:bg-text/3 transition-colors"> <article className='group flex flex-col gap-2 p-4 border border-border rounded-md hover:bg-text/3 transition-colors'>
<div className="flex flex-row items-center gap-2"> <div className='flex flex-row items-center gap-2'>
<span className="text-xs font-mono px-2 py-0.5 rounded bg-primary/10 text-primary border border-primary/20"> <span className='text-xs font-mono px-2 py-0.5 rounded bg-primary/10 text-primary border border-primary/20'>
v{update.version} v{update.version}
</span> </span>
<time className="text-xs text-text/60">{formattedDate}</time> <time className='text-xs text-text/60'>
{formattedDate}
</time>
</div> </div>
<h3 className="text-lg font-semibold group-hover:text-primary transition-colors"> <h3 className='text-lg font-semibold group-hover:text-primary transition-colors'>
{update.title} {update.title}
</h3> </h3>
<p className="text-sm text-text/60 line-clamp-2">{update.summary}</p> <p className='text-sm text-text/60 line-clamp-2'>
{update.summary}
</p>
</article> </article>
</Link> </Link>
) )
+1 -1
View File
@@ -27,7 +27,7 @@ export function UpdateViewer({
</aside> </aside>
{/* Main content */} {/* Main content */}
<article className="flex-1 min-w-0"> <article className="flex-1 min-w-0 prose prose-invert lg:proxe-xl">
<header className="mb-8"> <header className="mb-8">
<div className="flex flex-row items-center gap-2 mb-2"> <div className="flex flex-row items-center gap-2 mb-2">
<span className="text-xs font-mono px-2 py-0.5 rounded bg-primary/10 text-primary border border-primary/20"> <span className="text-xs font-mono px-2 py-0.5 rounded bg-primary/10 text-primary border border-primary/20">
+2 -1
View File
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation"
import { motion, AnimatePresence } from "motion/react" import { motion, AnimatePresence } from "motion/react"
import { StepIndicator } from "@/components/wizard/step-indicator" import { StepIndicator } from "@/components/wizard/step-indicator"
import { SetupStep, type GameVersionInfo } from "@/components/wizard/steps/setup-step" import { SetupStep, type GameVersionInfo } from "@/components/wizard/steps/setup-step"
import { AntiCheatStep, type AntiCheatData } from "@/components/wizard/steps/anti-cheat-step" import { type AntiCheatData } from "@/components/wizard/steps/anti-cheat-step"
import { PerformanceStep, type PerformanceData } from "@/components/wizard/steps/performance-step" import { PerformanceStep, type PerformanceData } from "@/components/wizard/steps/performance-step"
import { SettingsStep } from "@/components/wizard/steps/settings-step" import { SettingsStep } from "@/components/wizard/steps/settings-step"
import { EnvironmentStep, type EnvironmentData } from "@/components/wizard/steps/environment-step" import { EnvironmentStep, type EnvironmentData } from "@/components/wizard/steps/environment-step"
@@ -69,6 +69,7 @@ export function GameEntryWizard({ gameId, gameVersions, defaultVersionId, editEn
) ?? platformSupport.find((p) => p.antiCheatRelevant) ) ?? platformSupport.find((p) => p.antiCheatRelevant)
if (entry) { if (entry) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setAntiCheat({ setAntiCheat({
antiCheatRelevant: entry.antiCheatRelevant, antiCheatRelevant: entry.antiCheatRelevant,
antiCheatName: entry.antiCheatName ?? "", antiCheatName: entry.antiCheatName ?? "",
+2 -2
View File
@@ -1,7 +1,7 @@
"use client" "use client"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { Shield, ShieldCheck, ShieldX, ShieldQuestion, Pencil } from "lucide-react" import { Shield, ShieldCheck, ShieldX, ShieldQuestion } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
export type AntiCheatData = { export type AntiCheatData = {
@@ -61,7 +61,7 @@ export function AntiCheatStep({
value, value,
onChange, onChange,
}: AntiCheatStepProps) { }: AntiCheatStepProps) {
const [isEditing, setIsEditing] = useState(false) const [isEditing] = useState(false)
// Find the best existing entry to prefill: // Find the best existing entry to prefill:
// Prefer the entry for the currently selected hardware, // Prefer the entry for the currently selected hardware,
+1 -1
View File
@@ -1,7 +1,7 @@
"use client" "use client"
import { motion } from "motion/react" import { motion } from "motion/react"
import { Send, Loader2, AlertCircle, Monitor, Gauge, SlidersHorizontal, Terminal, FileText, GitBranch, Shield } from "lucide-react" import { Send, Loader2, AlertCircle, Monitor, Gauge, SlidersHorizontal, Terminal, FileText } from "lucide-react"
import { TiptapEditor } from "@/components/tiptap-editor" import { TiptapEditor } from "@/components/tiptap-editor"
import type { SettingCategory } from "@/components/wizard/settings-editor" import type { SettingCategory } from "@/components/wizard/settings-editor"
import type { PerformanceData } from "./performance-step" import type { PerformanceData } from "./performance-step"
+4 -2
View File
@@ -33,11 +33,13 @@ interface SetupStepProps {
} }
export function SetupStep({ export function SetupStep({
gameId, // eslint-disable-next-line @typescript-eslint/no-unused-vars
gameId: _gameId,
gameVersions, gameVersions,
hardwareSlug, hardwareSlug,
onHardwareChange, onHardwareChange,
hardwareName, // eslint-disable-next-line @typescript-eslint/no-unused-vars
hardwareName: _hardwareName,
selectedVersionId, selectedVersionId,
onVersionChange, onVersionChange,
newVersionString, newVersionString,
+9
View File
@@ -4,6 +4,15 @@ date: "2026-04-30"
version: "2026.0.95" version: "2026.0.95"
summary: "Major update adding anti-cheat tracking, Steam reviews integration, playability indicators, games list revamp, community suggestions, and more." summary: "Major update adding anti-cheat tracking, Steam reviews integration, playability indicators, games list revamp, community suggestions, and more."
--- ---
# Hello everyone!
Starting with somewhat every major release, we'll be making this quick changelog like updates page to track whats new and whats changed. Feel free to check for whats new as I'm always trying to improve the site and experience a lot more!
For now here are some of the recent updates if you have some time to read over them.
And if there are any issues/problems, feel free to visit the [contact page](https://deckyvault.xyz/contact) to submit a report or leave a review!
> Adrian Bonpin
## Added ## Added
+5 -5
View File
@@ -1,6 +1,6 @@
import { Elysia, t } from "elysia" import { Elysia, t } from "elysia"
import { db } from "@/lib/db/index" import { db } from "@/lib/db/index"
import { communitySuggestions, games, user } from "@/lib/db/schema" import { communitySuggestions, games, user, suggestionStatusEnum } from "@/lib/db/schema"
import { eq, and, desc } from "drizzle-orm" import { eq, and, desc } from "drizzle-orm"
import { requireAuth, requireContributorOrAdmin } from "@/lib/auth/guard" import { requireAuth, requireContributorOrAdmin } from "@/lib/auth/guard"
@@ -109,7 +109,7 @@ export const communitySuggestionRoutes = new Elysia({
return { error: "You already have a pending suggestion for this field" } return { error: "You already have a pending suggestion for this field" }
} }
const currentValue = String((game as any)[fieldName] ?? "") const currentValue = String((game as Record<string, unknown>)[fieldName] ?? "")
const [suggestion] = await db const [suggestion] = await db
.insert(communitySuggestions) .insert(communitySuggestions)
@@ -149,9 +149,9 @@ export const communitySuggestionRoutes = new Elysia({
async ({ params, query }) => { async ({ params, query }) => {
const status = query.status // optional filter const status = query.status // optional filter
let conditions = [eq(communitySuggestions.gameId, params.gameId)] const conditions = [eq(communitySuggestions.gameId, params.gameId)]
if (status) { if (status) {
conditions.push(eq(communitySuggestions.status, status as any)) conditions.push(eq(communitySuggestions.status, status as typeof suggestionStatusEnum.enumValues[number]))
} }
const suggestions = await db const suggestions = await db
@@ -263,7 +263,7 @@ export const communitySuggestionRoutes = new Elysia({
// If approved, apply the change to the game // If approved, apply the change to the game
if (status === "approved") { if (status === "approved") {
const updateData: Record<string, any> = {} const updateData: Record<string, string> = {}
updateData[suggestion.fieldName] = suggestion.proposedValue updateData[suggestion.fieldName] = suggestion.proposedValue
await db await db
+5 -5
View File
@@ -7,7 +7,7 @@ import {
gamePlatformSupport, gamePlatformSupport,
hardware, hardware,
} from "@/lib/db/schema" } from "@/lib/db/schema"
import { ilike, or, sql, eq, and, desc, asc, inArray, gte, lte } from "drizzle-orm" import { ilike, or, sql, eq, and, desc, asc, inArray, gte, lte, type SQL } from "drizzle-orm"
import { fuzzySearchTerm } from "@/lib/db/search" import { fuzzySearchTerm } from "@/lib/db/search"
const MAX_OFFSET = 10000 const MAX_OFFSET = 10000
@@ -92,11 +92,11 @@ export const gamesListingRoutes = new Elysia({ prefix: "/games/listing" }).get(
// ── FPS range filter (games with benchmarks in this range) ──── // ── FPS range filter (games with benchmarks in this range) ────
if (minFps || maxFps) { if (minFps || maxFps) {
const fpsConditions = [ const fpsConditions: (SQL | undefined)[] = [
eq(performanceEntries.isRemoved, false), eq(performanceEntries.isRemoved, false),
minFps ? gte(performanceEntries.fpsAvg, Number(minFps)) : undefined, minFps ? gte(performanceEntries.fpsAvg, Number(minFps)) : undefined,
maxFps ? lte(performanceEntries.fpsAvg, Number(maxFps)) : undefined, maxFps ? lte(performanceEntries.fpsAvg, Number(maxFps)) : undefined,
].filter(Boolean) as any[] ].filter((c): c is SQL => c !== undefined)
const fpsSubquery = db const fpsSubquery = db
.select({ gameId: gameVersions.gameId }) .select({ gameId: gameVersions.gameId })
@@ -127,7 +127,7 @@ export const gamesListingRoutes = new Elysia({ prefix: "/games/listing" }).get(
// ── Proton / Native filter ──────────────────────────────────── // ── Proton / Native filter ────────────────────────────────────
if (protonNative && ["proton", "native", "both"].includes(protonNative)) { if (protonNative && ["proton", "native", "both"].includes(protonNative)) {
const protonConditions: any[] = [] const protonConditions: SQL[] = []
if (protonNative === "proton" || protonNative === "both") { if (protonNative === "proton" || protonNative === "both") {
protonConditions.push(eq(gamePlatformSupport.protonStatus, "proton")) protonConditions.push(eq(gamePlatformSupport.protonStatus, "proton"))
@@ -148,7 +148,7 @@ export const gamesListingRoutes = new Elysia({ prefix: "/games/listing" }).get(
// ── Anti-cheat status filter ────────────────────────────────── // ── Anti-cheat status filter ──────────────────────────────────
const validAcStatuses = ["supported", "unsupported", "unknown", "none"] const validAcStatuses = ["supported", "unsupported", "unknown", "none"]
if (antiCheatStatus && antiCheatStatus !== "any" && validAcStatuses.includes(antiCheatStatus)) { if (antiCheatStatus && antiCheatStatus !== "any" && validAcStatuses.includes(antiCheatStatus)) {
const acConditions: any[] = [ const acConditions: SQL[] = [
eq(gamePlatformSupport.antiCheatRelevant, true), eq(gamePlatformSupport.antiCheatRelevant, true),
eq(gamePlatformSupport.antiCheatStatus, antiCheatStatus as "none" | "supported" | "unsupported" | "unknown"), eq(gamePlatformSupport.antiCheatStatus, antiCheatStatus as "none" | "supported" | "unsupported" | "unknown"),
] ]
+3 -42
View File
@@ -190,46 +190,7 @@ export const gameVersionsRoutes = new Elysia({ prefix: "/games/:gameId/versions"
// ── Game Sync Routes ──────────────────────────────────────────────── // ── Game Sync Routes ────────────────────────────────────────────────
const MAX_BULK_SYNC = 1000 const MAX_BULK_SYNC = 1000
const SYNC_CONCURRENCY = 5 // Number of parallel syncs
const SYNC_BATCH_DELAY_MS = 2000 // Delay between batches to respect rate limits (2 seconds)
/**
* Process syncs sequentially with delay to avoid Steam rate limiting.
* Steam API has aggressive rate limiting - parallel requests get blocked quickly.
*/
async function syncSequentially(
gamesToSync: { id: string; steamAppId: number | null }[]
): Promise<{ synced: number; failed: number; results: Map<string, { success: boolean; error?: string }> }> {
let synced = 0
let failed = 0
const results = new Map<string, { success: boolean; error?: string }>()
for (let i = 0; i < gamesToSync.length; i++) {
const game = gamesToSync[i]
if (!game.steamAppId) continue
try {
const result = await syncSteamGame(game.steamAppId, { forceRetry: true })
if (result.success) {
synced++
results.set(game.id, { success: true })
} else {
failed++
results.set(game.id, { success: false, error: result.error })
}
} catch (e) {
failed++
results.set(game.id, { success: false, error: String(e) })
}
// Delay between syncs to avoid rate limiting
if (i < gamesToSync.length - 1) {
await new Promise((resolve) => setTimeout(resolve, SYNC_BATCH_DELAY_MS))
}
}
return { synced, failed, results }
}
export const gameSyncRoutes = new Elysia({ prefix: "/games" }) export const gameSyncRoutes = new Elysia({ prefix: "/games" })
// Bulk sync with streaming progress (defined before /:gameId/sync to avoid route conflict) // Bulk sync with streaming progress (defined before /:gameId/sync to avoid route conflict)
@@ -285,7 +246,8 @@ export const gameSyncRoutes = new Elysia({ prefix: "/games" })
const encoder = new TextEncoder() const encoder = new TextEncoder()
const stream = new ReadableStream({ const stream = new ReadableStream({
async start(controller) { async start(controller) {
const send = (data: any) => { type ProgressEvent = { type: string; current?: number; total: number; synced: number; failed: number; currentGame?: string | null }
const send = (data: ProgressEvent) => {
controller.enqueue(encoder.encode(JSON.stringify(data) + "\n")) controller.enqueue(encoder.encode(JSON.stringify(data) + "\n"))
} }
@@ -294,7 +256,6 @@ export const gameSyncRoutes = new Elysia({ prefix: "/games" })
let synced = 0 let synced = 0
let failed = 0 let failed = 0
const batchSize = SYNC_CONCURRENCY
// Process syncs sequentially with delay to avoid rate limiting // Process syncs sequentially with delay to avoid rate limiting
for (let i = 0; i < gamesToSync.length; i++) { for (let i = 0; i < gamesToSync.length; i++) {
@@ -305,7 +266,7 @@ export const gameSyncRoutes = new Elysia({ prefix: "/games" })
const result = await syncSteamGame(game.steamAppId, { forceRetry: true }) const result = await syncSteamGame(game.steamAppId, { forceRetry: true })
if (result.success) synced++ if (result.success) synced++
else failed++ else failed++
} catch (e) { } catch {
failed++ failed++
} }
+7 -7
View File
@@ -1,6 +1,6 @@
import { Elysia } from "elysia"; import { Elysia } from "elysia";
import { db } from "@/lib/db/index"; import { db } from "@/lib/db/index";
import { games, gamePlatformSupport, performanceEntries, gameVersions } from "@/lib/db/schema"; import { games, gamePlatformSupport, performanceEntries, gameVersions, playabilityStatusEnum } from "@/lib/db/schema";
import { eq, and, avg, count, sql } from "drizzle-orm"; import { eq, and, avg, count, sql } from "drizzle-orm";
import { requireContributorOrAdmin } from "@/lib/auth/guard"; import { requireContributorOrAdmin } from "@/lib/auth/guard";
@@ -95,7 +95,7 @@ export async function recalculatePlayability(gameId: string): Promise<{
await db await db
.update(gamePlatformSupport) .update(gamePlatformSupport)
.set({ .set({
playabilityStatus: status as any, playabilityStatus: status as typeof playabilityStatusEnum.enumValues[number],
playabilityCalculatedAt: new Date(), playabilityCalculatedAt: new Date(),
}) })
.where( .where(
@@ -111,13 +111,13 @@ export async function recalculatePlayability(gameId: string): Promise<{
// Update aggregate game-level playability (worst of all devices) // Update aggregate game-level playability (worst of all devices)
const priority = { unplayable: 0, needs_tweaks: 1, playable: 2, great: 3, unknown: 4 }; const priority = { unplayable: 0, needs_tweaks: 1, playable: 2, great: 3, unknown: 4 };
let worstStatus: string = "unknown"; let worstStatus: typeof playabilityStatusEnum.enumValues[number] = "unknown";
for (const r of results) { for (const r of results) {
if ( if (
priority[r.playabilityStatus as keyof typeof priority] < priority[r.playabilityStatus as keyof typeof priority] <
priority[worstStatus as keyof typeof priority] priority[worstStatus as keyof typeof priority]
) { ) {
worstStatus = r.playabilityStatus; worstStatus = r.playabilityStatus as typeof playabilityStatusEnum.enumValues[number];
} }
} }
@@ -132,7 +132,7 @@ export async function recalculatePlayability(gameId: string): Promise<{
await db await db
.update(games) .update(games)
.set({ .set({
playabilityStatus: worstStatus as any, playabilityStatus: worstStatus as typeof playabilityStatusEnum.enumValues[number],
playabilityCalculatedAt: new Date(), playabilityCalculatedAt: new Date(),
}) })
.where(eq(games.id, gameId)); .where(eq(games.id, gameId));
@@ -174,7 +174,7 @@ export const playabilityRoutes = new Elysia({ prefix: "/playability" })
if (hardwareSlug) { if (hardwareSlug) {
await db await db
.update(gamePlatformSupport) .update(gamePlatformSupport)
.set({ playabilityStatus: status as any, playabilityOverride: true }) .set({ playabilityStatus: status as typeof playabilityStatusEnum.enumValues[number], playabilityOverride: true })
.where( .where(
and( and(
eq(gamePlatformSupport.gameId, params.gameId), eq(gamePlatformSupport.gameId, params.gameId),
@@ -184,7 +184,7 @@ export const playabilityRoutes = new Elysia({ prefix: "/playability" })
} else { } else {
await db await db
.update(games) .update(games)
.set({ playabilityStatus: status as any, playabilityOverride: true }) .set({ playabilityStatus: status as typeof playabilityStatusEnum.enumValues[number], playabilityOverride: true })
.where(eq(games.id, params.gameId)); .where(eq(games.id, params.gameId));
} }
+2 -2
View File
@@ -6,7 +6,7 @@ import { eq } from "drizzle-orm"
// In-memory cache for reviews (key -> { data, expires }) // In-memory cache for reviews (key -> { data, expires })
const reviewCache = new Map< const reviewCache = new Map<
string, string,
{ data: any; expires: number } { data: SteamReviewResponse; expires: number }
>() >()
const CACHE_TTL = 60 * 60 * 1000 // 1 hour const CACHE_TTL = 60 * 60 * 1000 // 1 hour
@@ -100,7 +100,7 @@ export const steamReviewRoutes = new Elysia({ prefix: "/steam-reviews" })
reviewCache.set(cacheKey, { data: slicedData, expires: Date.now() + CACHE_TTL }) reviewCache.set(cacheKey, { data: slicedData, expires: Date.now() + CACHE_TTL })
return slicedData return slicedData
} catch (error) { } catch {
set.status = 502 set.status = 502
return { error: "Steam review API unavailable" } return { error: "Steam review API unavailable" }
} }
+2 -2
View File
@@ -1,5 +1,5 @@
import { db } from "@/lib/db/index" import { db } from "@/lib/db/index"
import { games } from "@/lib/db/schema" import { games, steamReviewSentimentEnum } from "@/lib/db/schema"
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { recalculatePlayability } from "@/lib/api/playability" import { recalculatePlayability } from "@/lib/api/playability"
@@ -247,7 +247,7 @@ export async function syncSteamGame(
metacriticUrl: d.metacritic?.url ?? null, metacriticUrl: d.metacritic?.url ?? null,
recommendationsTotal: d.recommendations?.total ?? null, recommendationsTotal: d.recommendations?.total ?? null,
steamReviewScore: reviewData.reviewScore, steamReviewScore: reviewData.reviewScore,
steamReviewSentiment: reviewData.reviewSentiment as any, steamReviewSentiment: reviewData.reviewSentiment as typeof steamReviewSentimentEnum.enumValues[number] | null,
steamReviewCount: reviewData.reviewCount, steamReviewCount: reviewData.reviewCount,
priceCurrent: d.price_overview?.final ?? null, priceCurrent: d.price_overview?.final ?? null,
priceInitial: d.price_overview?.initial ?? null, priceInitial: d.price_overview?.initial ?? null,
+4
View File
@@ -1,6 +1,10 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import { version } from "./package.json";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
env: {
NEXT_PUBLIC_APP_VERSION: version,
},
// Elysia is Bun-native and must not be bundled by Next.js // Elysia is Bun-native and must not be bundled by Next.js
serverExternalPackages: ["elysia", "@elysiajs/eden"], serverExternalPackages: ["elysia", "@elysiajs/eden"],
images: { images: {
+1
View File
@@ -50,6 +50,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^20", "@types/node": "^20",
"@types/pg": "^8.20.0", "@types/pg": "^8.20.0",
"@types/react": "^19", "@types/react": "^19",