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

@@ -1,6 +1,25 @@
import { useState, useEffect, useRef } from 'react'
import { API_BASE, type Menu } from '@homaroma/types'
import { Save, Loader2, Plus, Trash2, GripVertical, Copy, Check, Download, Upload, Star, Pencil } from 'lucide-react'
import { ICON_REGISTRY, type AvailableIcons } from '../../utils/iconRegistry'
import FocalPointPicker from '../../components/page-builder/FocalPointPicker'
const IconSelect = ({ value, onChange }: { value?: string, onChange: (val: string) => void }) => {
return (
<select
className="admin-input w-full text-xs py-1 px-2 mt-2 bg-night-900 border border-white/10 text-white rounded"
value={value || ''}
onChange={e => onChange(e.target.value)}
>
<option value="">Default Icon (Sparkles/Box)</option>
{Object.keys(ICON_REGISTRY).map(iconName => (
<option key={iconName} value={iconName}>
{iconName}
</option>
))}
</select>
)
}
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
@@ -133,6 +152,15 @@ const FeaturedCardEditorFields = ({ featured, onChange }: any) => {
onChange={e => updateFeatured('imageUrl', e.target.value || null)}
/>
</div>
{featured.imageUrl && (
<div className="pt-2">
<FocalPointPicker
imageUrl={featured.imageUrl}
focalPoint={featured.focalPoint}
onChange={(focalPoint) => updateFeatured('focalPoint', focalPoint)}
/>
</div>
)}
<div className="space-y-1">
<label className="text-[10px] uppercase font-bold text-slate-500">Featured URL</label>
<input
@@ -233,10 +261,24 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured,
const featured = node.featured || {};
const featuredTitle = featured.title?.en || featured.title?.th || featured.sectionTitle?.en || featured.sectionTitle?.th || 'Not configured';
const [isCollapsed, setIsCollapsed] = useState(type === 'column');
return (
<div className={`border border-white/10 p-4 rounded-xl mt-4 ${depth > 0 ? 'ml-6 border-l-cyan-500/30' : ''}`}>
<div className="flex gap-4 items-start">
<div className="flex-1 space-y-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-white uppercase">{type}</span>
<span className="text-xs text-slate-400">{nodeLabel.en || nodeLabel.th || nodeLabel.ja || 'Untitled'}</span>
</div>
<button type="button" onClick={() => setIsCollapsed(!isCollapsed)} className="text-xs text-cyan-glow bg-white/5 hover:bg-white/10 px-2 py-1 rounded">
{isCollapsed ? 'Expand' : 'Collapse'}
</button>
</div>
{!isCollapsed && (
<>
<div className="flex gap-4 items-start">
<div className="flex-1 space-y-4">
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold text-slate-500">{labelKey} Translations</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
@@ -272,6 +314,12 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured,
placeholder="https://cdn.example.com/image.webp"
onChange={e => onChange({ ...node, [imageFieldKey]: e.target.value || null })}
/>
{type !== 'root' && (
<IconSelect
value={node.icon}
onChange={val => onChange({ ...node, icon: val || undefined })}
/>
)}
</div>
</div>
@@ -338,6 +386,8 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured,
))}
</div>
)}
</>
)}
</div>
);
}
+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;
+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;
@@ -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
}