feat: initial landing and search pages

This commit is contained in:
2026-04-25 21:01:00 +08:00
parent 327dce2b19
commit 592b0c7b87
6 changed files with 381 additions and 35 deletions
+2
View File
@@ -8,6 +8,8 @@
--color-secondary: #571b8b;
--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);
/* Defaults */
--default-transition-duration: 0.3s;
+6 -1
View File
@@ -2,6 +2,8 @@ import type { Metadata } from "next"
import { Lexend } from "next/font/google"
import "./globals.css"
import Script from "next/script"
import Navbar from "@/components/navbar"
import { Suspense } from "react"
const font = Lexend({
variable: "--font-lexend",
@@ -80,7 +82,10 @@ export default function RootLayout({
lang='en'
className={`${font.variable} bg-background text-text antialiased overscroll-none`}
>
<body className='min-h-full flex flex-col relative'>
<body className='min-h-full w-dvw flex flex-col relative'>
<Suspense>
<Navbar />
</Suspense>
{children}
<Script
src='https://analytics.ranlabs.space/api/script.js'
+67 -34
View File
@@ -2,12 +2,13 @@
import { AnimatePresence, motion } from "motion/react"
import { useState } from "react"
import logo from "@/app/icon.png"
import Image from "next/image"
import Link from "next/link"
import { Gamepad2Icon, SearchIcon } from "lucide-react"
import { useRouter } from "next/navigation"
export default function Landing() {
const words = ["benchmarks", "settings", "guides", "reviews"]
const router = useRouter()
const words = ["benchmarks", "settings", "reviews"]
const [currentWord, setCurrentWord] = useState(0)
@@ -15,39 +16,51 @@ export default function Landing() {
setCurrentWord((currentWord + 1) % words.length)
}, 2000)
const [searchQuery, setSearchQuery] = useState("")
const handleSearchSubmit = () => {
if (searchQuery.trim()) {
sessionStorage.setItem("focusSearch", "true")
router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`)
}
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
handleSearchSubmit()
}
}
return (
<section
id='hero'
className='w-dvw h-dvh 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"
>
<Image
src={logo}
alt=''
className='h-24 md:h-30 w-auto'
loading='eager'
/>
<motion.h1
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className='font-bold text-3xl md:text-5xl text-center'
className="font-bold text-3xl md:text-5xl text-center"
>
DeckyVault
</motion.h1>
<motion.h2
layout
initial={{
opacity: 0,
}}
animate={{ opacity: 1, transition: { delay: 0.5 } }}
className='mt-4 flex flex-col items-center font-semibold text-lg md:text-2xl'
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"
>
<motion.span
key='intro'
className='text-center'
>
A fast, modern browser for finding game
</motion.span>
{"Find your game".split(" ").map((word, index) => (
<motion.span
key={index}
className="text-center"
>
{word}
</motion.span>
))}
<AnimatePresence
mode='wait'
mode="wait"
initial={false}
>
<motion.span
@@ -55,29 +68,49 @@ 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>
</AnimatePresence>
</motion.h2>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 0.6, transition: { delay: 1 } }}
className='mt-4 text-center md:text-lg max-w-xl'
>
Stay tuned for the launch of DeckyVault
</motion.p>
<AnimatePresence>
<motion.label
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"
>
<Gamepad2Icon className="h-6 w-6 group-focus-within:stroke-accent transition-colors shrink-0" />
<input
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"
/>
<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"
>
<SearchIcon className="h-3 w-3" />
search
</motion.button>
</motion.label>
</AnimatePresence>
<motion.small
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 1.5 } }}
className='absolute bottom-4 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>
<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'
title="Visit our Github Repository"
href="https://github.com/AdrianBonpin/deckyvault"
className="text-accent opacity-60 hover:opacity-100 transition-opacity"
>
Github.
</Link>
+52
View File
@@ -0,0 +1,52 @@
"use client"
import { Suspense } from "react"
import { useSearchParams } from "next/navigation"
import { motion } from "motion/react"
import { Gamepad2Icon } from "lucide-react"
function SearchContent() {
const searchParams = useSearchParams()
const query = searchParams.get("q") || ""
return (
<section className="w-full min-h-[calc(100vh-3.6rem)] flex flex-col items-center p-4 md:px-[10svw]">
<div className="w-full max-w-4xl mt-8">
<h1 className="text-2xl font-bold mb-2">
{query ? `Search results for "${query}"` : "Search"}
</h1>
<p className="text-text/60 text-sm mb-8">
{query
? "Showing results from the database..."
: "Enter a game name or AppID to find benchmarks, settings, and reviews."}
</p>
{!query && (
<div className="flex flex-col items-center justify-center py-20 gap-4">
<Gamepad2Icon className="h-12 w-12 text-text/20" />
<p className="text-text/40 text-sm">
Start typing to search for games
</p>
</div>
)}
{query && (
<div className="flex flex-col items-center justify-center py-20 gap-4">
<div className="animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" />
<p className="text-text/60 text-sm">
Searching for &quot;{query}&quot;...
</p>
</div>
)}
</div>
</section>
)
}
export default function SearchPage() {
return (
<Suspense>
<SearchContent />
</Suspense>
)
}