Files
homaroma/apps/spa/src/components/DesktopMegaMenu.tsx
T
2026-06-16 20:04:40 -04:00

112 lines
4.5 KiB
TypeScript

import { useState, useRef } from 'react';
import { Link } from 'react-router-dom';
import type { MegaMenuPayload } from '../store/menuStore';
import { useTranslation } from 'react-i18next';
interface DesktopMegaMenuProps {
items: MegaMenuPayload[];
}
const getLocalizedText = (textObj: Record<string, string> | string | undefined, lang: string) => {
if (typeof textObj === 'string') return textObj;
if (!textObj) return '';
return textObj[lang] || textObj['en'] || Object.values(textObj)[0] || '';
};
export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
const [openId, setOpenId] = useState<string | null>(null);
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const { i18n } = useTranslation();
const lang = i18n.language ? i18n.language.substring(0, 2) : 'en';
// Open: cancel any pending close, then open this menu
const handleOpen = (id: string) => {
if (closeTimer.current) clearTimeout(closeTimer.current);
setOpenId(id);
};
// Close: wait 150ms — if mouse enters the dropdown in time, the close is cancelled
const handleClose = () => {
closeTimer.current = setTimeout(() => setOpenId(null), 150);
};
return (
<div className="hidden md:flex space-x-7 text-sm font-medium tracking-wide uppercase">
{items.map((item) => {
const uniqueId = item.nodeId || item.id || `menu-${item.url}`;
const isOpen = openId === uniqueId;
const hasColumns = item.columns && item.columns.length > 0;
return (
<div
key={uniqueId}
className="relative"
onMouseEnter={() => handleOpen(uniqueId)}
onMouseLeave={handleClose}
>
{/* Nav trigger */}
<Link
to={item.url || '#'}
className={`flex items-center gap-1 transition-colors py-2 border-b ${
isOpen
? 'text-spa-forest border-spa-forest'
: 'text-spa-stone border-transparent hover:text-spa-forest hover:border-spa-forest'
}`}
>
{getLocalizedText(item.label, lang)}
{hasColumns && (
<svg
className={`w-3 h-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>
{/* Dropdown panel — fixed, full-width.
It handles its own onMouseEnter/Leave so the delay timer is
cancelled the moment the mouse enters the panel. */}
{hasColumns && (
<div
className={`fixed left-0 right-0 z-50 transition-opacity duration-200 ${
isOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'
}`}
style={{ top: 'var(--navbar-bottom, 56px)' }}
onMouseEnter={() => handleOpen(uniqueId)}
onMouseLeave={handleClose}
>
<div className="w-full bg-[#F9F8F5] border-b border-black/5 shadow-sm cursor-default">
<div className="flex justify-center gap-16 py-10 max-w-7xl mx-auto px-6">
{item.columns!.map((col, idx) => (
<div key={idx} className="flex flex-col min-w-[140px] text-left">
<h4 className="text-[10px] uppercase tracking-widest text-spa-stone font-bold mb-4">
{getLocalizedText(col.title, lang)}
</h4>
<ul className="space-y-3">
{col.links.map((link, lIdx) => (
<li key={lIdx}>
<Link
to={link.url || '#'}
className="text-sm font-normal normal-case tracking-normal text-spa-stone hover:text-spa-forest hover:opacity-80 block transition-colors"
>
{getLocalizedText(link.label, lang)}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
)}
</div>
);
})}
</div>
);
}