"use client" import { AnimatePresence, motion } from "motion/react" import logo from "@/app/icon.png" import Image from "next/image" import Link from "next/link" import { useEffect, useSyncExternalStore, useRef, useState } from "react" import { Bookmark, CircleXIcon, Gamepad2Icon, LogOut, MenuIcon, User, XIcon, } from "lucide-react" import { routes, authRoutes } from "@/lib/routes" import { usePathname, useRouter, useSearchParams } from "next/navigation" import { useDebounce } from "@/lib/hooks/useDebounce" import { authClient, useSession } from "@/lib/auth-client" export default function Navbar() { const pathname = usePathname() const router = useRouter() const searchParams = useSearchParams() const isLanding = pathname === "/" const isAuthRoute = pathname.startsWith("/login") || pathname.startsWith("/signup") || pathname.startsWith("/forgot-password") || pathname.startsWith("/reset-password") const [searchQuery, setSearchQuery] = useState( () => searchParams.get("q") || "", ) const debouncedQuery = useDebounce(searchQuery, 300) const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [isFocused, setIsFocused] = useState(false) const [forceFocusStyles, setForceFocusStyles] = useState(false) const inputRef = useRef(null) // When syncing state from the URL (e.g. after navigating from the landing // page), skip the next URL-write cycle so the stale/empty debounced value // doesn't overwrite the URL params before it catches up. const skipNextUrlWrite = useRef(false) const { data: session, isPending: isSessionLoading } = useSession() const [userMenuOpen, setUserMenuOpen] = useState(false) // Prevent hydration mismatch: useSession resolves differently on // server (isPending=true) vs client. We delay rendering the // auth-dependent UI until after the first client paint. // useSyncExternalStore avoids the React lint warning about setState in effects. const subscribe = () => () => {} const mounted = useSyncExternalStore(subscribe, () => true, () => false) const showAuth = mounted && !isSessionLoading // Sync search query with URL ?q= param const searchQueryRef = useRef(searchQuery) useEffect(() => { searchQueryRef.current = searchQuery }, [searchQuery]) useEffect(() => { const q = searchParams.get("q") || "" if (q === searchQueryRef.current) return // If the URL has a different value than our state, we're syncing after // a navigation — skip the next URL-write to avoid clearing the param skipNextUrlWrite.current = true const id = setTimeout(() => setSearchQuery(q), 0) return () => clearTimeout(id) }, [searchParams]) // Update URL when debounced query changes (skip if already matches) useEffect(() => { // Skip one cycle after syncing from URL so the stale/empty debounced // value doesn't overwrite the URL params before it catches up if (skipNextUrlWrite.current) { skipNextUrlWrite.current = false return } if (isLanding) return const currentQ = searchParams.get("q") || "" if (debouncedQuery === currentQ) return // Don't overwrite URL if the typed query hasn't debounced yet if (searchQuery !== debouncedQuery) return const params = new URLSearchParams(searchParams.toString()) if (debouncedQuery) { params.set("q", debouncedQuery) } else { params.delete("q") } router.replace(`/search?${params.toString()}`, { scroll: false }) }, [debouncedQuery, isLanding, router, searchParams, searchQuery]) // Maintain focus & styles when flying from landing page search useEffect(() => { if ( !isLanding && searchQuery && sessionStorage.getItem("focusSearch") === "true" ) { sessionStorage.removeItem("focusSearch") const focusTimeout = setTimeout(() => setForceFocusStyles(true), 0) // Focus the input after the layout animation element mounts requestAnimationFrame(() => { inputRef.current?.focus() }) const timer = setTimeout(() => setForceFocusStyles(false), 450) return () => { clearTimeout(focusTimeout) clearTimeout(timer) } } }, [isLanding, searchQuery]) const handleSearchChange = (e: React.ChangeEvent) => { setSearchQuery(e.target.value) } const handleSearchSubmit = () => { if (searchQuery.trim()) { router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`) } } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { handleSearchSubmit() } } const navbarRoutes = routes.filter( (route) => route.href !== "/" && route.href !== "/search", ) // Hide navbar on auth routes if (isAuthRoute) { return null } return ( <> DeckyVault Logo {!isLanding && ( DeckyVault )} {!isLanding && ( setIsFocused(true)} onBlur={() => setIsFocused(false)} className='flex-1 outline-none bg-transparent text-sm min-w-0' /> { setSearchQuery("") if (!isLanding) { const params = new URLSearchParams( searchParams.toString(), ) params.delete("q") router.replace( `/search?${params.toString()}`, { scroll: false, }, ) } }} className={`h-3 w-3 transition-color cursor-default hover:stroke-accent transition-all ${ isFocused || forceFocusStyles ? "opacity-100" : "opacity-0" }`} /> )} {/* Desktop Navigation Links */}
    {navbarRoutes.map((route) => ( {route.title} ))} {!showAuth ? ( ) : session ? ( {userMenuOpen && ( <>
    setUserMenuOpen(false) } />
    {authRoutes.map((route) => ( setUserMenuOpen( false, ) } className='w-full flex items-center gap-2 px-3 py-2 text-sm text-text/70 hover:text-text hover:bg-text/5 transition-colors cursor-pointer' > {route.icon === "User" && ( )} {route.icon === "Bookmark" && ( )} {route.title} ))}
    )} ) : ( login )}
{/* Mobile Hamburger Button */}
{/* Mobile Sidebar Overlay */} {mobileMenuOpen && ( <> setMobileMenuOpen(false)} />
Menu
{/* Mobile Auth Controls */}
{!showAuth ? (
) : session ? (
{authRoutes.map((route) => ( setMobileMenuOpen(false) } className='flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-text/70 hover:text-text hover:bg-text/5 transition-colors cursor-pointer' > {route.icon === "User" && ( )} {route.icon === "Bookmark" && ( )} {route.title} ))}
) : (
setMobileMenuOpen(false) } className='block w-full text-center px-3 py-2 rounded-lg border border-white/10 text-sm text-text hover:bg-text/5 transition-colors cursor-pointer' > Login
)}
)} ) }