This commit is contained in:
Your Name committed 2026-07-14 02:26:38 -04:00
1 parent d8cca4fcfe
commit 52a5217d9b
8 files changed
+554 -107

No files matched your search

@@ -3,7 +3,20 @@
import Link from '../LocalizedLink'
import Image from 'next/image'
import { useState, useRef } from 'react'
import type { MegaMenuPayload } from './menuTypes'
import {
ArrowRight,
Award,
Box,
ChevronRight,
Gift,
Leaf,
Package,
ShoppingBag,
Sparkles,
Star,
type LucideIcon,
} from 'lucide-react'
import type { MegaMenuPayload, MenuColumn } from './menuTypes'
import { getLocalizedMenuText } from './menuTypes'
interface DesktopMegaMenuProps {
@@ -12,6 +25,27 @@ interface DesktopMegaMenuProps {
currentPath: string
}
const MENU_ICONS: LucideIcon[] = [
ShoppingBag,
Sparkles,
Leaf,
Package,
Gift,
Award,
]
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`
}
// Render a 1:1 image using next/image for optimization (website uses CDN images)
function MenuImage({
src,
@@ -64,6 +98,14 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
}, 150)
}
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 space-x-7 text-sm font-medium uppercase tracking-wide md:flex">
{items.map((item, index) => {
@@ -72,9 +114,22 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
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
const visibleColumns = item.columns?.slice(0, 2) ?? []
const featuredLink = getFirstImageLink(item.columns)
const featuredConfig = item.featured
const showFeatured = featuredConfig?.enabled !== false
const configuredImage = featuredConfig?.imageUrl || null
const featuredImage = hoveredLinkImage ?? configuredImage ?? featuredLink?.imageUrl ?? item.rootImageUrl
const featuredHeading = getLocalizedMenuText(featuredConfig?.sectionTitle, lang) || 'Featured'
const featuredBadge = getLocalizedMenuText(featuredConfig?.badge, lang) || 'Recommended'
const featuredTitle = getLocalizedMenuText(featuredConfig?.title, lang)
|| getLocalizedMenuText(featuredLink?.label, lang)
|| getLocalizedMenuText(item.label, lang)
const featuredSubtitle = getLocalizedMenuText(featuredConfig?.subtitle, lang) || (item.columns?.[0]
? getLocalizedMenuText(item.columns[0].title, lang)
: getLocalizedMenuText(item.label, lang))
const featuredHref = featuredConfig?.url || featuredLink?.url || item.url || '#'
const featuredCta = getLocalizedMenuText(featuredConfig?.ctaLabel, lang) || 'View details'
return (
<div
@@ -106,76 +161,150 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
{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'
className={`fixed left-1/2 z-50 w-[min(100vw-2rem,980px)] -translate-x-1/2 transition-all duration-200 ${
isOpen ? 'pointer-events-auto translate-y-0 opacity-100' : 'pointer-events-none -translate-y-1 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">
<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-line-primary/15 bg-surface-card/95" />
</div>
<div className="w-full cursor-default overflow-hidden rounded-lg border border-line-primary/15 bg-surface-card/95 shadow-card backdrop-blur-xl">
<div className={`grid gap-0 px-8 py-8 ${showFeatured ? 'grid-cols-[1fr_1fr_270px]' : 'grid-cols-2'}`}>
{visibleColumns.map((column, columnIndex) => (
<div
key={columnIndex}
className={`pr-8 ${columnIndex > 0 ? 'border-l border-line-primary/15 pl-8' : ''}`}
>
<h4 className="mb-5 flex items-center gap-3 text-xs font-bold normal-case tracking-normal text-text-main">
{(() => {
const HeadingIcon = MENU_ICONS[columnIndex] ?? Sparkles
return <HeadingIcon className="h-4 w-4 text-brand-primary" strokeWidth={1.5} />
})()}
{getLocalizedMenuText(column.title, lang)}
</h4>
<ul className="space-y-2">
{column.links.slice(0, MAX_TOP_LEVEL_LINKS).map((link, linkIndex) => {
const RowIcon = MENU_ICONS[(columnIndex * 3 + linkIndex + 1) % MENU_ICONS.length] ?? Box
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 || '#'
{/* 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}>
return (
<li key={linkIndex}>
<div
className="rounded-md"
onMouseEnter={() => setHoveredLinkImage(link.imageUrl || null)}
onMouseLeave={() => setHoveredLinkImage(null)}
>
<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)}
href={link.url || link.links?.[0]?.url || '#'}
className="group flex items-center gap-4 rounded-md px-1 py-2.5 text-left transition-colors hover:bg-surface-muted"
>
{getLocalizedMenuText(link.label, lang)}
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-line-primary/20 bg-surface-page text-brand-primary transition-colors group-hover:border-brand-primary/40">
<RowIcon 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-text-main">
{getLocalizedMenuText(link.label, lang)}
</span>
<span className="mt-0.5 block truncate text-xs font-normal normal-case tracking-normal text-muted-text">
{getLocalizedMenuText(column.title, lang)}
</span>
</span>
<ChevronRight className="h-4 w-4 shrink-0 text-text-main/40 transition-colors group-hover:text-brand-primary" />
</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"
/>
{hasNestedLinks && (
<ul className="ml-[3.25rem] mt-1 space-y-1 border-l border-line-primary/15 pl-3">
{visibleChildLinks.map((childLink, childIndex) => (
<li key={childIndex}>
<Link
href={childLink.url || '#'}
className="block rounded-sm px-2 py-1.5 text-xs font-normal normal-case tracking-normal text-muted-text transition-colors hover:bg-surface-muted hover:text-brand-primary"
onMouseEnter={() => setHoveredLinkImage(childLink.imageUrl || link.imageUrl || null)}
>
{getLocalizedMenuText(childLink.label, lang)}
</Link>
</li>
))}
{hiddenChildCount > 0 && (
<li>
<Link
href={moreChildHref}
className="block rounded-sm px-2 py-1.5 text-xs font-semibold normal-case tracking-normal text-brand-primary transition-colors hover:bg-surface-muted"
>
{getMoreLabel(hiddenChildCount, lang)}
</Link>
</li>
)}
</ul>
)}
</div>
</li>
)
})}
</ul>
<Link
href={item.url || '#'}
className="mt-7 flex items-center justify-between text-xs font-medium normal-case tracking-normal text-text-main/65 transition-colors hover:text-brand-primary"
>
<span>{getLocalizedMenuText(item.label, lang)}</span>
<ArrowRight className="h-4 w-4" />
</Link>
</div>
))}
{showFeatured && (
<div className="border-l border-line-primary/15 pl-8">
<h4 className="mb-5 flex items-center gap-3 text-xs font-bold normal-case tracking-normal text-text-main">
<Star className="h-4 w-4 text-brand-primary" strokeWidth={1.5} />
{featuredHeading}
</h4>
<Link
href={featuredHref}
className="group block overflow-hidden rounded-lg border border-line-primary/20 bg-surface-page transition-colors hover:border-brand-primary/40"
onMouseEnter={() => setHoveredLinkImage(configuredImage ?? featuredLink?.imageUrl ?? null)}
onMouseLeave={() => setHoveredLinkImage(null)}
>
<div className="relative h-40 w-full overflow-hidden bg-surface-muted">
<MenuImage
src={featuredImage}
alt={featuredTitle}
placeholder={getLocalizedMenuText(item.label, lang)}
/>
</div>
<div className="space-y-2 p-4">
{featuredBadge && (
<span className="inline-flex rounded-full bg-surface-card px-2.5 py-1 text-[10px] font-bold normal-case tracking-normal text-brand-primary">
{featuredBadge}
</span>
)}
<div>
<p className="line-clamp-2 text-sm font-bold normal-case tracking-normal text-text-main">
{featuredTitle}
</p>
{featuredSubtitle && (
<p className="mt-1 line-clamp-1 text-xs font-normal normal-case tracking-normal text-muted-text">
{featuredSubtitle}
</p>
)}
{featuredConfig?.price && (
<p className="mt-2 text-sm font-bold normal-case tracking-normal text-text-main">
{featuredConfig.price}
</p>
)}
</div>
<span className="flex w-full items-center justify-center rounded-full border border-brand-primary/40 px-4 py-2 text-xs font-bold normal-case tracking-normal text-text-main transition-colors group-hover:bg-brand-primary group-hover:text-surface-card">
{featuredCta}
</span>
</div>
</Link>
</div>
)}
</div>
</div>
</div>