fix: apply designer agent improvements to src/components/public/PublicHeader.tsx

This commit is contained in:
cupadev-admin 2026-03-09 14:32:15 +00:00
parent f4f8fae43e
commit 1aa365177f
1 changed files with 194 additions and 0 deletions

View File

@ -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>
</>
)
}