chore: format components, switch to CDN hostname, and improve error handling in game stats

- Format long lines and union types in game page, preset modal, games page, and filter drawer
- Replace **.r2.dev with cdn.deckyvault.xyz in next.config image hostnames
- Replace sql ANY with drizzle inArray for hardware query in game stats
- Add try/catch error handler in game stats route
- Add post.md to .gitignore
This commit is contained in:
2026-05-10 04:15:38 +08:00
parent f02408c0c7
commit bcf2d540b8
7 changed files with 2251 additions and 1780 deletions
+74 -72
View File
@@ -5,84 +5,86 @@ import { XIcon } from "lucide-react"
import { motion, AnimatePresence } from "motion/react"
interface FilterDrawerProps {
isOpen: boolean
onClose: () => void
children: React.ReactNode
isOpen: boolean
onClose: () => void
children: React.ReactNode
}
export function FilterDrawer({ isOpen, onClose, children }: FilterDrawerProps) {
const drawerRef = useRef<HTMLDivElement>(null)
const drawerRef = useRef<HTMLDivElement>(null)
// Close on Escape
useEffect(() => {
if (!isOpen) return
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose()
}
document.addEventListener("keydown", handleKeyDown)
return () => document.removeEventListener("keydown", handleKeyDown)
}, [isOpen, onClose])
// Close on Escape
useEffect(() => {
if (!isOpen) return
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose()
}
document.addEventListener("keydown", handleKeyDown)
return () => document.removeEventListener("keydown", handleKeyDown)
}, [isOpen, onClose])
// Prevent body scroll when open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden"
} else {
document.body.style.overflow = ""
}
return () => {
document.body.style.overflow = ""
}
}, [isOpen])
// Prevent body scroll when open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden"
} else {
document.body.style.overflow = ""
}
return () => {
document.body.style.overflow = ""
}
}, [isOpen])
// Focus trap
useEffect(() => {
if (isOpen && drawerRef.current) {
const firstFocusable = drawerRef.current.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
)
firstFocusable?.focus()
}
}, [isOpen])
// Focus trap
useEffect(() => {
if (isOpen && drawerRef.current) {
const firstFocusable = drawerRef.current.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
)
firstFocusable?.focus()
}
}, [isOpen])
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm lg:hidden"
onClick={onClose}
/>
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className='fixed inset-0 z-40 bg-black/60 backdrop-blur-sm lg:hidden'
onClick={onClose}
/>
{/* Drawer */}
<motion.div
ref={drawerRef}
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", damping: 30, stiffness: 300 }}
className="fixed top-0 right-0 bottom-0 z-50 w-[85vw] max-w-sm overflow-y-auto bg-background border-l border-border lg:hidden"
>
<div className="sticky top-0 z-10 flex items-center justify-between p-4 border-b border-border bg-background">
<h2 className="text-sm font-semibold">Filters</h2>
<button
onClick={onClose}
className="p-1.5 rounded-md hover:bg-text/5 transition-colors cursor-pointer"
aria-label="Close filters"
>
<XIcon className="h-4 w-4 text-text/50" />
</button>
</div>
<div className="p-4">
{children}
</div>
</motion.div>
</>
)}
</AnimatePresence>
)
{/* Drawer */}
<motion.div
ref={drawerRef}
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{
type: "spring",
damping: 30,
stiffness: 300,
}}
className='fixed top-0 right-0 bottom-0 z-50 w-[85vw] max-w-sm overflow-y-auto bg-background border-l border-border lg:hidden'
>
<div className='sticky top-0 z-10 flex items-center justify-between p-4 border-b border-border bg-background'>
<h2 className='text-sm font-semibold'>Filters</h2>
<button
onClick={onClose}
className='p-1.5 rounded-md hover:bg-text/5 transition-colors cursor-pointer'
aria-label='Close filters'
>
<XIcon className='h-4 w-4 text-text/50' />
</button>
</div>
<div className='p-4'>{children}</div>
</motion.div>
</>
)}
</AnimatePresence>
)
}