wwwlayermeshusa/src/Header/Nav/index.tsx
2026-02-20 13:22:07 +01:00

372 lines
13 KiB
TypeScript

'use client'
import React, { useState, useEffect, useRef } from 'react'
import type { Header as HeaderType, Page } from '@/payload-types'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { MenuIcon, XIcon, ChevronDownIcon, ChevronRightIcon } from 'lucide-react'
import { SocialIconsRow } from '@/components/SocialIcons'
import type { SocialLinkData } from '@/components/SocialIcons'
type NavChild = NonNullable<NonNullable<NonNullable<HeaderType['navItems']>[number]['children']>[number]>
type NavItem = NonNullable<HeaderType['navItems']>[number]
function resolveHref(item: {
type?: string | null
url?: string | null
reference?: { relationTo?: string; value?: number | Page | null } | null
}): string {
if (item.type === 'reference' && item.reference?.value) {
const page = item.reference.value
if (typeof page === 'object' && page !== null) {
return page.slug === 'home' || page.slug === 'startsida' ? '/' : `/${(page as Page).slug}`
}
return '#'
}
return item.url || '#'
}
const SwedishFlag = () => (
<svg width="28" height="20" viewBox="0 0 28 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" className="rounded-[3px]">
<rect width="28" height="20" fill="#006AA7" />
<rect x="8" width="4" height="20" fill="#FECC02" />
<rect y="8" width="28" height="4" fill="#FECC02" />
</svg>
)
function groupChildren(children: NavChild[]): { heading: string | null; links: NavChild[] }[] {
const main: NavChild[] = []
const groups: Record<string, NavChild[]> = {}
const groupOrder: string[] = []
for (const child of children) {
if (!child.group) {
main.push(child)
} else {
if (!groups[child.group]) {
groups[child.group] = []
groupOrder.push(child.group)
}
groups[child.group].push(child)
}
}
const result: { heading: string | null; links: NavChild[] }[] = []
if (main.length > 0) result.push({ heading: null, links: main })
for (const g of groupOrder) result.push({ heading: g, links: groups[g] })
return result
}
const MegaMenuPanel: React.FC<{
item: NavItem
headerRef: React.RefObject<HTMLElement | null>
panelRef: React.RefObject<HTMLDivElement | null>
onClose: () => void
}> = ({ item, headerRef, panelRef, onClose }) => {
const [top, setTop] = useState(72)
useEffect(() => {
const update = () => {
if (headerRef.current) {
const rect = headerRef.current.getBoundingClientRect()
setTop(rect.bottom)
}
}
update()
window.addEventListener('scroll', update, { passive: true })
window.addEventListener('resize', update)
return () => {
window.removeEventListener('scroll', update)
window.removeEventListener('resize', update)
}
}, [headerRef])
const columns = groupChildren(item.children || [])
return (
<>
{/* Blur backdrop */}
<div
className="fixed inset-0 z-30"
style={{ top, backdropFilter: 'blur(6px)', WebkitBackdropFilter: 'blur(6px)', backgroundColor: 'rgba(14, 35, 56, 0.08)' }}
onClick={onClose}
/>
{/* Panel — white in light, navy in dark */}
<div
ref={panelRef}
className="fixed left-0 right-0 z-40 bg-white dark:bg-fd-navy border-b border-fd-navy/10 dark:border-white/10"
style={{ top }}
>
<div className="max-w-[1200px] mx-auto px-6 md:px-8 py-10 md:py-14">
<div className="grid grid-cols-[200px_1fr] gap-12 md:gap-20">
{/* Left: category title */}
<div>
<h2 className="font-joey-heavy text-fd-h1 text-fd-navy dark:text-fd-yellow leading-tight">
{item.label}
</h2>
</div>
{/* Right: columns */}
<div className={`grid gap-10 ${columns.length === 1 ? 'grid-cols-1' : columns.length === 2 ? 'grid-cols-2' : 'grid-cols-3'}`}>
{columns.map((col, ci) => (
<div key={ci}>
{col.heading && (
<p className="font-joey-medium text-fd-small text-fd-navy/40 dark:text-white/40 uppercase tracking-widest mb-4">
{col.heading}
</p>
)}
<ul className="flex flex-col gap-1">
{col.links.map((link, li) => (
<li key={li}>
<Link
href={resolveHref(link)}
onClick={onClose}
className={`block py-1.5 transition-colors duration-150 ${
col.heading
? 'font-joey text-fd-body text-fd-navy/75 dark:text-white/70 hover:text-fd-navy dark:hover:text-fd-yellow'
: 'font-joey-bold text-fd-h3 text-fd-navy dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow'
}`}
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
</div>
</>
)
}
export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkData[] }> = ({
data,
socialLinks = [],
}) => {
const navItems = (data?.navItems || []) as NavItem[]
const [mobileOpen, setMobileOpen] = useState(false)
const [openDropdown, setOpenDropdown] = useState<string | null>(null)
const [mobileOpenSection, setMobileOpenSection] = useState<string | null>(null)
const pathname = usePathname()
const navRef = useRef<HTMLDivElement>(null)
const headerRef = useRef<HTMLElement | null>(null)
const megaMenuRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
if (navRef.current) {
headerRef.current = navRef.current.closest('header')
}
}, [])
useEffect(() => {
setMobileOpen(false)
setMobileOpenSection(null)
setOpenDropdown(null)
}, [pathname])
useEffect(() => {
document.body.style.overflow = mobileOpen ? 'hidden' : ''
return () => { document.body.style.overflow = '' }
}, [mobileOpen])
useEffect(() => {
const handler = (e: MouseEvent) => {
if (
navRef.current && !navRef.current.contains(e.target as Node) &&
megaMenuRef.current && !megaMenuRef.current.contains(e.target as Node)
) {
setOpenDropdown(null)
}
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [])
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setOpenDropdown(null)
setMobileOpen(false)
}
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [])
const closeMobile = () => {
setMobileOpen(false)
setMobileOpenSection(null)
}
const activeItem = navItems.find((item) => item.label === openDropdown)
return (
<>
{/* ── Desktop nav ── */}
<nav ref={navRef} className="hidden md:flex gap-6 items-center">
{navItems.map((item, i) => {
const hasChildren = item.children && item.children.length > 0
const isOpen = openDropdown === item.label
const isMega = item.megaMenu && hasChildren
return hasChildren ? (
<div key={i} className="relative">
<button
onClick={() => setOpenDropdown(isOpen ? null : item.label ?? null)}
className={`flex items-center gap-1 font-joey-bold transition-colors text-base ${
isOpen ? 'text-fd-yellow' : 'text-fd-navy dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow'
}`}
aria-haspopup="true"
aria-expanded={isOpen}
>
{item.label}
<ChevronDownIcon
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
/>
</button>
{/* Small card dropdown */}
{!isMega && (
<div
className={`absolute top-full left-0 mt-3 min-w-[220px] bg-white dark:bg-fd-navy rounded-[16px] shadow-lg border border-fd-navy/10 dark:border-white/10 py-2 z-50 transition-all duration-200 origin-top ${
isOpen
? 'opacity-100 scale-y-100 pointer-events-auto'
: 'opacity-0 scale-y-95 pointer-events-none'
}`}
>
{item.children!.map((child, j) => (
<Link
key={j}
href={resolveHref(child)}
onClick={() => setOpenDropdown(null)}
className="block px-5 py-2.5 font-joey text-fd-navy dark:text-white hover:bg-fd-yellow/20 dark:hover:bg-white/10 transition-colors text-sm"
>
{child.label}
</Link>
))}
</div>
)}
</div>
) : (
<Link
key={i}
href={resolveHref(item)}
className="font-joey-bold text-fd-navy dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow transition-colors text-base"
>
{item.label}
</Link>
)
})}
<SwedishFlag />
</nav>
{/* ── Mega menu panel ── */}
{activeItem?.megaMenu && activeItem.children?.length && (
<MegaMenuPanel
item={activeItem}
headerRef={headerRef}
panelRef={megaMenuRef}
onClose={() => setOpenDropdown(null)}
/>
)}
{/* ── Mobile hamburger ── */}
<button
className="md:hidden text-fd-navy dark:text-white p-2"
onClick={() => setMobileOpen(!mobileOpen)}
aria-label={mobileOpen ? 'Stäng meny' : 'Öppna meny'}
>
{mobileOpen ? <XIcon className="w-6 h-6" /> : <MenuIcon className="w-6 h-6" />}
</button>
{/* ── Mobile full-screen overlay ── */}
<div
className={`fixed inset-0 z-50 bg-fd-navy flex flex-col transition-transform duration-300 md:hidden ${
mobileOpen ? 'translate-x-0' : 'translate-x-full'
}`}
role="dialog"
aria-modal="true"
aria-label="Navigeringsmeny"
>
{/* Header bar */}
<div className="flex items-center justify-between px-6 py-5 border-b border-white/10">
<Link href="/" onClick={closeMobile}>
<span className="font-joey-heavy text-fd-yellow text-xl">Fiber Direkt</span>
</Link>
<button
onClick={closeMobile}
className="text-white p-2 hover:text-fd-yellow transition-colors"
aria-label="Stäng meny"
>
<XIcon className="w-6 h-6" />
</button>
</div>
{/* Nav links */}
<nav className="flex-1 overflow-y-auto px-6 py-6 flex flex-col">
{navItems.map((item, i) => {
const hasChildren = item.children && item.children.length > 0
const isExpanded = mobileOpenSection === item.label
return hasChildren ? (
<div key={i} className="border-b border-white/10">
<button
onClick={() => setMobileOpenSection(isExpanded ? null : item.label ?? null)}
className="w-full flex justify-between items-center py-4 text-white font-joey-medium text-xl hover:text-fd-yellow transition-colors"
>
{item.label}
<ChevronRightIcon
className={`w-5 h-5 transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
/>
</button>
<div
className={`overflow-hidden transition-all duration-300 ${
isExpanded ? 'max-h-[500px] pb-3' : 'max-h-0'
}`}
>
{item.children!.map((child, j) => (
<Link
key={j}
href={resolveHref(child)}
onClick={closeMobile}
className="block pl-4 py-3 text-fd-yellow font-joey text-lg border-b border-white/5 hover:text-white transition-colors"
>
{child.label}
</Link>
))}
</div>
</div>
) : (
<Link
key={i}
href={resolveHref(item)}
onClick={closeMobile}
className="py-4 text-white font-joey-medium text-xl border-b border-white/10 hover:text-fd-yellow transition-colors block"
>
{item.label}
</Link>
)
})}
</nav>
{/* ── Social icons row at bottom of mobile menu ── */}
{socialLinks.length > 0 && (
<div className="px-6 py-5 border-t border-white/10">
<SocialIconsRow
links={socialLinks}
linkClassName="text-white/50 hover:text-white"
iconSize="w-6 h-6"
/>
</div>
)}
</div>
</>
)
}