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

This commit is contained in:
cupadev-admin 2026-03-09 18:16:23 +00:00
parent 4e960ba152
commit 64d1d67eb6
1 changed files with 263 additions and 0 deletions

View File

@ -0,0 +1,263 @@
'use client'
import { useState, useEffect, useRef, useCallback } from 'react'
import Link from 'next/link'
import Image from 'next/image'
import { Menu, X, ChevronDown, Phone, Mail } from 'lucide-react'
import { useSettingsStore } from '@/store/settingsStore'
import { useNavigationStore } from '@/store/navigationStore'
import clsx from 'clsx'
export default function SiteHeader() {
const { settings } = useSettingsStore()
const { items } = useNavigationStore()
const [mobileOpen, setMobileOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
const mobileMenuRef = useRef<HTMLDivElement>(null)
const menuButtonRef = useRef<HTMLButtonElement>(null)
const visibleItems = items
.filter((i) => i.isVisible && i.parentId === null)
.sort((a, b) => a.order - b.order)
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 10)
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [])
useEffect(() => {
if (!mobileOpen) return
const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setMobileOpen(false)
menuButtonRef.current?.focus()
}
}
document.addEventListener('keydown', handleKey)
return () => document.removeEventListener('keydown', handleKey)
}, [mobileOpen])
useEffect(() => {
settings.applyThemeVars?.()
}, [settings])
const closeMobile = useCallback(() => {
setMobileOpen(false)
menuButtonRef.current?.focus()
}, [])
const headerStyle = {
backgroundColor: settings.headerBgColor,
color: settings.headerTextColor,
position: settings.headerPosition as 'sticky' | 'static' | 'fixed',
}
return (
<>
{/* Top Bar */}
{settings.showTopBar && settings.topBarText && (
<div
className="py-2 px-4 text-center text-sm font-medium"
style={{ backgroundColor: settings.topBarBgColor, color: '#1a1a2e' }}
role="banner"
aria-label="Annonce"
>
{settings.topBarText}
</div>
)}
{/* Tricolor stripe */}
<div className="tricolor-bar" aria-hidden="true" />
{/* Main Header */}
<header
style={headerStyle}
className={clsx(
'top-0 z-50 w-full transition-shadow duration-300',
scrolled && 'shadow-lg'
)}
role="banner"
>
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div
className={clsx(
'flex h-20 items-center gap-4',
settings.headerLayout === 'center'
? 'justify-center'
: settings.headerLayout === 'right'
? 'justify-end'
: 'justify-between'
)}
>
{/* Logo / Site Name */}
<Link
href="/"
className="flex items-center gap-3 shrink-0 group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fr-gold rounded-md"
aria-label={`${settings.siteName} - Retour à l'accueil`}
>
{settings.logoUrl ? (
<Image
src={settings.logoUrl}
alt={settings.siteName}
width={48}
height={48}
className="rounded-full object-cover"
/>
) : (
<div
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-full border-2 border-fr-gold text-lg font-bold"
style={{ color: settings.headerTextColor }}
aria-hidden="true"
>
🎖
</div>
)}
<div>
<span
className="block font-serif text-lg font-bold leading-tight group-hover:opacity-90 transition-opacity"
style={{ color: settings.headerTextColor }}
>
{settings.siteName}
</span>
{settings.siteTagline && (
<span
className="block text-xs opacity-80 tracking-wide"
style={{ color: settings.headerTextColor }}
>
{settings.siteTagline}
</span>
)}
</div>
</Link>
{/* Desktop Navigation */}
<nav
className="hidden lg:flex items-center gap-1"
aria-label="Navigation principale"
>
{visibleItems.map((item) => (
<Link
key={item.id}
href={item.href}
target={item.target}
rel={item.target === '_blank' ? 'noopener noreferrer' : undefined}
className={clsx(
'relative px-4 py-2 text-sm font-semibold tracking-wide rounded-md transition-all duration-200',
'hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fr-gold',
'after:absolute after:bottom-0 after:left-1/2 after:-translate-x-1/2 after:h-0.5 after:w-0 after:bg-fr-gold after:transition-all after:duration-200',
'hover:after:w-3/4'
)}
style={{ color: settings.headerTextColor }}
>
{item.label}
</Link>
))}
</nav>
{/* Contact info (desktop) */}
<div className="hidden xl:flex items-center gap-4">
{settings.footerPhone && (
<a
href={`tel:${settings.footerPhone}`}
className="flex items-center gap-1.5 text-xs opacity-80 hover:opacity-100 transition-opacity"
style={{ color: settings.headerTextColor }}
aria-label={`Téléphone: ${settings.footerPhone}`}
>
<Phone size={14} aria-hidden="true" />
{settings.footerPhone}
</a>
)}
</div>
{/* Mobile hamburger */}
<button
ref={menuButtonRef}
type="button"
className={clsx(
'lg:hidden flex items-center justify-center w-10 h-10 rounded-md',
'hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fr-gold',
'transition-colors duration-200'
)}
style={{ color: settings.headerTextColor }}
onClick={() => setMobileOpen((v) => !v)}
aria-expanded={mobileOpen}
aria-controls="mobile-menu"
aria-label={mobileOpen ? 'Fermer le menu' : 'Ouvrir le menu'}
>
{mobileOpen ? (
<X size={24} aria-hidden="true" />
) : (
<Menu size={24} aria-hidden="true" />
)}
</button>
</div>
</div>
{/* Mobile Menu */}
<div
id="mobile-menu"
ref={mobileMenuRef}
className={clsx(
'lg:hidden overflow-hidden transition-all duration-300',
mobileOpen ? 'max-h-screen opacity-100' : 'max-h-0 opacity-0'
)}
aria-hidden={!mobileOpen}
>
<div
className="border-t border-white/20 py-4 px-4 space-y-1"
style={{ backgroundColor: settings.headerBgColor }}
>
<nav aria-label="Navigation mobile">
{visibleItems.map((item) => (
<Link
key={item.id}
href={item.href}
target={item.target}
rel={item.target === '_blank' ? 'noopener noreferrer' : undefined}
className={clsx(
'flex items-center px-4 py-3 rounded-md text-sm font-semibold',
'hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fr-gold',
'transition-colors duration-200'
)}
style={{ color: settings.headerTextColor }}
onClick={closeMobile}
>
{item.label}
</Link>
))}
</nav>
{/* Mobile contact */}
{(settings.footerPhone || settings.footerEmail) && (
<div className="mt-4 pt-4 border-t border-white/20 space-y-2">
{settings.footerPhone && (
<a
href={`tel:${settings.footerPhone}`}
className="flex items-center gap-2 px-4 py-2 text-sm opacity-80"
style={{ color: settings.headerTextColor }}
>
<Phone size={14} aria-hidden="true" />
{settings.footerPhone}
</a>
)}
{settings.footerEmail && (
<a
href={`mailto:${settings.footerEmail}`}
className="flex items-center gap-2 px-4 py-2 text-sm opacity-80"
style={{ color: settings.headerTextColor }}
>
<Mail size={14} aria-hidden="true" />
{settings.footerEmail}
</a>
)}
</div>
)}
</div>
</div>
</header>
{/* Bottom tricolor stripe */}
<div className="tricolor-bar" aria-hidden="true" />
</>
)
}