fix: apply designer agent improvements to src/components/public/PublicHeader.tsx
This commit is contained in:
parent
f4f8fae43e
commit
1aa365177f
|
|
@ -0,0 +1,194 @@
|
|||
'use client'
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Menu, X, Zap } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
{ label: 'About', href: '/about' },
|
||||
] as const
|
||||
|
||||
export default function PublicHeader() {
|
||||
const pathname = usePathname()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const mobileMenuRef = useRef<HTMLDivElement>(null)
|
||||
const hamburgerRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
/* Scroll shadow */
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 12)
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
return () => window.removeEventListener('scroll', onScroll)
|
||||
}, [])
|
||||
|
||||
/* Close mobile menu on route change */
|
||||
useEffect(() => { setOpen(false) }, [pathname])
|
||||
|
||||
/* Trap focus inside mobile menu */
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const focusable = mobileMenuRef.current?.querySelectorAll<HTMLElement>(
|
||||
'a, button, [tabindex]:not([tabindex="-1"])'
|
||||
)
|
||||
focusable?.[0]?.focus()
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setOpen(false)
|
||||
hamburgerRef.current?.focus()
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Skip link */}
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4
|
||||
focus:z-[100] focus:px-4 focus:py-2 focus:rounded-lg
|
||||
focus:bg-brand-600 focus:text-white focus:shadow-glow-sm
|
||||
focus:text-sm focus:font-medium transition-all"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
|
||||
<header
|
||||
role="banner"
|
||||
className={clsx(
|
||||
'sticky top-0 z-50 w-full transition-all duration-300',
|
||||
scrolled
|
||||
? 'bg-white/90 backdrop-blur-md shadow-card border-b border-slate-100'
|
||||
: 'bg-transparent'
|
||||
)}
|
||||
>
|
||||
<div className="section">
|
||||
<div className="container-cms">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="CupaDev CMS — Home"
|
||||
className="flex items-center gap-2.5 group focus-visible:outline-none
|
||||
focus-visible:ring-2 focus-visible:ring-brand-500 rounded-lg"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-lg
|
||||
bg-brand-600 text-white shadow-glow-sm
|
||||
group-hover:bg-brand-700 transition-colors"
|
||||
>
|
||||
<Zap size={16} strokeWidth={2.5} />
|
||||
</span>
|
||||
<span className="font-display font-semibold text-ink text-sm tracking-tight">
|
||||
CupaDev
|
||||
<span className="text-brand-600 ml-0.5">CMS</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop nav */}
|
||||
<nav aria-label="Main navigation" className="hidden md:flex items-center gap-1">
|
||||
{NAV_LINKS.map(({ label, href }) => {
|
||||
const active = pathname === href
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
className={clsx(
|
||||
'btn-ghost text-sm font-medium rounded-lg px-4 py-2 transition-colors',
|
||||
active
|
||||
? 'text-brand-600 bg-brand-50'
|
||||
: 'text-ink-muted hover:text-ink hover:bg-surface-subtle'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Desktop CTA */}
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
<Link href="/admin" className="btn-primary text-sm px-5 py-2.5">
|
||||
Admin
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile hamburger */}
|
||||
<button
|
||||
ref={hamburgerRef}
|
||||
type="button"
|
||||
aria-label={open ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={open}
|
||||
aria-controls="mobile-menu"
|
||||
onClick={() => setOpen(o => !o)}
|
||||
className="md:hidden btn-ghost p-2 rounded-xl"
|
||||
>
|
||||
{open
|
||||
? <X size={20} aria-hidden="true" />
|
||||
: <Menu size={20} aria-hidden="true" />
|
||||
}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
<div
|
||||
id="mobile-menu"
|
||||
ref={mobileMenuRef}
|
||||
role="dialog"
|
||||
aria-label="Navigation menu"
|
||||
aria-modal="true"
|
||||
hidden={!open}
|
||||
className={clsx(
|
||||
'md:hidden border-t border-slate-100 bg-white/95 backdrop-blur-md',
|
||||
open ? 'block' : 'hidden'
|
||||
)}
|
||||
>
|
||||
<nav
|
||||
aria-label="Mobile navigation"
|
||||
className="section py-4 flex flex-col gap-1"
|
||||
>
|
||||
{NAV_LINKS.map(({ label, href }) => {
|
||||
const active = pathname === href
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
className={clsx(
|
||||
'flex items-center px-4 py-3 rounded-xl text-sm font-medium transition-colors',
|
||||
active
|
||||
? 'text-brand-600 bg-brand-50'
|
||||
: 'text-ink-muted hover:text-ink hover:bg-surface-subtle'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
<div className="mt-3 pt-3 border-t border-slate-100">
|
||||
<Link
|
||||
href="/admin"
|
||||
className="btn-primary w-full justify-center text-sm"
|
||||
>
|
||||
Admin Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue