feat: mobile hamburger menu with full-screen overlay for categories

This commit is contained in:
achmad
2026-05-29 03:24:18 +07:00
parent c4727d87aa
commit 73a5596857
+112 -36
View File
@@ -1,8 +1,9 @@
'use client' 'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import { Search } from 'lucide-react' import { Search, Menu, X } from 'lucide-react'
import type { Category } from '@/lib/types' import type { Category } from '@/lib/types'
interface Props { interface Props {
@@ -11,50 +12,125 @@ interface Props {
export default function NavbarClient({ categories }: Props) { export default function NavbarClient({ categories }: Props) {
const pathname = usePathname() const pathname = usePathname()
const [menuOpen, setMenuOpen] = useState(false)
useEffect(() => {
setMenuOpen(false)
}, [pathname])
useEffect(() => {
if (menuOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => { document.body.style.overflow = '' }
}, [menuOpen])
function openSearch() { function openSearch() {
window.dispatchEvent(new CustomEvent('kotobane:open-search')) window.dispatchEvent(new CustomEvent('kotobane:open-search'))
} }
return ( return (
<nav className="bg-bg-elevated border-b border-border sticky top-0 z-40"> <>
<div className="max-w-[1200px] mx-auto px-6 h-14 flex items-center gap-6"> <nav className="bg-bg-elevated border-b border-border sticky top-0 z-40">
<Link href="/" className="text-lg font-black text-accent tracking-[3px] shrink-0"> <div className="max-w-[1200px] mx-auto px-4 h-14 flex items-center">
{/* Mobile hamburger */}
</Link> <button
onClick={() => setMenuOpen(true)}
className="sm:hidden mr-3 text-text-muted hover:text-text-secondary transition-colors"
aria-label="Open menu"
>
<Menu size={20} />
</button>
<div className="w-px h-4 bg-border shrink-0" /> {/* Logo — center on mobile, left on desktop */}
<Link
href="/"
className="text-lg font-black text-accent tracking-[3px] shrink-0 sm:mr-6 sm:ml-0 mx-auto sm:mx-0"
>
</Link>
<div className="flex items-center gap-1 overflow-x-auto"> {/* Desktop nav links */}
{categories.map((cat) => { <div className="hidden sm:flex items-center gap-1 flex-1 overflow-x-auto">
const isActive = pathname.startsWith(`/${cat.slug}`) <div className="w-px h-4 bg-border shrink-0 mr-4" />
return ( {categories.map((cat) => {
<Link const isActive = pathname.startsWith(`/${cat.slug}`)
key={cat.slug} return (
href={`/${cat.slug}`} <Link
className={`text-xs font-medium px-3 py-1 rounded-sm whitespace-nowrap transition-colors ${ key={cat.slug}
isActive href={`/${cat.slug}`}
? 'text-accent' className={`text-xs font-medium px-3 py-1 rounded-sm whitespace-nowrap transition-colors outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg-elevated ${
: 'text-text-muted hover:text-text-secondary' isActive
}`} ? 'text-accent'
> : 'text-text-muted hover:text-text-secondary'
{cat.name} }`}
</Link> >
) {cat.name}
})} </Link>
)
})}
</div>
{/* Search */}
<button
onClick={openSearch}
className="ml-auto sm:ml-0 flex items-center gap-2 text-text-muted hover:text-text-secondary transition-colors shrink-0"
aria-label="Open search"
>
<Search size={16} />
<kbd className="hidden sm:inline-flex bg-bg-card border border-border rounded px-1.5 py-0.5 text-[10px] font-mono">
K
</kbd>
</button>
</div> </div>
</nav>
<button {/* Mobile overlay */}
onClick={openSearch} {menuOpen && (
className="ml-auto flex items-center gap-2 text-text-muted hover:text-text-secondary transition-colors shrink-0" <div className="fixed inset-0 z-50 sm:hidden">
aria-label="Open search" <div className="absolute inset-0 bg-black/60" onClick={() => setMenuOpen(false)} />
> <div className="relative w-full h-full bg-bg-elevated flex flex-col">
<Search size={16} /> {/* Overlay header */}
<kbd className="hidden sm:inline-flex bg-bg-card border border-border rounded px-1.5 py-0.5 text-[10px] font-mono"> <div className="flex items-center justify-between px-4 h-14 border-b border-border">
K <span className="text-lg font-black text-accent tracking-[3px]"></span>
</kbd> <button
</button> onClick={() => setMenuOpen(false)}
</div> className="text-text-muted hover:text-text-secondary transition-colors"
</nav> aria-label="Close menu"
>
<X size={22} />
</button>
</div>
{/* Category list */}
<div className="flex-1 overflow-y-auto py-4">
{categories.map((cat) => {
const isActive = pathname.startsWith(`/${cat.slug}`)
return (
<Link
key={cat.slug}
href={`/${cat.slug}`}
className={`block px-6 py-3.5 text-sm font-medium transition-colors ${
isActive
? 'text-accent bg-bg-card border-l-2 border-accent'
: 'text-text-muted hover:text-text-secondary hover:bg-bg-card'
}`}
>
{cat.name}
{cat.description && (
<span className="block text-[11px] text-text-disabled font-normal mt-0.5">
{cat.description}
</span>
)}
</Link>
)
})}
</div>
</div>
</div>
)}
</>
) )
} }