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

+32 -25
View File
@@ -4,17 +4,21 @@ import type { MegaMenuPayload, MenuColumn } from '../store/menuStore';
import { useTranslation } from 'react-i18next';
import {
ArrowRight,
Award,
Box,
ChevronRight,
Gift,
Leaf,
Package,
ShoppingBag,
Sparkles,
Star,
type LucideIcon,
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[];
@@ -26,14 +30,7 @@ const getLocalizedText = (textObj: Record<string, string> | string | undefined,
return textObj[lang] || textObj.en || textObj.th || Object.values(textObj)[0] || '';
};
const MENU_ICONS: LucideIcon[] = [
ShoppingBag,
Sparkles,
Leaf,
Package,
Gift,
Award,
];
const MAX_TOP_LEVEL_LINKS = 5;
const MAX_INLINE_CHILD_LINKS = 3;
@@ -48,16 +45,16 @@ const getMoreLabel = (count: number, lang: string) => {
};
// Image block shared between Root and Hover areas
function MenuImage({ src, alt, placeholder }: { src?: string | null; alt: string; placeholder?: string }) {
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">
<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" loading="lazy" />;
return <img src={src} alt={alt} className={`w-full h-full object-cover ${className || ''}`} style={style} loading="lazy" />;
}
export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
@@ -105,6 +102,8 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
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 = getLocalizedText(featuredConfig?.sectionTitle, lang) || 'Featured';
const featuredBadge = getLocalizedText(featuredConfig?.badge, lang) || 'Recommended';
const featuredTitle = getLocalizedText(featuredConfig?.title, lang)
@@ -165,15 +164,16 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
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">
{(() => {
const HeadingIcon = MENU_ICONS[idx] ?? Sparkles;
return <HeadingIcon className="h-4 w-4 text-pop-black/70" strokeWidth={1.5} />;
})()}
<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 RowIcon = MENU_ICONS[(idx * 3 + lIdx + 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);
@@ -191,7 +191,12 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
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">
<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-pop-black">
@@ -261,6 +266,8 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
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">
+3
View File
@@ -6,12 +6,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 {
@@ -22,6 +24,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;
}
+103
View File
@@ -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;