fix: apply designer agent improvements to src/components/public/SiteHeader.tsx
This commit is contained in:
parent
4e960ba152
commit
64d1d67eb6
|
|
@ -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" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue