189 lines
7.2 KiB
TypeScript
189 lines
7.2 KiB
TypeScript
"use client"
|
|
|
|
import Link from '../LocalizedLink'
|
|
import Image from 'next/image'
|
|
import { useState, useRef } from 'react'
|
|
import type { MegaMenuPayload } from './menuTypes'
|
|
import { getLocalizedMenuText } from './menuTypes'
|
|
|
|
interface DesktopMegaMenuProps {
|
|
items: MegaMenuPayload[]
|
|
lang: string
|
|
currentPath: string
|
|
}
|
|
|
|
// Render a 1:1 image using next/image for optimization (website uses CDN images)
|
|
function MenuImage({
|
|
src,
|
|
alt,
|
|
placeholder,
|
|
className = '',
|
|
}: {
|
|
src?: string | null
|
|
alt: string
|
|
placeholder?: string
|
|
className?: string
|
|
}) {
|
|
const [loaded, setLoaded] = useState(false)
|
|
|
|
if (!src) {
|
|
if (!placeholder) return null
|
|
return (
|
|
<span className="text-text-main/30 text-xs text-center px-3 leading-relaxed">{placeholder}</span>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Image
|
|
src={src}
|
|
alt={alt}
|
|
fill
|
|
sizes="(max-width: 1024px) 0px, 224px"
|
|
className={`object-cover transition-opacity duration-300 ${loaded ? 'opacity-100' : 'opacity-0'} ${className}`}
|
|
onLoad={() => setLoaded(true)}
|
|
loading="lazy"
|
|
/>
|
|
)
|
|
}
|
|
|
|
export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuProps) {
|
|
const [openId, setOpenId] = useState<string | null>(null)
|
|
const [hoveredLinkImage, setHoveredLinkImage] = useState<string | null>(null)
|
|
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
|
|
const handleOpen = (id: string) => {
|
|
if (closeTimer.current) clearTimeout(closeTimer.current)
|
|
if (openId !== id) setHoveredLinkImage(null)
|
|
setOpenId(id)
|
|
}
|
|
|
|
const handleClose = () => {
|
|
closeTimer.current = setTimeout(() => {
|
|
setOpenId(null)
|
|
setHoveredLinkImage(null)
|
|
}, 150)
|
|
}
|
|
|
|
return (
|
|
<div className="hidden space-x-7 text-sm font-medium uppercase tracking-wide md:flex">
|
|
{items.map((item, index) => {
|
|
const uniqueId = item.nodeId || item.id || `menu-${item.url || index}`
|
|
const isOpen = openId === uniqueId
|
|
const hasColumns = !!item.columns?.length
|
|
const isActive = item.url ? currentPath === item.url : false
|
|
|
|
const hasAnyColumnIcon = item.columns?.some(col => !!col.iconUrl)
|
|
const hasAnyLinkImage = item.columns?.some(col => col.links.some(l => !!l.imageUrl))
|
|
const showRightPanel = item.rootImageUrl || hasAnyLinkImage
|
|
|
|
return (
|
|
<div
|
|
key={uniqueId}
|
|
className="relative"
|
|
onMouseEnter={() => handleOpen(uniqueId)}
|
|
onMouseLeave={handleClose}
|
|
>
|
|
<Link
|
|
href={item.url || '#'}
|
|
className={`flex items-center gap-1 border-b py-2 transition-colors ${
|
|
isOpen || isActive
|
|
? 'border-brand-primary text-text-main'
|
|
: 'border-transparent text-text-main hover:border-brand-primary hover:text-text-main'
|
|
}`}
|
|
>
|
|
{getLocalizedMenuText(item.label, lang)}
|
|
{hasColumns && (
|
|
<svg
|
|
className={`h-3 w-3 opacity-50 transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`}
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
)}
|
|
</Link>
|
|
|
|
{hasColumns && (
|
|
<div
|
|
className={`fixed left-0 right-0 z-50 transition-opacity duration-200 ${
|
|
isOpen ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'
|
|
}`}
|
|
style={{ top: 'var(--navbar-bottom, 64px)' }}
|
|
onMouseEnter={() => handleOpen(uniqueId)}
|
|
onMouseLeave={handleClose}
|
|
>
|
|
<div className="w-full cursor-default border-b border-black/5 bg-surface-card/95 shadow-card backdrop-blur-xl">
|
|
<div className="mx-auto flex max-w-7xl items-start gap-10 px-6 py-10">
|
|
|
|
{/* LEFT: Root menu image (1:1) */}
|
|
{item.rootImageUrl && (
|
|
<div className="relative hidden h-56 w-56 shrink-0 overflow-hidden rounded-xl border border-black/8 lg:block">
|
|
<MenuImage
|
|
src={item.rootImageUrl}
|
|
alt={getLocalizedMenuText(item.label, lang)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* CENTER: Columns */}
|
|
<div className="flex flex-1 gap-12">
|
|
{item.columns!.map((column, columnIndex) => (
|
|
<div key={columnIndex} className="flex min-w-[160px] gap-4 text-left items-start">
|
|
|
|
{/* Column icon (1:1) */}
|
|
{hasAnyColumnIcon && (
|
|
<div className="relative h-14 w-14 shrink-0 mt-0.5 overflow-hidden rounded-lg border border-black/8 bg-surface-muted">
|
|
<MenuImage
|
|
src={column.iconUrl}
|
|
alt={getLocalizedMenuText(column.title, lang)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Title and links */}
|
|
<div className="flex flex-col">
|
|
<h4 className="mb-3 text-[10px] font-bold uppercase tracking-widest text-text-main/50">
|
|
{getLocalizedMenuText(column.title, lang)}
|
|
</h4>
|
|
<ul className="space-y-2.5">
|
|
{column.links.map((link, linkIndex) => (
|
|
<li key={linkIndex}>
|
|
<Link
|
|
href={link.url || '#'}
|
|
className="block text-sm font-normal normal-case tracking-normal text-text-main/65 transition-colors duration-150 hover:text-brand-primary"
|
|
onMouseEnter={() => setHoveredLinkImage(link.imageUrl || null)}
|
|
onMouseLeave={() => setHoveredLinkImage(null)}
|
|
>
|
|
{getLocalizedMenuText(link.label, lang)}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* RIGHT: Link hover preview / Root fallback (1:1) */}
|
|
{showRightPanel && (
|
|
<div className="relative hidden h-56 w-56 shrink-0 overflow-hidden rounded-xl border border-black/8 bg-surface-muted lg:flex items-center justify-center">
|
|
<MenuImage
|
|
src={hoveredLinkImage ?? item.rootImageUrl}
|
|
alt="Preview"
|
|
placeholder="Hover a link to preview"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|