640 lines
35 KiB
TypeScript
640 lines
35 KiB
TypeScript
import { useState, useRef, useEffect } from 'react';
|
|
import { Link, useLocation } from 'react-router-dom';
|
|
import type { MegaMenuPayload, MenuColumn } from '../store/menuStore';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
ArrowRight,
|
|
ChevronRight,
|
|
Star,
|
|
Sparkles,
|
|
Box,
|
|
} from 'lucide-react';
|
|
import { ICON_REGISTRY, type AvailableIcons } from '../utils/iconRegistry';
|
|
|
|
function DynamicIcon({ iconName, defaultIcon: DefaultIcon, className, strokeWidth }: { iconName?: string, defaultIcon: any, className?: string, strokeWidth?: number }) {
|
|
const IconComponent = (iconName && ICON_REGISTRY[iconName as AvailableIcons])
|
|
? ICON_REGISTRY[iconName as AvailableIcons]
|
|
: DefaultIcon;
|
|
|
|
return <IconComponent className={className} strokeWidth={strokeWidth} />;
|
|
}
|
|
|
|
|
|
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 || textObj.th || Object.values(textObj)[0] || '';
|
|
};
|
|
|
|
|
|
|
|
const MAX_TOP_LEVEL_LINKS = 5;
|
|
const MAX_INLINE_CHILD_LINKS = 3;
|
|
|
|
const getMoreLabel = (count: number, lang: string) => {
|
|
if (lang === 'th') return `+ อีก ${count} รายการ`;
|
|
if (lang === 'ja') return `+ 他${count}件`;
|
|
if (lang === 'zh') return `+ 还有 ${count} 项`;
|
|
if (lang === 'vi') return `+ ${count} mục khác`;
|
|
if (lang === 'ko') return `+ ${count}개 더`;
|
|
return `+ ${count} more`;
|
|
};
|
|
|
|
// Image block shared between Root and Hover areas
|
|
function MenuImage({ src, alt, placeholder, className, style }: { src?: string | null; alt: string; placeholder?: string; className?: string; style?: React.CSSProperties }) {
|
|
if (!src) {
|
|
if (!placeholder) return null;
|
|
return (
|
|
<div className={`w-full h-full flex items-center justify-center ${className || ''}`} style={style}>
|
|
<span className="text-pop-black/30 text-xs text-center px-3 leading-relaxed">{placeholder}</span>
|
|
</div>
|
|
);
|
|
}
|
|
return <img src={src} alt={alt} className={`w-full h-full object-cover ${className || ''}`} style={style} loading="lazy" />;
|
|
}
|
|
|
|
export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
|
|
const [openId, setOpenId] = useState<string | null>(null);
|
|
const [hoveredLinkImage, setHoveredLinkImage] = useState<string | null>(null);
|
|
const [activeTabByMenu, setActiveTabByMenu] = useState<Record<string, number>>({});
|
|
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const location = useLocation();
|
|
const { i18n } = useTranslation();
|
|
const lang = i18n.language ? i18n.language.substring(0, 2) : 'en';
|
|
|
|
// Automatically close mega menu when route changes
|
|
useEffect(() => {
|
|
if (closeTimer.current) clearTimeout(closeTimer.current);
|
|
setOpenId(null);
|
|
setHoveredLinkImage(null);
|
|
}, [location.pathname, location.search]);
|
|
|
|
// Open: cancel any pending close, reset hover image when switching root menu
|
|
const handleOpen = (id: string) => {
|
|
if (closeTimer.current) clearTimeout(closeTimer.current);
|
|
if (openId !== id) {
|
|
setHoveredLinkImage(null);
|
|
}
|
|
setOpenId(id);
|
|
};
|
|
|
|
// Close: wait 150ms — if mouse enters the dropdown in time, the close is cancelled
|
|
const handleClose = () => {
|
|
closeTimer.current = setTimeout(() => {
|
|
setOpenId(null);
|
|
setHoveredLinkImage(null);
|
|
}, 150);
|
|
};
|
|
|
|
const handleLinkClick = () => {
|
|
if (closeTimer.current) clearTimeout(closeTimer.current);
|
|
setOpenId(null);
|
|
setHoveredLinkImage(null);
|
|
};
|
|
|
|
const getFirstImageLink = (columns: MenuColumn[] = []) => {
|
|
for (const column of columns) {
|
|
const found = column.links.find((link) => link.imageUrl);
|
|
if (found) return found;
|
|
}
|
|
return columns[0]?.links[0];
|
|
};
|
|
|
|
return (
|
|
<div className="hidden md:flex space-x-7 text-sm font-medium tracking-wide uppercase">
|
|
{items.map((item, index) => {
|
|
const uniqueId = item.nodeId || item.id || `menu-${item.url || index}`;
|
|
const isOpen = openId === uniqueId;
|
|
const hasColumns = item.columns && item.columns.length > 0;
|
|
|
|
const layout = item.layout || 'columns';
|
|
const isClassic = layout === 'classic';
|
|
const isTabbed = layout === 'tabbed';
|
|
const isVisualCards = layout === 'visual-cards';
|
|
const configuredCols = item.layoutConfig?.columnCount || (item.columns?.length ? Math.min(Math.max(item.columns.length, 2), 4) : 2);
|
|
const visibleColumns = item.columns?.slice(0, configuredCols) ?? [];
|
|
const featuredLink = getFirstImageLink(item.columns);
|
|
const featuredConfig = item.featured;
|
|
const showFeatured = (item.layoutConfig?.showFeatured !== false) && (featuredConfig?.enabled !== false) && !isClassic && !isVisualCards && !isTabbed && (
|
|
!!(featuredConfig?.imageUrl || featuredConfig?.title || featuredConfig?.sectionTitle || featuredLink?.imageUrl)
|
|
);
|
|
const configuredImage = featuredConfig?.imageUrl || null;
|
|
const featuredImage = hoveredLinkImage ?? configuredImage ?? featuredLink?.imageUrl ?? item.rootImageUrl;
|
|
const focalPoint = featuredConfig?.focalPoint;
|
|
const objectPosition = focalPoint ? `${focalPoint.x} ${focalPoint.y}` : 'center';
|
|
const featuredHeading = getLocalizedText(featuredConfig?.sectionTitle, lang) || 'Featured';
|
|
const featuredBadge = getLocalizedText(featuredConfig?.badge, lang) || 'Recommended';
|
|
const featuredTitle = getLocalizedText(featuredConfig?.title, lang)
|
|
|| getLocalizedText(featuredLink?.label, lang)
|
|
|| getLocalizedText(item.label, lang);
|
|
const featuredSubtitle = getLocalizedText(featuredConfig?.subtitle, lang) || (item.columns?.[0]
|
|
? getLocalizedText(item.columns[0].title, lang)
|
|
: getLocalizedText(item.label, lang));
|
|
const featuredHref = featuredConfig?.url || featuredLink?.url || item.url || '#';
|
|
const featuredCta = getLocalizedText(featuredConfig?.ctaLabel, lang) || 'View details';
|
|
|
|
const getMegaContainerClass = (cols: number, hasFeatured: boolean) => {
|
|
if (hasFeatured) {
|
|
if (cols >= 4) return 'w-[min(100vw-2rem,1260px)]';
|
|
if (cols === 3) return 'w-[min(100vw-2rem,1120px)]';
|
|
return 'w-[min(100vw-2rem,980px)]';
|
|
}
|
|
if (cols >= 4) return 'w-[min(100vw-2rem,1180px)]';
|
|
if (cols === 3) return 'w-[min(100vw-2rem,960px)]';
|
|
return 'w-[min(100vw-2rem,720px)]';
|
|
};
|
|
|
|
const getMegaGridColsClass = (cols: number, hasFeatured: boolean) => {
|
|
if (hasFeatured) {
|
|
if (cols >= 4) return 'grid-cols-[1fr_1fr_1fr_1fr_260px]';
|
|
if (cols === 3) return 'grid-cols-[1fr_1fr_1fr_270px]';
|
|
return 'grid-cols-[1fr_1fr_270px]';
|
|
}
|
|
if (cols >= 4) return 'grid-cols-4';
|
|
if (cols === 3) return 'grid-cols-3';
|
|
return 'grid-cols-2';
|
|
};
|
|
|
|
return (
|
|
<div
|
|
key={uniqueId}
|
|
className="relative"
|
|
onMouseEnter={() => handleOpen(uniqueId)}
|
|
onMouseLeave={handleClose}
|
|
>
|
|
{/* Nav trigger */}
|
|
<Link
|
|
to={item.url || '#'}
|
|
onClick={handleLinkClick}
|
|
className={`flex items-center gap-1 transition-colors py-2 border-b ${
|
|
isOpen
|
|
? 'text-pop-black border-pop-black'
|
|
: 'text-pop-black/70 border-transparent hover:text-pop-black hover:border-pop-black'
|
|
}`}
|
|
>
|
|
{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>
|
|
|
|
{hasColumns && isClassic && (
|
|
<div
|
|
className={`absolute left-0 top-full z-50 pt-2 transition-all duration-200 ${
|
|
isOpen ? 'opacity-100 pointer-events-auto translate-y-0' : 'opacity-0 pointer-events-none -translate-y-1'
|
|
}`}
|
|
onMouseEnter={() => handleOpen(uniqueId)}
|
|
onMouseLeave={handleClose}
|
|
>
|
|
<div className="min-w-[230px] max-w-xs cursor-default overflow-hidden rounded-xl border border-pop-black/10 bg-pop-bg/95 p-2 shadow-2xl backdrop-blur-xl">
|
|
{item.columns?.map((col, colIdx) => (
|
|
<div key={colIdx} className={colIdx > 0 ? 'mt-2 border-t border-pop-black/10 pt-2' : ''}>
|
|
{getLocalizedText(col.title, lang) && item.columns && item.columns.length > 1 && (
|
|
<h5 className="px-3 py-1 text-[10px] font-bold uppercase tracking-wider text-pop-black/50">
|
|
{getLocalizedText(col.title, lang)}
|
|
</h5>
|
|
)}
|
|
<ul className="space-y-0.5">
|
|
{col.links.map((link, linkIdx) => (
|
|
<li key={linkIdx}>
|
|
<Link
|
|
to={link.url || '#'}
|
|
onClick={handleLinkClick}
|
|
className="group flex items-center justify-between rounded-lg px-3 py-2 text-xs font-semibold normal-case tracking-normal text-pop-black transition-colors hover:bg-pop-card hover:text-pop-black"
|
|
>
|
|
<span className="flex items-center gap-2.5">
|
|
{link.icon && (
|
|
<DynamicIcon
|
|
iconName={link.icon}
|
|
defaultIcon={Box}
|
|
className="h-4 w-4 shrink-0 text-pop-black/70 transition-colors group-hover:text-pop-black"
|
|
strokeWidth={1.5}
|
|
/>
|
|
)}
|
|
<span>{getLocalizedText(link.label, lang)}</span>
|
|
</span>
|
|
{link.links?.length ? (
|
|
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-pop-black/30 transition-colors group-hover:text-pop-black" />
|
|
) : null}
|
|
</Link>
|
|
{link.links && link.links.length > 0 && (
|
|
<ul className="ml-4 space-y-0.5 border-l border-pop-black/10 pl-2 my-1">
|
|
{link.links.map((child, childIdx) => (
|
|
<li key={childIdx}>
|
|
<Link
|
|
to={child.url || '#'}
|
|
onClick={handleLinkClick}
|
|
className="block rounded-md px-2 py-1 text-[11px] font-normal normal-case tracking-normal text-pop-black/60 transition-colors hover:bg-pop-card hover:text-pop-black"
|
|
>
|
|
{getLocalizedText(child.label, lang)}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{hasColumns && isTabbed && (
|
|
<div
|
|
className={`fixed left-1/2 z-50 w-[min(100vw-2rem,1040px)] -translate-x-1/2 transition-all duration-200 ${
|
|
isOpen ? 'opacity-100 pointer-events-auto translate-y-0' : 'opacity-0 pointer-events-none -translate-y-1'
|
|
}`}
|
|
style={{ top: 'var(--navbar-bottom, 56px)' }}
|
|
onMouseEnter={() => handleOpen(uniqueId)}
|
|
onMouseLeave={handleClose}
|
|
>
|
|
<div className="mx-auto h-3 w-20 overflow-hidden">
|
|
<div className="mx-auto mt-2 h-5 w-5 rotate-45 rounded-sm border-l border-t border-pop-black/10 bg-pop-bg/95" />
|
|
</div>
|
|
<div className="w-full cursor-default overflow-hidden rounded-xl border border-pop-black/10 bg-pop-bg/95 shadow-2xl backdrop-blur-xl">
|
|
<div className="flex min-h-[380px] max-h-[500px]">
|
|
{/* Left Category Tabs Sidebar */}
|
|
<div className="w-64 shrink-0 border-r border-pop-black/10 bg-pop-card/30 p-3 space-y-1 overflow-y-auto">
|
|
<div className="px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-pop-black/40">
|
|
{lang === 'th' ? 'หมวดหมู่หลัก' : 'Categories'}
|
|
</div>
|
|
{item.columns?.map((column, colIdx) => {
|
|
const activeIdx = activeTabByMenu[uniqueId] ?? 0;
|
|
const isSelected = activeIdx === colIdx;
|
|
return (
|
|
<button
|
|
key={colIdx}
|
|
type="button"
|
|
onMouseEnter={() => {
|
|
setActiveTabByMenu(prev => ({ ...prev, [uniqueId]: colIdx }));
|
|
setHoveredLinkImage(null);
|
|
}}
|
|
className={`flex w-full items-center justify-between rounded-lg px-3.5 py-2.5 text-left transition-all ${
|
|
isSelected
|
|
? 'bg-pop-primary/10 text-pop-primary font-bold shadow-sm'
|
|
: 'text-pop-black/80 hover:bg-pop-card hover:text-pop-black font-medium'
|
|
}`}
|
|
>
|
|
<span className="flex items-center gap-2.5 min-w-0">
|
|
<DynamicIcon
|
|
iconName={column.icon}
|
|
defaultIcon={Sparkles}
|
|
className={`h-4 w-4 shrink-0 ${isSelected ? 'text-pop-primary' : 'text-pop-black/50'}`}
|
|
strokeWidth={1.5}
|
|
/>
|
|
<span className="truncate text-xs normal-case">{getLocalizedText(column.title, lang)}</span>
|
|
</span>
|
|
<ChevronRight className={`h-3.5 w-3.5 shrink-0 transition-transform ${isSelected ? 'text-pop-primary translate-x-0.5' : 'text-pop-black/20'}`} />
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Right Content Panel for Selected Tab */}
|
|
{(() => {
|
|
const activeIdx = activeTabByMenu[uniqueId] ?? 0;
|
|
const activeCol = item.columns?.[activeIdx] || item.columns?.[0];
|
|
if (!activeCol) return null;
|
|
|
|
return (
|
|
<div className="flex-1 p-6 flex flex-col justify-between overflow-y-auto">
|
|
<div>
|
|
<div className="flex items-center justify-between border-b border-pop-black/10 pb-3 mb-4">
|
|
<div className="flex items-center gap-2.5">
|
|
<DynamicIcon
|
|
iconName={activeCol.icon}
|
|
defaultIcon={Sparkles}
|
|
className="h-5 w-5 text-pop-primary"
|
|
strokeWidth={1.5}
|
|
/>
|
|
<h3 className="text-sm font-bold normal-case tracking-normal text-pop-black">
|
|
{getLocalizedText(activeCol.title, lang)}
|
|
</h3>
|
|
</div>
|
|
<Link
|
|
to={activeCol.links[0]?.url || item.url || '#'}
|
|
onClick={handleLinkClick}
|
|
className="flex items-center gap-1.5 text-xs font-semibold normal-case text-pop-primary hover:underline"
|
|
>
|
|
<span>{lang === 'th' ? 'ดูรายการทั้งหมด' : 'View all'}</span>
|
|
<ArrowRight className="h-3.5 w-3.5" />
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3">
|
|
{activeCol.links.map((link, lIdx) => (
|
|
<div
|
|
key={lIdx}
|
|
className="rounded-lg p-2.5 hover:bg-pop-card transition-colors group"
|
|
onMouseEnter={() => setHoveredLinkImage(link.imageUrl || null)}
|
|
onMouseLeave={() => setHoveredLinkImage(null)}
|
|
>
|
|
<Link
|
|
to={link.url || '#'}
|
|
onClick={handleLinkClick}
|
|
className="flex items-center gap-2.5"
|
|
>
|
|
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-pop-black/10 bg-pop-bg text-pop-primary group-hover:border-pop-primary/40">
|
|
<DynamicIcon
|
|
iconName={link.icon}
|
|
defaultIcon={Box}
|
|
className="h-3.5 w-3.5"
|
|
strokeWidth={1.35}
|
|
/>
|
|
</span>
|
|
<span className="truncate text-xs font-semibold normal-case text-pop-black group-hover:text-pop-primary transition-colors">
|
|
{getLocalizedText(link.label, lang)}
|
|
</span>
|
|
</Link>
|
|
|
|
{link.links && link.links.length > 0 && (
|
|
<div className="ml-9 mt-1.5 flex flex-wrap gap-1.5">
|
|
{link.links.slice(0, 3).map((child, cIdx) => (
|
|
<Link
|
|
key={cIdx}
|
|
to={child.url || '#'}
|
|
onClick={handleLinkClick}
|
|
className="text-[10px] text-pop-black/60 hover:text-pop-primary transition-colors bg-pop-card px-1.5 py-0.5 rounded border border-pop-black/5"
|
|
>
|
|
{getLocalizedText(child.label, lang)}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t border-pop-black/10 pt-4 mt-6 flex items-center justify-between text-xs text-pop-black/50">
|
|
<span>{item.columns?.length || 0} {lang === 'th' ? 'หมวดหมู่ย่อย' : 'categories'}</span>
|
|
<Link
|
|
to={item.url || '#'}
|
|
onClick={handleLinkClick}
|
|
className="flex items-center gap-1.5 font-semibold text-pop-black hover:text-pop-primary transition-colors"
|
|
>
|
|
<span>{getLocalizedText(item.label, lang)}</span>
|
|
<ArrowRight className="h-3.5 w-3.5" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{hasColumns && isVisualCards && (
|
|
<div
|
|
className={`fixed left-1/2 z-50 w-[min(100vw-2rem,1040px)] -translate-x-1/2 transition-all duration-200 ${
|
|
isOpen ? 'opacity-100 pointer-events-auto translate-y-0' : 'opacity-0 pointer-events-none -translate-y-1'
|
|
}`}
|
|
style={{ top: 'var(--navbar-bottom, 56px)' }}
|
|
onMouseEnter={() => handleOpen(uniqueId)}
|
|
onMouseLeave={handleClose}
|
|
>
|
|
<div className="mx-auto h-3 w-20 overflow-hidden">
|
|
<div className="mx-auto mt-2 h-5 w-5 rotate-45 rounded-sm border-l border-t border-pop-black/10 bg-pop-bg/95" />
|
|
</div>
|
|
<div className="w-full cursor-default overflow-hidden rounded-2xl border border-pop-black/10 bg-pop-bg/95 p-6 shadow-2xl backdrop-blur-xl">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h4 className="text-xs font-bold uppercase tracking-wider text-pop-black/40">
|
|
{getLocalizedText(item.label, lang)}
|
|
</h4>
|
|
<Link
|
|
to={item.url || '#'}
|
|
onClick={handleLinkClick}
|
|
className="flex items-center gap-1 text-xs font-semibold text-pop-primary hover:underline"
|
|
>
|
|
<span>{lang === 'th' ? 'ดูทั้งหมด' : 'Explore all'}</span>
|
|
<ArrowRight className="h-3.5 w-3.5" />
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
|
{item.columns?.slice(0, 4).map((column, colIdx) => {
|
|
const cardImg = column.iconUrl || column.links?.[0]?.imageUrl || configuredImage || item.rootImageUrl;
|
|
const cardHref = column.links?.[0]?.url || item.url || '#';
|
|
const cardTitle = getLocalizedText(column.title, lang);
|
|
const firstSubLink = column.links?.[0];
|
|
const subText = firstSubLink ? getLocalizedText(firstSubLink.label, lang) : null;
|
|
|
|
return (
|
|
<Link
|
|
key={colIdx}
|
|
to={cardHref}
|
|
onClick={handleLinkClick}
|
|
className="group relative block overflow-hidden rounded-xl border border-pop-black/10 bg-pop-card transition-all hover:border-pop-primary/50 hover:shadow-lg"
|
|
>
|
|
<div className="relative aspect-[4/5] w-full overflow-hidden bg-pop-card/50">
|
|
<MenuImage
|
|
src={cardImg}
|
|
alt={cardTitle}
|
|
placeholder={cardTitle}
|
|
className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-105"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/85 via-black/25 to-transparent" />
|
|
<div className="absolute inset-x-0 bottom-0 p-4 space-y-1">
|
|
<span className="block text-sm font-bold text-white normal-case leading-snug drop-shadow-sm">
|
|
{cardTitle}
|
|
</span>
|
|
{subText && (
|
|
<span className="flex items-center justify-between text-[11px] font-normal text-white/80 normal-case">
|
|
<span className="truncate">{subText}</span>
|
|
<ArrowRight className="h-3.5 w-3.5 shrink-0 text-white/80 transition-transform group-hover:translate-x-1" />
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{hasColumns && !isClassic && !isTabbed && !isVisualCards && (
|
|
<div
|
|
className={`fixed left-1/2 z-50 ${getMegaContainerClass(visibleColumns.length, showFeatured)} -translate-x-1/2 transition-all duration-200 ${
|
|
isOpen ? 'opacity-100 pointer-events-auto translate-y-0' : 'opacity-0 pointer-events-none -translate-y-1'
|
|
}`}
|
|
style={{ top: 'var(--navbar-bottom, 56px)' }}
|
|
onMouseEnter={() => handleOpen(uniqueId)}
|
|
onMouseLeave={handleClose}
|
|
>
|
|
<div className="mx-auto h-3 w-20 overflow-hidden">
|
|
<div className="mx-auto mt-2 h-5 w-5 rotate-45 rounded-sm border-l border-t border-pop-black/10 bg-pop-bg/95" />
|
|
</div>
|
|
<div className="w-full overflow-hidden rounded-lg border border-pop-black/10 bg-pop-bg/95 shadow-2xl cursor-default backdrop-blur-xl">
|
|
<div className={`grid gap-0 px-8 py-8 ${getMegaGridColsClass(visibleColumns.length, showFeatured)}`}>
|
|
{visibleColumns.map((col, idx) => (
|
|
<div
|
|
key={idx}
|
|
className={`pr-8 text-left ${idx > 0 ? 'border-l border-pop-black/10 pl-8' : ''}`}
|
|
>
|
|
<h4 className="mb-5 flex items-center gap-3 text-xs font-bold normal-case tracking-normal text-pop-black">
|
|
<DynamicIcon
|
|
iconName={col.icon}
|
|
defaultIcon={Sparkles}
|
|
className="h-4 w-4 text-pop-black/70"
|
|
strokeWidth={1.5}
|
|
/>
|
|
{getLocalizedText(col.title, lang)}
|
|
</h4>
|
|
<ul className="space-y-2">
|
|
{col.links.slice(0, MAX_TOP_LEVEL_LINKS).map((link, lIdx) => {
|
|
const hasNestedLinks = !!link.links?.length;
|
|
const visibleChildLinks = link.links?.slice(0, MAX_INLINE_CHILD_LINKS) ?? [];
|
|
const hiddenChildCount = Math.max((link.links?.length ?? 0) - MAX_INLINE_CHILD_LINKS, 0);
|
|
const moreChildHref = link.url || link.links?.[MAX_INLINE_CHILD_LINKS]?.url || link.links?.[0]?.url || '#';
|
|
|
|
return (
|
|
<li key={lIdx}>
|
|
<div
|
|
className="rounded-md"
|
|
onMouseEnter={() => setHoveredLinkImage(link.imageUrl || null)}
|
|
onMouseLeave={() => setHoveredLinkImage(null)}
|
|
>
|
|
<Link
|
|
to={link.url || link.links?.[0]?.url || '#'}
|
|
onClick={handleLinkClick}
|
|
className="group flex items-center gap-4 rounded-md px-1 py-2.5 text-left transition-colors hover:bg-pop-black/5"
|
|
>
|
|
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-pop-black/10 bg-pop-bg text-pop-black/70 transition-colors group-hover:border-pop-black/30">
|
|
<DynamicIcon
|
|
iconName={link.icon}
|
|
defaultIcon={Box}
|
|
className="h-5 w-5"
|
|
strokeWidth={1.35}
|
|
/>
|
|
</span>
|
|
<span className="min-w-0 flex-1">
|
|
<span className="block text-sm font-bold normal-case tracking-normal text-pop-black">
|
|
{getLocalizedText(link.label, lang)}
|
|
</span>
|
|
<span className="mt-0.5 block truncate text-xs font-normal normal-case tracking-normal text-pop-black/55">
|
|
{getLocalizedText(col.title, lang)}
|
|
</span>
|
|
</span>
|
|
<ChevronRight className="h-4 w-4 shrink-0 text-pop-black/40 transition-colors group-hover:text-pop-black" />
|
|
</Link>
|
|
|
|
{hasNestedLinks && (
|
|
<ul className="ml-[3.25rem] mt-1 space-y-1 border-l border-pop-black/10 pl-3">
|
|
{visibleChildLinks.map((childLink, childIndex) => (
|
|
<li key={childIndex}>
|
|
<Link
|
|
to={childLink.url || '#'}
|
|
onClick={handleLinkClick}
|
|
className="block rounded-sm px-2 py-1.5 text-xs font-normal normal-case tracking-normal text-pop-black/60 transition-colors hover:bg-pop-black/5 hover:text-pop-black"
|
|
onMouseEnter={() => setHoveredLinkImage(childLink.imageUrl || link.imageUrl || null)}
|
|
>
|
|
{getLocalizedText(childLink.label, lang)}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
{hiddenChildCount > 0 && (
|
|
<li>
|
|
<Link
|
|
to={moreChildHref}
|
|
onClick={handleLinkClick}
|
|
className="block rounded-sm px-2 py-1.5 text-xs font-semibold normal-case tracking-normal text-pop-black transition-colors hover:bg-pop-black/5"
|
|
>
|
|
{getMoreLabel(hiddenChildCount, lang)}
|
|
</Link>
|
|
</li>
|
|
)}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
<Link
|
|
to={item.url || '#'}
|
|
onClick={handleLinkClick}
|
|
className="mt-7 flex items-center justify-between text-xs font-medium normal-case tracking-normal text-pop-black/60 transition-colors hover:text-pop-black"
|
|
>
|
|
<span>{getLocalizedText(item.label, lang)}</span>
|
|
<ArrowRight className="h-4 w-4" />
|
|
</Link>
|
|
</div>
|
|
))}
|
|
|
|
{showFeatured && (
|
|
<div className="border-l border-pop-black/10 pl-8">
|
|
<h4 className="mb-5 flex items-center gap-3 text-xs font-bold normal-case tracking-normal text-pop-black">
|
|
<Star className="h-4 w-4 text-pop-black/70" strokeWidth={1.5} />
|
|
{featuredHeading}
|
|
</h4>
|
|
<Link
|
|
to={featuredHref}
|
|
onClick={handleLinkClick}
|
|
className="group block overflow-hidden rounded-lg border border-pop-black/10 bg-pop-black/5 transition-colors hover:border-pop-black/30"
|
|
onMouseEnter={() => setHoveredLinkImage(configuredImage ?? featuredLink?.imageUrl ?? null)}
|
|
onMouseLeave={() => setHoveredLinkImage(null)}
|
|
>
|
|
<div className="relative h-40 w-full overflow-hidden bg-pop-black/5">
|
|
<MenuImage
|
|
src={featuredImage}
|
|
alt={featuredTitle}
|
|
placeholder={getLocalizedText(item.label, lang)}
|
|
className="transition-transform duration-700 group-hover:scale-105"
|
|
style={{ objectPosition }}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2 p-4">
|
|
{featuredBadge && (
|
|
<span className="inline-flex rounded-full bg-pop-bg px-2.5 py-1 text-[10px] font-bold normal-case tracking-normal text-pop-black/70">
|
|
{featuredBadge}
|
|
</span>
|
|
)}
|
|
<div>
|
|
<p className="line-clamp-2 text-sm font-bold normal-case tracking-normal text-pop-black">
|
|
{featuredTitle}
|
|
</p>
|
|
{featuredSubtitle && (
|
|
<p className="mt-1 line-clamp-1 text-xs font-normal normal-case tracking-normal text-pop-black/55">
|
|
{featuredSubtitle}
|
|
</p>
|
|
)}
|
|
{featuredConfig?.price && (
|
|
<p className="mt-2 text-sm font-bold normal-case tracking-normal text-pop-black">
|
|
{featuredConfig.price}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<span className="flex w-full items-center justify-center rounded-full border border-pop-black/20 px-4 py-2 text-xs font-bold normal-case tracking-normal text-pop-black transition-colors group-hover:bg-pop-black group-hover:text-pop-bg">
|
|
{featuredCta}
|
|
</span>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|