feat: updates page and lint fixes
This commit is contained in:
@@ -169,7 +169,7 @@ export function GamesClient() {
|
||||
setSyncCompleted(true)
|
||||
setSelectedIds(new Set())
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
@@ -255,7 +255,7 @@ export function GamesClient() {
|
||||
}))
|
||||
setSyncCompleted(true)
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,18 +485,18 @@ export function GamesPageClient({
|
||||
sortBy: sort,
|
||||
}}
|
||||
onLoad={(filters) => {
|
||||
setMinFps(filters.minFps || "")
|
||||
setMaxFps(filters.maxFps || "")
|
||||
setFsrSupport(filters.fsrSupport || false)
|
||||
setProtonNative(filters.protonNative || "any")
|
||||
setAntiCheatStatus(filters.antiCheatStatus || "any")
|
||||
setPlayabilityStatus(filters.playabilityStatus || "")
|
||||
setSteamReviewMin(filters.steamReviewMin || "")
|
||||
setIsFree(filters.isFree || false)
|
||||
setHasMultiplayer(filters.hasMultiplayer || false)
|
||||
if (filters.genre) setSelectedGenres([filters.genre])
|
||||
setMinFps((filters.minFps as string) || "")
|
||||
setMaxFps((filters.maxFps as string) || "")
|
||||
setFsrSupport((filters.fsrSupport as boolean) || false)
|
||||
setProtonNative((filters.protonNative as string) || "any")
|
||||
setAntiCheatStatus((filters.antiCheatStatus as string) || "any")
|
||||
setPlayabilityStatus((filters.playabilityStatus as string) || "")
|
||||
setSteamReviewMin((filters.steamReviewMin as string) || "")
|
||||
setIsFree((filters.isFree as boolean) || false)
|
||||
setHasMultiplayer((filters.hasMultiplayer as boolean) || false)
|
||||
if (filters.genre) setSelectedGenres([filters.genre as string])
|
||||
else setSelectedGenres([])
|
||||
if (filters.device) setSelectedDevice(filters.device)
|
||||
if (filters.device) setSelectedDevice(filters.device as string)
|
||||
else setSelectedDevice("")
|
||||
if (filters.sortBy) setSort(filters.sortBy as SortOption)
|
||||
}}
|
||||
|
||||
+7
-149
@@ -1,4 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@theme inline {
|
||||
/* Colors */
|
||||
@@ -9,158 +10,15 @@
|
||||
--color-accent: #fb793c;
|
||||
--color-text: #ebe4f1;
|
||||
--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 */
|
||||
--default-transition-duration: 0.3s;
|
||||
|
||||
/* Fonts */
|
||||
--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
@@ -33,16 +33,18 @@ export default function Landing() {
|
||||
|
||||
return (
|
||||
<section
|
||||
id="hero"
|
||||
className="w-full h-[calc(100vh-3.6rem)] flex flex-col items-center justify-center relative p-4"
|
||||
id='hero'
|
||||
className='w-full h-[calc(100vh-3.6rem)] flex flex-col items-center justify-center relative p-4'
|
||||
>
|
||||
<motion.h1
|
||||
initial={{ opacity: 0 }}
|
||||
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
|
||||
<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.h2
|
||||
layout
|
||||
@@ -50,18 +52,18 @@ export default function Landing() {
|
||||
opacity: 0,
|
||||
}}
|
||||
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) => (
|
||||
<motion.span
|
||||
key={index}
|
||||
className="text-center"
|
||||
className='text-center'
|
||||
>
|
||||
{word}
|
||||
</motion.span>
|
||||
))}
|
||||
<AnimatePresence
|
||||
mode="wait"
|
||||
mode='wait'
|
||||
initial={false}
|
||||
>
|
||||
<motion.span
|
||||
@@ -69,7 +71,7 @@ export default function Landing() {
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="text-primary font-bold"
|
||||
className='text-primary font-bold'
|
||||
>
|
||||
{words[currentWord]}
|
||||
</motion.span>
|
||||
@@ -77,27 +79,27 @@ export default function Landing() {
|
||||
</motion.h2>
|
||||
<AnimatePresence>
|
||||
<motion.label
|
||||
key="search-bar"
|
||||
layoutId="search-bar"
|
||||
key='search-bar'
|
||||
layoutId='search-bar'
|
||||
initial={{ opacity: 0 }}
|
||||
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
|
||||
type="text"
|
||||
placeholder="search by game or appid..."
|
||||
type='text'
|
||||
placeholder='search by game or appid...'
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
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
|
||||
onClick={handleSearchSubmit}
|
||||
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
|
||||
</motion.button>
|
||||
</motion.label>
|
||||
@@ -105,26 +107,41 @@ export default function Landing() {
|
||||
<motion.small
|
||||
initial={{ opacity: 0 }}
|
||||
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
|
||||
title="Visit our Github Repository"
|
||||
href="https://github.com/AdrianBonpin/deckyvault"
|
||||
className="text-accent opacity-60 hover:opacity-100 transition-opacity cursor-pointer"
|
||||
title='Visit our Github Repository'
|
||||
href='/updates'
|
||||
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.
|
||||
</Link>
|
||||
<span className="opacity-60">v{process.env.NEXT_PUBLIC_APP_VERSION}</span>
|
||||
</motion.small>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
name: "DeckyVault",
|
||||
url: "https://deckyvault.xyz",
|
||||
description: "Steam Deck benchmarks, settings, and performance guides",
|
||||
description:
|
||||
"Steam Deck benchmarks, settings, and performance guides",
|
||||
potentialAction: {
|
||||
"@type": "SearchAction",
|
||||
target: "https://deckyvault.xyz/search?q={search_term_string}",
|
||||
|
||||
@@ -43,7 +43,7 @@ export default async function UpdatePage({
|
||||
|
||||
return (
|
||||
<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
|
||||
href="/updates"
|
||||
className="inline-flex items-center gap-1 text-sm text-text/60 hover:text-primary transition-colors"
|
||||
|
||||
+34
-28
@@ -5,36 +5,42 @@ import { UpdateCard } from "@/components/updates/update-card"
|
||||
import { ArrowLeftIcon } from "lucide-react"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Updates",
|
||||
description: "See what's new in DeckyVault — version release notes, features, and improvements.",
|
||||
title: "Updates",
|
||||
description:
|
||||
"See what's new in DeckyVault — version release notes, features, and improvements.",
|
||||
}
|
||||
|
||||
export default function UpdatesPage() {
|
||||
const updates = getAllUpdates()
|
||||
const updates = getAllUpdates()
|
||||
|
||||
return (
|
||||
<main className="w-full max-w-2xl mx-auto px-4 py-8">
|
||||
<div className="flex flex-row items-center gap-2 mb-6">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-text/60 hover:text-primary transition-colors"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold">Updates</h1>
|
||||
</div>
|
||||
<p className="text-text/60 mb-8">
|
||||
Release notes and changelogs for every DeckyVault version.
|
||||
</p>
|
||||
{updates.length === 0 ? (
|
||||
<p className="text-text/40 text-center py-16">No updates yet.</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
{updates.map((update) => (
|
||||
<UpdateCard key={update.slug} update={update} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
return (
|
||||
<main className='w-full max-w-7xl mx-auto px-4 py-8'>
|
||||
<div className='flex flex-row items-center gap-2 mb-6'>
|
||||
<Link
|
||||
href='/'
|
||||
className='text-text/60 hover:text-primary transition-colors'
|
||||
>
|
||||
<ArrowLeftIcon className='w-4 h-4' />
|
||||
</Link>
|
||||
<h1 className='text-2xl font-bold'>Updates</h1>
|
||||
</div>
|
||||
<p className='text-text/60 mb-8'>
|
||||
Release notes, changelogs, and the like for DeckyVault.
|
||||
</p>
|
||||
{updates.length === 0 ? (
|
||||
<p className='text-text/40 text-center py-16'>
|
||||
No updates yet.
|
||||
</p>
|
||||
) : (
|
||||
<div className='flex flex-col gap-3'>
|
||||
{updates.map((update) => (
|
||||
<UpdateCard
|
||||
key={update.slug}
|
||||
update={update}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/node": "^20",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@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/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/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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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-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-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=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -31,6 +31,7 @@ export function CommentSection({ gameId, initialCount }: CommentSectionProps) {
|
||||
const limit = 20
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
"use client";
|
||||
|
||||
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";
|
||||
|
||||
interface SavedFilter {
|
||||
id: string;
|
||||
name: string;
|
||||
filters: Record<string, any>;
|
||||
filters: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface SavedFiltersProps {
|
||||
currentFilters: Record<string, any>;
|
||||
onLoad: (filters: Record<string, any>) => void;
|
||||
currentFilters: Record<string, unknown>;
|
||||
onLoad: (filters: Record<string, unknown>) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { ThumbsUp, ThumbsDown, ExternalLink, MessageSquare } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -42,7 +42,7 @@ export function SteamReviews({ gameId, steamAppId, className }: SteamReviewsProp
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
const fetchReviews = async (newOffset: number) => {
|
||||
const fetchReviews = useCallback(async (newOffset: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
@@ -64,11 +64,12 @@ export function SteamReviews({ gameId, steamAppId, className }: SteamReviewsProp
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [gameId, setData, setLoading, setError, setOffset]);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
fetchReviews(0);
|
||||
}, [gameId]);
|
||||
}, [gameId, fetchReviews]);
|
||||
|
||||
// Loading skeleton
|
||||
if (loading && !data) {
|
||||
|
||||
@@ -2,26 +2,30 @@ import Link from "next/link"
|
||||
import type { UpdateMeta } from "@/lib/updates"
|
||||
|
||||
export function UpdateCard({ update }: { update: UpdateMeta }) {
|
||||
const formattedDate = new Date(update.date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
const formattedDate = new Date(update.date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
|
||||
return (
|
||||
<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">
|
||||
<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">
|
||||
v{update.version}
|
||||
</span>
|
||||
<time className="text-xs text-text/60">{formattedDate}</time>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold group-hover:text-primary transition-colors">
|
||||
{update.title}
|
||||
</h3>
|
||||
<p className="text-sm text-text/60 line-clamp-2">{update.summary}</p>
|
||||
</article>
|
||||
</Link>
|
||||
)
|
||||
return (
|
||||
<Link href={`/updates/${update.slug}`}>
|
||||
<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'>
|
||||
<span className='text-xs font-mono px-2 py-0.5 rounded bg-primary/10 text-primary border border-primary/20'>
|
||||
v{update.version}
|
||||
</span>
|
||||
<time className='text-xs text-text/60'>
|
||||
{formattedDate}
|
||||
</time>
|
||||
</div>
|
||||
<h3 className='text-lg font-semibold group-hover:text-primary transition-colors'>
|
||||
{update.title}
|
||||
</h3>
|
||||
<p className='text-sm text-text/60 line-clamp-2'>
|
||||
{update.summary}
|
||||
</p>
|
||||
</article>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function UpdateViewer({
|
||||
</aside>
|
||||
|
||||
{/* 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">
|
||||
<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">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation"
|
||||
import { motion, AnimatePresence } from "motion/react"
|
||||
import { StepIndicator } from "@/components/wizard/step-indicator"
|
||||
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 { SettingsStep } from "@/components/wizard/steps/settings-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)
|
||||
|
||||
if (entry) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setAntiCheat({
|
||||
antiCheatRelevant: entry.antiCheatRelevant,
|
||||
antiCheatName: entry.antiCheatName ?? "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
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"
|
||||
|
||||
export type AntiCheatData = {
|
||||
@@ -61,7 +61,7 @@ export function AntiCheatStep({
|
||||
value,
|
||||
onChange,
|
||||
}: AntiCheatStepProps) {
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [isEditing] = useState(false)
|
||||
|
||||
// Find the best existing entry to prefill:
|
||||
// Prefer the entry for the currently selected hardware,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
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 type { SettingCategory } from "@/components/wizard/settings-editor"
|
||||
import type { PerformanceData } from "./performance-step"
|
||||
|
||||
@@ -33,11 +33,13 @@ interface SetupStepProps {
|
||||
}
|
||||
|
||||
export function SetupStep({
|
||||
gameId,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
gameId: _gameId,
|
||||
gameVersions,
|
||||
hardwareSlug,
|
||||
onHardwareChange,
|
||||
hardwareName,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
hardwareName: _hardwareName,
|
||||
selectedVersionId,
|
||||
onVersionChange,
|
||||
newVersionString,
|
||||
|
||||
@@ -4,6 +4,15 @@ date: "2026-04-30"
|
||||
version: "2026.0.95"
|
||||
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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Elysia, t } from "elysia"
|
||||
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 { 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" }
|
||||
}
|
||||
|
||||
const currentValue = String((game as any)[fieldName] ?? "")
|
||||
const currentValue = String((game as Record<string, unknown>)[fieldName] ?? "")
|
||||
|
||||
const [suggestion] = await db
|
||||
.insert(communitySuggestions)
|
||||
@@ -149,9 +149,9 @@ export const communitySuggestionRoutes = new Elysia({
|
||||
async ({ params, query }) => {
|
||||
const status = query.status // optional filter
|
||||
|
||||
let conditions = [eq(communitySuggestions.gameId, params.gameId)]
|
||||
const conditions = [eq(communitySuggestions.gameId, params.gameId)]
|
||||
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
|
||||
@@ -263,7 +263,7 @@ export const communitySuggestionRoutes = new Elysia({
|
||||
|
||||
// If approved, apply the change to the game
|
||||
if (status === "approved") {
|
||||
const updateData: Record<string, any> = {}
|
||||
const updateData: Record<string, string> = {}
|
||||
updateData[suggestion.fieldName] = suggestion.proposedValue
|
||||
|
||||
await db
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
gamePlatformSupport,
|
||||
hardware,
|
||||
} 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"
|
||||
|
||||
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) ────
|
||||
if (minFps || maxFps) {
|
||||
const fpsConditions = [
|
||||
const fpsConditions: (SQL | undefined)[] = [
|
||||
eq(performanceEntries.isRemoved, false),
|
||||
minFps ? gte(performanceEntries.fpsAvg, Number(minFps)) : undefined,
|
||||
maxFps ? lte(performanceEntries.fpsAvg, Number(maxFps)) : undefined,
|
||||
].filter(Boolean) as any[]
|
||||
].filter((c): c is SQL => c !== undefined)
|
||||
|
||||
const fpsSubquery = db
|
||||
.select({ gameId: gameVersions.gameId })
|
||||
@@ -127,7 +127,7 @@ export const gamesListingRoutes = new Elysia({ prefix: "/games/listing" }).get(
|
||||
|
||||
// ── Proton / Native filter ────────────────────────────────────
|
||||
if (protonNative && ["proton", "native", "both"].includes(protonNative)) {
|
||||
const protonConditions: any[] = []
|
||||
const protonConditions: SQL[] = []
|
||||
|
||||
if (protonNative === "proton" || protonNative === "both") {
|
||||
protonConditions.push(eq(gamePlatformSupport.protonStatus, "proton"))
|
||||
@@ -148,7 +148,7 @@ export const gamesListingRoutes = new Elysia({ prefix: "/games/listing" }).get(
|
||||
// ── Anti-cheat status filter ──────────────────────────────────
|
||||
const validAcStatuses = ["supported", "unsupported", "unknown", "none"]
|
||||
if (antiCheatStatus && antiCheatStatus !== "any" && validAcStatuses.includes(antiCheatStatus)) {
|
||||
const acConditions: any[] = [
|
||||
const acConditions: SQL[] = [
|
||||
eq(gamePlatformSupport.antiCheatRelevant, true),
|
||||
eq(gamePlatformSupport.antiCheatStatus, antiCheatStatus as "none" | "supported" | "unsupported" | "unknown"),
|
||||
]
|
||||
|
||||
+3
-42
@@ -190,46 +190,7 @@ export const gameVersionsRoutes = new Elysia({ prefix: "/games/:gameId/versions"
|
||||
|
||||
// ── Game Sync Routes ────────────────────────────────────────────────
|
||||
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" })
|
||||
// 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 stream = new ReadableStream({
|
||||
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"))
|
||||
}
|
||||
|
||||
@@ -294,7 +256,6 @@ export const gameSyncRoutes = new Elysia({ prefix: "/games" })
|
||||
|
||||
let synced = 0
|
||||
let failed = 0
|
||||
const batchSize = SYNC_CONCURRENCY
|
||||
|
||||
// Process syncs sequentially with delay to avoid rate limiting
|
||||
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 })
|
||||
if (result.success) synced++
|
||||
else failed++
|
||||
} catch (e) {
|
||||
} catch {
|
||||
failed++
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Elysia } from "elysia";
|
||||
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 { requireContributorOrAdmin } from "@/lib/auth/guard";
|
||||
|
||||
@@ -95,7 +95,7 @@ export async function recalculatePlayability(gameId: string): Promise<{
|
||||
await db
|
||||
.update(gamePlatformSupport)
|
||||
.set({
|
||||
playabilityStatus: status as any,
|
||||
playabilityStatus: status as typeof playabilityStatusEnum.enumValues[number],
|
||||
playabilityCalculatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
@@ -111,13 +111,13 @@ export async function recalculatePlayability(gameId: string): Promise<{
|
||||
|
||||
// Update aggregate game-level playability (worst of all devices)
|
||||
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) {
|
||||
if (
|
||||
priority[r.playabilityStatus 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
|
||||
.update(games)
|
||||
.set({
|
||||
playabilityStatus: worstStatus as any,
|
||||
playabilityStatus: worstStatus as typeof playabilityStatusEnum.enumValues[number],
|
||||
playabilityCalculatedAt: new Date(),
|
||||
})
|
||||
.where(eq(games.id, gameId));
|
||||
@@ -174,7 +174,7 @@ export const playabilityRoutes = new Elysia({ prefix: "/playability" })
|
||||
if (hardwareSlug) {
|
||||
await db
|
||||
.update(gamePlatformSupport)
|
||||
.set({ playabilityStatus: status as any, playabilityOverride: true })
|
||||
.set({ playabilityStatus: status as typeof playabilityStatusEnum.enumValues[number], playabilityOverride: true })
|
||||
.where(
|
||||
and(
|
||||
eq(gamePlatformSupport.gameId, params.gameId),
|
||||
@@ -184,7 +184,7 @@ export const playabilityRoutes = new Elysia({ prefix: "/playability" })
|
||||
} else {
|
||||
await db
|
||||
.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));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { eq } from "drizzle-orm"
|
||||
// In-memory cache for reviews (key -> { data, expires })
|
||||
const reviewCache = new Map<
|
||||
string,
|
||||
{ data: any; expires: number }
|
||||
{ data: SteamReviewResponse; expires: number }
|
||||
>()
|
||||
|
||||
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 })
|
||||
|
||||
return slicedData
|
||||
} catch (error) {
|
||||
} catch {
|
||||
set.status = 502
|
||||
return { error: "Steam review API unavailable" }
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
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 { recalculatePlayability } from "@/lib/api/playability"
|
||||
|
||||
@@ -247,7 +247,7 @@ export async function syncSteamGame(
|
||||
metacriticUrl: d.metacritic?.url ?? null,
|
||||
recommendationsTotal: d.recommendations?.total ?? null,
|
||||
steamReviewScore: reviewData.reviewScore,
|
||||
steamReviewSentiment: reviewData.reviewSentiment as any,
|
||||
steamReviewSentiment: reviewData.reviewSentiment as typeof steamReviewSentimentEnum.enumValues[number] | null,
|
||||
steamReviewCount: reviewData.reviewCount,
|
||||
priceCurrent: d.price_overview?.final ?? null,
|
||||
priceInitial: d.price_overview?.initial ?? null,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { NextConfig } from "next";
|
||||
import { version } from "./package.json";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
env: {
|
||||
NEXT_PUBLIC_APP_VERSION: version,
|
||||
},
|
||||
// Elysia is Bun-native and must not be bundled by Next.js
|
||||
serverExternalPackages: ["elysia", "@elysiajs/eden"],
|
||||
images: {
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/node": "^20",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "^19",
|
||||
|
||||
Reference in New Issue
Block a user