This commit is contained in:
Your Name committed 2026-07-14 19:26:20 -04:00
1 parent f6c188bd93
commit b78c9467bc
9 files changed
+656 -49

No files matched your search

@@ -5,19 +5,24 @@ import Image from 'next/image'
import { useState, useRef } from 'react'
import {
ArrowRight,
Award,
Box,
ChevronRight,
Gift,
Leaf,
Package,
ShoppingBag,
Sparkles,
Star,
type LucideIcon,
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[]
@@ -25,14 +30,7 @@ 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
@@ -52,11 +50,13 @@ function MenuImage({
alt,
placeholder,
className = '',
style,
}: {
src?: string | null
alt: string
placeholder?: string
className?: string
style?: React.CSSProperties
}) {
const [loaded, setLoaded] = useState(false)
@@ -74,6 +74,7 @@ function MenuImage({
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"
/>
@@ -120,6 +121,8 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
const showFeatured = featuredConfig?.enabled !== false
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)
@@ -179,15 +182,16 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
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} />
})()}
<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 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)
@@ -205,7 +209,12 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
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">
<RowIcon className="h-5 w-5" strokeWidth={1.35} />
<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">
@@ -275,6 +284,8 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
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">
@@ -0,0 +1,103 @@
import {
// Business
Sparkles, Leaf, Wind, Droplets, Flower2, Flame, Sofa, BedDouble, Bath,
Sun, Moon, TreePine, Sprout, Coffee, Waves, Cherry, Citrus, Snowflake,
Palmtree, Crown, Gem, Feather, Smile, Zap, Cloud, Beaker, SprayCan,
Wine, MountainSnow, Vegan,
// E-commerce
ShoppingBag, ShoppingCart, Package, Truck, CreditCard, Wallet, Tag, Percent,
Gift, Ticket, Store, User, Heart, Clock, MapPin, Settings, LogOut, LogIn,
Bell, Bookmark, Search, Menu, X, ArrowRight, ArrowLeft, ChevronRight,
ChevronDown, Plus, Minus, Trash2, Eye, Filter, SlidersHorizontal, RefreshCw,
Camera, Share2, Award, ShieldCheck, Star, ThumbsUp, HelpCircle, Info,
Phone, Mail, MessageCircle, CheckCircle2, XCircle, AlertCircle, Calendar, Box
} from 'lucide-react';
export const ICON_REGISTRY = {
// Business
'sparkles': Sparkles,
'leaf': Leaf,
'wind': Wind,
'droplets': Droplets,
'flower': Flower2,
'flame': Flame,
'sofa': Sofa,
'bed': BedDouble,
'bath': Bath,
'sun': Sun,
'moon': Moon,
'tree-pine': TreePine,
'sprout': Sprout,
'coffee': Coffee,
'waves': Waves,
'cherry': Cherry,
'citrus': Citrus,
'snowflake': Snowflake,
'palmtree': Palmtree,
'crown': Crown,
'gem': Gem,
'feather': Feather,
'smile': Smile,
'zap': Zap,
'cloud': Cloud,
'beaker': Beaker,
'spray-can': SprayCan,
'wine': Wine,
'mountain': MountainSnow,
'vegan': Vegan,
// E-commerce
'shopping-bag': ShoppingBag,
'shopping-cart': ShoppingCart,
'package': Package,
'truck': Truck,
'credit-card': CreditCard,
'wallet': Wallet,
'tag': Tag,
'percent': Percent,
'gift': Gift,
'ticket': Ticket,
'store': Store,
'user': User,
'heart': Heart,
'clock': Clock,
'map-pin': MapPin,
'settings': Settings,
'logout': LogOut,
'login': LogIn,
'bell': Bell,
'bookmark': Bookmark,
'search': Search,
'menu': Menu,
'close': X,
'arrow-right': ArrowRight,
'arrow-left': ArrowLeft,
'chevron-right': ChevronRight,
'chevron-down': ChevronDown,
'plus': Plus,
'minus': Minus,
'trash': Trash2,
'eye': Eye,
'filter': Filter,
'sliders': SlidersHorizontal,
'refresh': RefreshCw,
'camera': Camera,
'share': Share2,
'award': Award,
'shield-check': ShieldCheck,
'star': Star,
'thumbs-up': ThumbsUp,
'help': HelpCircle,
'info': Info,
'phone': Phone,
'mail': Mail,
'chat': MessageCircle,
'check-circle': CheckCircle2,
'x-circle': XCircle,
'alert': AlertCircle,
'calendar': Calendar,
'box': Box,
} as const;
export type AvailableIcons = keyof typeof ICON_REGISTRY;
@@ -3,12 +3,14 @@ export interface MenuLink {
url?: string
links?: MenuLink[]
imageUrl?: string | null // รูปที่แสดงเมื่อ Hover ลิงก์
icon?: string // ชื่อ icon จาก registry (lucide)
}
export interface MenuColumn {
title: Record<string, string> | string
links: MenuLink[]
iconUrl?: string | null // รูปประจำคอลัมน์ (1:1)
icon?: string // ชื่อ icon จาก registry (lucide)
}
export interface MenuFeaturedCard {
@@ -19,6 +21,7 @@ export interface MenuFeaturedCard {
subtitle?: Record<string, string> | string
price?: string
imageUrl?: string | null
focalPoint?: { x: string; y: string }
url?: string
ctaLabel?: Record<string, string> | string
}