Files
homaroma/apps/website/src/components/navigation/DesktopMegaMenu.tsx
T
2026-10-18 08:01:36 +07:00

657 lines
35 KiB
TypeScript

"use client"
import Link from '../LocalizedLink'
import Image from 'next/image'
import { useState, useRef, useEffect } from 'react'
import {
ArrowRight,
ChevronRight,
Star,
Sparkles,
Box,
} from 'lucide-react'
import type { MegaMenuPayload, MenuColumn } from './menuTypes'
import { getLocalizedMenuText } from './menuTypes'
import { ICON_REGISTRY, type AvailableIcons } from './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[]
lang: string
currentPath: string
}
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,
alt,
placeholder,
className = '',
style,
}: {
src?: string | null
alt: string
placeholder?: string
className?: string
style?: React.CSSProperties
}) {
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}`}
style={style}
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 [activeTabByMenu, setActiveTabByMenu] = useState<Record<string, number>>({})
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
// Automatically close mega menu when route changes
useEffect(() => {
if (closeTimer.current) clearTimeout(closeTimer.current)
setOpenId(null)
setHoveredLinkImage(null)
}, [currentPath])
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)
}
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 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 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 = 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'
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}
>
<Link
href={item.url || '#'}
onClick={handleLinkClick}
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 && isClassic && (
<div
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)' }}
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-lg border border-line-primary/15 bg-surface-card/95 shadow-card backdrop-blur-xl">
<div className={`grid gap-0 px-8 py-8 ${getMegaGridColsClass(visibleColumns.length, showFeatured)}`}>
{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">
<DynamicIcon
iconName={column.icon}
defaultIcon={Sparkles}
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 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={linkIndex}>
<div
className="rounded-md"
onMouseEnter={() => setHoveredLinkImage(link.imageUrl || null)}
onMouseLeave={() => setHoveredLinkImage(null)}
>
<Link
href={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-surface-muted"
>
<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">
<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-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>
{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 || '#'}
onClick={handleLinkClick}
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}
onClick={handleLinkClick}
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 || '#'}
onClick={handleLinkClick}
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}
onClick={handleLinkClick}
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)}
className="h-full w-full object-cover 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-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>
)}
</div>
)
})}
</div>
)
}