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
}
+224
View File
@@ -0,0 +1,224 @@
# HomAroma: Curated Lucide Icon Registry
This document contains a curated list of 80 `lucide-react` icons tailored specifically for the **HomAroma** project. It is divided into 30 business-specific icons (Home Fragrance & Wellness) and 50 general E-commerce icons.
You can use this list directly in your `iconRegistry.ts` file to populate the Admin Dropdown and Frontend components.
## 🌿 30 Business-Specific Icons (HomAroma / Fragrance / Wellness)
These icons are selected to represent product categories, scent notes, moods, and the core identity of a home fragrance and wellness brand.
| Icon Name | Suggested Usage / Meaning |
| :--- | :--- |
| `Sparkles` | Signature products, Best sellers, Magic / Premium |
| `Leaf` | Organic, Natural ingredients, Green notes |
| `Wind` | Aroma, Diffusers, Air circulation, Scent throw |
| `Droplets` | Essential oils, Serums, Liquid extracts |
| `Flower2` | Floral scent notes (Rose, Jasmine, etc.) |
| `Flame` | Candles, Warmth, Fireplace |
| `Sofa` | Home decor, Living room ambiance |
| `BedDouble` | Bedroom, Sleep wellness, Pillow mists |
| `Bath` | Bath & Body products, Spa, Relaxation |
| `Sun` | Morning routines, Bright / Uplifting scents |
| `Moon` | Night routines, Calming / Sleep scents |
| `TreePine` | Woody scent notes (Cedar, Sandalwood, Pine) |
| `Sprout` | Plant-based, Vegan, Clean beauty |
| `Coffee` | Gourmand notes (Coffee, Vanilla, Warmth) |
| `Waves` | Ocean breeze, Aquatic scent notes |
| `Cherry` | Fruity / Berry scent notes |
| `Citrus` | Citrus notes (Orange, Lemon, Bergamot) |
| `Snowflake` | Cooling, Mint, Crisp winter notes |
| `Palmtree` | Tropical / Summer collections |
| `Crown` | Luxury / Exclusive collections |
| `Gem` | Rare / Premium ingredients |
| `Feather` | Light, Gentle, Sensitive skin |
| `Smile` | Mood enhancing, Happiness |
| `Zap` | Energizing, Invigorating scents |
| `Cloud` | Airy, Soft, Linen sprays |
| `Beaker` | Formulation, Lab, Science of scent |
| `SprayCan` | Room sprays, Body mists |
| `Wine` | Rich / Boozy notes (Bourbon, Rum) |
| `MountainSnow` | Earthy, Fresh mountain air |
| `Vegan` | Cruelty-free, Vegan certification |
---
## 🛒 50 General E-commerce & UI Icons
These icons are essential for standard E-commerce navigation, user accounts, checkout flows, and UI controls.
### Shopping & Cart
| Icon Name | Suggested Usage / Meaning |
| :--- | :--- |
| `ShoppingBag` | Cart (Bag style) |
| `ShoppingCart` | Cart (Trolley style) |
| `Package` | Orders, Physical products |
| `Truck` | Shipping, Delivery |
| `CreditCard` | Payment methods |
| `Wallet` | Store credit, Points |
| `Tag` | Sale, Price tags |
| `Percent` | Discounts, Promotions |
| `Gift` | Gift cards, Wrapping service |
| `Ticket` | Vouchers, Coupons |
| `Store` | Physical stores, About the brand |
### Account & User
| Icon Name | Suggested Usage / Meaning |
| :--- | :--- |
| `User` | My Account, Profile |
| `Heart` | Wishlist, Favorites |
| `Clock` | Order history, Recently viewed |
| `MapPin` | Shipping address, Store locator |
| `Settings` | Account settings |
| `LogOut` | Sign out |
| `LogIn` | Sign in |
| `Bell` | Notifications |
| `Bookmark` | Saved for later |
### Navigation & Actions
| Icon Name | Suggested Usage / Meaning |
| :--- | :--- |
| `Search` | Search bar |
| `Menu` | Mobile hamburger menu |
| `X` | Close modals / drawers |
| `ArrowRight` | Next step, Call to action |
| `ArrowLeft` | Go back |
| `ChevronRight` | Breadcrumbs, Accordion |
| `ChevronDown` | Dropdowns |
| `Plus` | Add to cart / Expand |
| `Minus` | Decrease quantity / Collapse |
| `Trash2` | Remove item |
| `Eye` | Quick view |
| `Filter` | Filter products |
| `SlidersHorizontal` | Sort & Filter options |
| `RefreshCw` | Returns, Exchanges, Sync |
| `Camera` | Upload photo (Reviews) |
| `Share2` | Share product |
### Information & Trust
| Icon Name | Suggested Usage / Meaning |
| :--- | :--- |
| `Award` | Quality guarantee, Awards |
| `ShieldCheck` | Secure payment, Warranty |
| `Star` | Reviews, Ratings |
| `ThumbsUp` | Helpful review |
| `HelpCircle` | FAQ, Help center |
| `Info` | Product details, About |
| `Phone` | Customer service (Phone) |
| `Mail` | Newsletter, Email support |
| `MessageCircle` | Live chat |
| `CheckCircle2` | Success states, Validation |
| `XCircle` | Error states |
| `AlertCircle` | Warnings, Important notices |
| `Calendar` | Delivery dates, Events |
| `Box` | Inventory, General item |
---
## 💻 Example Implementation (TypeScript)
You can copy and paste this into your `iconRegistry.ts` file:
```typescript
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;
```