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