x
This commit is contained in:
1 parent
dd79519bf7
commit
765cdd1fa9
11 files changed
+1007
-71
No files matched your search
@@ -84,6 +84,7 @@ function MenuImage({
|
||||
export function DesktopMegaMenu({ items, lang, currentPath }: 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)
|
||||
|
||||
// Automatically close mega menu when route changes
|
||||
@@ -128,10 +129,17 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
|
||||
const hasColumns = !!item.columns?.length
|
||||
const isActive = item.url ? currentPath === item.url : false
|
||||
|
||||
const visibleColumns = item.columns?.slice(0, 2) ?? []
|
||||
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 = featuredConfig?.enabled !== false
|
||||
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
|
||||
@@ -147,6 +155,28 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
|
||||
const featuredHref = featuredConfig?.url || featuredLink?.url || item.url || '#'
|
||||
const featuredCta = getLocalizedMenuText(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}
|
||||
@@ -176,9 +206,288 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{hasColumns && (
|
||||
{hasColumns && isClassic && (
|
||||
<div
|
||||
className={`fixed left-1/2 z-50 w-[min(100vw-2rem,980px)] -translate-x-1/2 transition-all duration-200 ${
|
||||
className={`absolute left-0 top-full z-50 pt-2 transition-all duration-200 ${
|
||||
isOpen ? 'pointer-events-auto translate-y-0 opacity-100' : 'pointer-events-none -translate-y-1 opacity-0'
|
||||
}`}
|
||||
onMouseEnter={() => handleOpen(uniqueId)}
|
||||
onMouseLeave={handleClose}
|
||||
>
|
||||
<div className="min-w-[230px] max-w-xs cursor-default overflow-hidden rounded-xl border border-line-primary/15 bg-surface-card/95 p-2 shadow-card backdrop-blur-xl">
|
||||
{item.columns?.map((column, columnIndex) => (
|
||||
<div key={columnIndex} className={columnIndex > 0 ? 'mt-2 border-t border-line-primary/10 pt-2' : ''}>
|
||||
{getLocalizedMenuText(column.title, lang) && item.columns && item.columns.length > 1 && (
|
||||
<h5 className="px-3 py-1 text-[10px] font-bold uppercase tracking-wider text-muted-text">
|
||||
{getLocalizedMenuText(column.title, lang)}
|
||||
</h5>
|
||||
)}
|
||||
<ul className="space-y-0.5">
|
||||
{column.links.map((link, linkIndex) => (
|
||||
<li key={linkIndex}>
|
||||
<Link
|
||||
href={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-text-main transition-colors hover:bg-surface-muted hover:text-brand-primary"
|
||||
>
|
||||
<span className="flex items-center gap-2.5">
|
||||
{link.icon && (
|
||||
<DynamicIcon
|
||||
iconName={link.icon}
|
||||
defaultIcon={Box}
|
||||
className="h-4 w-4 shrink-0 text-brand-primary/80 transition-colors group-hover:text-brand-primary"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
<span>{getLocalizedMenuText(link.label, lang)}</span>
|
||||
</span>
|
||||
{link.links?.length ? (
|
||||
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-text-main/30 transition-colors group-hover:text-brand-primary" />
|
||||
) : null}
|
||||
</Link>
|
||||
{link.links && link.links.length > 0 && (
|
||||
<ul className="ml-4 space-y-0.5 border-l border-line-primary/15 pl-2 my-1">
|
||||
{link.links.map((child, childIdx) => (
|
||||
<li key={childIdx}>
|
||||
<Link
|
||||
href={child.url || '#'}
|
||||
onClick={handleLinkClick}
|
||||
className="block rounded-md px-2 py-1 text-[11px] font-normal normal-case tracking-normal text-muted-text transition-colors hover:bg-surface-muted hover:text-brand-primary"
|
||||
>
|
||||
{getLocalizedMenuText(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 ? '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="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-xl border border-line-primary/15 bg-surface-card/95 shadow-card 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-line-primary/15 bg-surface-page/50 p-3 space-y-1 overflow-y-auto">
|
||||
<div className="px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-muted-text">
|
||||
{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-brand-primary/10 text-brand-primary font-bold shadow-sm'
|
||||
: 'text-text-main/80 hover:bg-surface-muted hover:text-text-main 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-brand-primary' : 'text-brand-primary/60'}`}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<span className="truncate text-xs normal-case">{getLocalizedMenuText(column.title, lang)}</span>
|
||||
</span>
|
||||
<ChevronRight className={`h-3.5 w-3.5 shrink-0 transition-transform ${isSelected ? 'text-brand-primary translate-x-0.5' : 'text-text-main/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-line-primary/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-brand-primary"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<h3 className="text-sm font-bold normal-case tracking-normal text-text-main">
|
||||
{getLocalizedMenuText(activeCol.title, lang)}
|
||||
</h3>
|
||||
</div>
|
||||
<Link
|
||||
href={activeCol.links[0]?.url || item.url || '#'}
|
||||
onClick={handleLinkClick}
|
||||
className="flex items-center gap-1.5 text-xs font-semibold normal-case text-brand-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-surface-muted transition-colors group"
|
||||
onMouseEnter={() => setHoveredLinkImage(link.imageUrl || null)}
|
||||
onMouseLeave={() => setHoveredLinkImage(null)}
|
||||
>
|
||||
<Link
|
||||
href={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-line-primary/20 bg-surface-page text-brand-primary group-hover:border-brand-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-text-main group-hover:text-brand-primary transition-colors">
|
||||
{getLocalizedMenuText(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}
|
||||
href={child.url || '#'}
|
||||
onClick={handleLinkClick}
|
||||
className="text-[10px] text-muted-text hover:text-brand-primary transition-colors bg-surface-page/60 px-1.5 py-0.5 rounded border border-line-primary/10"
|
||||
>
|
||||
{getLocalizedMenuText(child.label, lang)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-line-primary/10 pt-4 mt-6 flex items-center justify-between text-xs text-muted-text">
|
||||
<span>{item.columns?.length || 0} {lang === 'th' ? 'หมวดหมู่ย่อย' : 'categories'}</span>
|
||||
<Link
|
||||
href={item.url || '#'}
|
||||
onClick={handleLinkClick}
|
||||
className="flex items-center gap-1.5 font-semibold text-text-main hover:text-brand-primary transition-colors"
|
||||
>
|
||||
<span>{getLocalizedMenuText(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 ? '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="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-2xl border border-line-primary/15 bg-surface-card/95 p-6 shadow-card backdrop-blur-xl">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-muted-text">
|
||||
{getLocalizedMenuText(item.label, lang)}
|
||||
</h4>
|
||||
<Link
|
||||
href={item.url || '#'}
|
||||
onClick={handleLinkClick}
|
||||
className="flex items-center gap-1 text-xs font-semibold text-brand-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 = getLocalizedMenuText(column.title, lang)
|
||||
const firstSubLink = column.links?.[0]
|
||||
const subText = firstSubLink ? getLocalizedMenuText(firstSubLink.label, lang) : null
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={colIdx}
|
||||
href={cardHref}
|
||||
onClick={handleLinkClick}
|
||||
className="group relative block overflow-hidden rounded-xl border border-line-primary/20 bg-surface-page transition-all hover:border-brand-primary/50 hover:shadow-lg"
|
||||
>
|
||||
<div className="relative aspect-[4/5] w-full overflow-hidden bg-surface-muted">
|
||||
<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 ? 'pointer-events-auto translate-y-0 opacity-100' : 'pointer-events-none -translate-y-1 opacity-0'
|
||||
}`}
|
||||
style={{ top: 'var(--navbar-bottom, 64px)' }}
|
||||
@@ -189,7 +498,7 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
|
||||
<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'}`}>
|
||||
<div className={`grid gap-0 px-8 py-8 ${getMegaGridColsClass(visibleColumns.length, showFeatured)}`}>
|
||||
{visibleColumns.map((column, columnIndex) => (
|
||||
<div
|
||||
key={columnIndex}
|
||||
|
||||
@@ -26,11 +26,21 @@ export interface MenuFeaturedCard {
|
||||
ctaLabel?: Record<string, string> | string
|
||||
}
|
||||
|
||||
export type SubmenuLayoutType = 'columns' | 'classic' | 'tabbed' | 'visual-cards'
|
||||
|
||||
export interface SubmenuLayoutConfig {
|
||||
columnCount?: 2 | 3 | 4
|
||||
showFeatured?: boolean
|
||||
itemStyle?: 'icon' | 'thumbnail' | 'text-only'
|
||||
}
|
||||
|
||||
export interface MegaMenuPayload {
|
||||
id?: string
|
||||
nodeId?: string
|
||||
label: Record<string, string> | string
|
||||
url?: string
|
||||
layout?: SubmenuLayoutType
|
||||
layoutConfig?: SubmenuLayoutConfig
|
||||
columns?: MenuColumn[]
|
||||
rootImageUrl?: string | null // รูปหลักฝั่งซ้ายสุด (1:1)
|
||||
featured?: MenuFeaturedCard
|
||||
|
||||
Reference in new issue
Block a user