x
This commit is contained in:
1 parent
f6c188bd93
commit
b78c9467bc
9 files changed
+654
-47
No files matched your search
@@ -1,6 +1,25 @@
|
|||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { API_BASE, type Menu } from '@homaroma/types'
|
import { API_BASE, type Menu } from '@homaroma/types'
|
||||||
import { Save, Loader2, Plus, Trash2, GripVertical, Copy, Check, Download, Upload, Star, Pencil } from 'lucide-react'
|
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'];
|
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)}
|
onChange={e => updateFeatured('imageUrl', e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="space-y-1">
|
||||||
<label className="text-[10px] uppercase font-bold text-slate-500">Featured URL</label>
|
<label className="text-[10px] uppercase font-bold text-slate-500">Featured URL</label>
|
||||||
<input
|
<input
|
||||||
@@ -233,8 +261,22 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured,
|
|||||||
const featured = node.featured || {};
|
const featured = node.featured || {};
|
||||||
const featuredTitle = featured.title?.en || featured.title?.th || featured.sectionTitle?.en || featured.sectionTitle?.th || 'Not configured';
|
const featuredTitle = featured.title?.en || featured.title?.th || featured.sectionTitle?.en || featured.sectionTitle?.th || 'Not configured';
|
||||||
|
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(type === 'column');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`border border-white/10 p-4 rounded-xl mt-4 ${depth > 0 ? 'ml-6 border-l-cyan-500/30' : ''}`}>
|
<div className={`border border-white/10 p-4 rounded-xl mt-4 ${depth > 0 ? 'ml-6 border-l-cyan-500/30' : ''}`}>
|
||||||
|
<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 gap-4 items-start">
|
||||||
<div className="flex-1 space-y-4">
|
<div className="flex-1 space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -272,6 +314,12 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured,
|
|||||||
placeholder="https://cdn.example.com/image.webp"
|
placeholder="https://cdn.example.com/image.webp"
|
||||||
onChange={e => onChange({ ...node, [imageFieldKey]: e.target.value || null })}
|
onChange={e => onChange({ ...node, [imageFieldKey]: e.target.value || null })}
|
||||||
/>
|
/>
|
||||||
|
{type !== 'root' && (
|
||||||
|
<IconSelect
|
||||||
|
value={node.icon}
|
||||||
|
onChange={val => onChange({ ...node, icon: val || undefined })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -338,6 +386,8 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured,
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -4,17 +4,21 @@ import type { MegaMenuPayload, MenuColumn } from '../store/menuStore';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Award,
|
|
||||||
Box,
|
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Gift,
|
|
||||||
Leaf,
|
|
||||||
Package,
|
|
||||||
ShoppingBag,
|
|
||||||
Sparkles,
|
|
||||||
Star,
|
Star,
|
||||||
type LucideIcon,
|
Sparkles,
|
||||||
|
Box,
|
||||||
} from 'lucide-react';
|
} 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 {
|
interface DesktopMegaMenuProps {
|
||||||
items: MegaMenuPayload[];
|
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] || '';
|
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_TOP_LEVEL_LINKS = 5;
|
||||||
const MAX_INLINE_CHILD_LINKS = 3;
|
const MAX_INLINE_CHILD_LINKS = 3;
|
||||||
@@ -48,16 +45,16 @@ const getMoreLabel = (count: number, lang: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Image block shared between Root and Hover areas
|
// 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 (!src) {
|
||||||
if (!placeholder) return null;
|
if (!placeholder) return null;
|
||||||
return (
|
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>
|
<span className="text-pop-black/30 text-xs text-center px-3 leading-relaxed">{placeholder}</span>
|
||||||
</div>
|
</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) {
|
export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
|
||||||
@@ -105,6 +102,8 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
|
|||||||
const showFeatured = featuredConfig?.enabled !== false;
|
const showFeatured = featuredConfig?.enabled !== false;
|
||||||
const configuredImage = featuredConfig?.imageUrl || null;
|
const configuredImage = featuredConfig?.imageUrl || null;
|
||||||
const featuredImage = hoveredLinkImage ?? configuredImage ?? featuredLink?.imageUrl ?? item.rootImageUrl;
|
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 featuredHeading = getLocalizedText(featuredConfig?.sectionTitle, lang) || 'Featured';
|
||||||
const featuredBadge = getLocalizedText(featuredConfig?.badge, lang) || 'Recommended';
|
const featuredBadge = getLocalizedText(featuredConfig?.badge, lang) || 'Recommended';
|
||||||
const featuredTitle = getLocalizedText(featuredConfig?.title, lang)
|
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' : ''}`}
|
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">
|
<h4 className="mb-5 flex items-center gap-3 text-xs font-bold normal-case tracking-normal text-pop-black">
|
||||||
{(() => {
|
<DynamicIcon
|
||||||
const HeadingIcon = MENU_ICONS[idx] ?? Sparkles;
|
iconName={col.icon}
|
||||||
return <HeadingIcon className="h-4 w-4 text-pop-black/70" strokeWidth={1.5} />;
|
defaultIcon={Sparkles}
|
||||||
})()}
|
className="h-4 w-4 text-pop-black/70"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
{getLocalizedText(col.title, lang)}
|
{getLocalizedText(col.title, lang)}
|
||||||
</h4>
|
</h4>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{col.links.slice(0, MAX_TOP_LEVEL_LINKS).map((link, lIdx) => {
|
{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 hasNestedLinks = !!link.links?.length;
|
||||||
const visibleChildLinks = link.links?.slice(0, MAX_INLINE_CHILD_LINKS) ?? [];
|
const visibleChildLinks = link.links?.slice(0, MAX_INLINE_CHILD_LINKS) ?? [];
|
||||||
const hiddenChildCount = Math.max((link.links?.length ?? 0) - MAX_INLINE_CHILD_LINKS, 0);
|
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"
|
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">
|
<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>
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block text-sm font-bold normal-case tracking-normal text-pop-black">
|
<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}
|
src={featuredImage}
|
||||||
alt={featuredTitle}
|
alt={featuredTitle}
|
||||||
placeholder={getLocalizedText(item.label, lang)}
|
placeholder={getLocalizedText(item.label, lang)}
|
||||||
|
className="transition-transform duration-700 group-hover:scale-105"
|
||||||
|
style={{ objectPosition }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 p-4">
|
<div className="space-y-2 p-4">
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ export interface MenuLink {
|
|||||||
url?: string;
|
url?: string;
|
||||||
links?: MenuLink[];
|
links?: MenuLink[];
|
||||||
imageUrl?: string | null; // รูปที่แสดงเมื่อ Hover ลิงก์
|
imageUrl?: string | null; // รูปที่แสดงเมื่อ Hover ลิงก์
|
||||||
|
icon?: string; // ชื่อ icon จาก registry (lucide)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuColumn {
|
export interface MenuColumn {
|
||||||
title: Record<string, string> | string;
|
title: Record<string, string> | string;
|
||||||
links: MenuLink[];
|
links: MenuLink[];
|
||||||
iconUrl?: string | null; // รูปประจำคอลัมน์ (1:1)
|
iconUrl?: string | null; // รูปประจำคอลัมน์ (1:1)
|
||||||
|
icon?: string; // ชื่อ icon จาก registry (lucide)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuFeaturedCard {
|
export interface MenuFeaturedCard {
|
||||||
@@ -22,6 +24,7 @@ export interface MenuFeaturedCard {
|
|||||||
subtitle?: Record<string, string> | string;
|
subtitle?: Record<string, string> | string;
|
||||||
price?: string;
|
price?: string;
|
||||||
imageUrl?: string | null;
|
imageUrl?: string | null;
|
||||||
|
focalPoint?: { x: string; y: string };
|
||||||
url?: string;
|
url?: string;
|
||||||
ctaLabel?: Record<string, string> | string;
|
ctaLabel?: Record<string, string> | string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { useState, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Award,
|
|
||||||
Box,
|
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Gift,
|
|
||||||
Leaf,
|
|
||||||
Package,
|
|
||||||
ShoppingBag,
|
|
||||||
Sparkles,
|
|
||||||
Star,
|
Star,
|
||||||
type LucideIcon,
|
Sparkles,
|
||||||
|
Box,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { MegaMenuPayload, MenuColumn } from './menuTypes'
|
import type { MegaMenuPayload, MenuColumn } from './menuTypes'
|
||||||
import { getLocalizedMenuText } 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 {
|
interface DesktopMegaMenuProps {
|
||||||
items: MegaMenuPayload[]
|
items: MegaMenuPayload[]
|
||||||
@@ -25,14 +30,7 @@ interface DesktopMegaMenuProps {
|
|||||||
currentPath: string
|
currentPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const MENU_ICONS: LucideIcon[] = [
|
|
||||||
ShoppingBag,
|
|
||||||
Sparkles,
|
|
||||||
Leaf,
|
|
||||||
Package,
|
|
||||||
Gift,
|
|
||||||
Award,
|
|
||||||
]
|
|
||||||
|
|
||||||
const MAX_TOP_LEVEL_LINKS = 5
|
const MAX_TOP_LEVEL_LINKS = 5
|
||||||
const MAX_INLINE_CHILD_LINKS = 3
|
const MAX_INLINE_CHILD_LINKS = 3
|
||||||
@@ -52,11 +50,13 @@ function MenuImage({
|
|||||||
alt,
|
alt,
|
||||||
placeholder,
|
placeholder,
|
||||||
className = '',
|
className = '',
|
||||||
|
style,
|
||||||
}: {
|
}: {
|
||||||
src?: string | null
|
src?: string | null
|
||||||
alt: string
|
alt: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
className?: string
|
className?: string
|
||||||
|
style?: React.CSSProperties
|
||||||
}) {
|
}) {
|
||||||
const [loaded, setLoaded] = useState(false)
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
|
||||||
@@ -74,6 +74,7 @@ function MenuImage({
|
|||||||
fill
|
fill
|
||||||
sizes="(max-width: 1024px) 0px, 224px"
|
sizes="(max-width: 1024px) 0px, 224px"
|
||||||
className={`object-cover transition-opacity duration-300 ${loaded ? 'opacity-100' : 'opacity-0'} ${className}`}
|
className={`object-cover transition-opacity duration-300 ${loaded ? 'opacity-100' : 'opacity-0'} ${className}`}
|
||||||
|
style={style}
|
||||||
onLoad={() => setLoaded(true)}
|
onLoad={() => setLoaded(true)}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
@@ -120,6 +121,8 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
|
|||||||
const showFeatured = featuredConfig?.enabled !== false
|
const showFeatured = featuredConfig?.enabled !== false
|
||||||
const configuredImage = featuredConfig?.imageUrl || null
|
const configuredImage = featuredConfig?.imageUrl || null
|
||||||
const featuredImage = hoveredLinkImage ?? configuredImage ?? featuredLink?.imageUrl ?? item.rootImageUrl
|
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 featuredHeading = getLocalizedMenuText(featuredConfig?.sectionTitle, lang) || 'Featured'
|
||||||
const featuredBadge = getLocalizedMenuText(featuredConfig?.badge, lang) || 'Recommended'
|
const featuredBadge = getLocalizedMenuText(featuredConfig?.badge, lang) || 'Recommended'
|
||||||
const featuredTitle = getLocalizedMenuText(featuredConfig?.title, lang)
|
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' : ''}`}
|
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">
|
<h4 className="mb-5 flex items-center gap-3 text-xs font-bold normal-case tracking-normal text-text-main">
|
||||||
{(() => {
|
<DynamicIcon
|
||||||
const HeadingIcon = MENU_ICONS[columnIndex] ?? Sparkles
|
iconName={column.icon}
|
||||||
return <HeadingIcon className="h-4 w-4 text-brand-primary" strokeWidth={1.5} />
|
defaultIcon={Sparkles}
|
||||||
})()}
|
className="h-4 w-4 text-brand-primary"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
/>
|
||||||
{getLocalizedMenuText(column.title, lang)}
|
{getLocalizedMenuText(column.title, lang)}
|
||||||
</h4>
|
</h4>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{column.links.slice(0, MAX_TOP_LEVEL_LINKS).map((link, linkIndex) => {
|
{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 hasNestedLinks = !!link.links?.length
|
||||||
const visibleChildLinks = link.links?.slice(0, MAX_INLINE_CHILD_LINKS) ?? []
|
const visibleChildLinks = link.links?.slice(0, MAX_INLINE_CHILD_LINKS) ?? []
|
||||||
const hiddenChildCount = Math.max((link.links?.length ?? 0) - MAX_INLINE_CHILD_LINKS, 0)
|
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"
|
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">
|
<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>
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block text-sm font-bold normal-case tracking-normal text-text-main">
|
<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}
|
src={featuredImage}
|
||||||
alt={featuredTitle}
|
alt={featuredTitle}
|
||||||
placeholder={getLocalizedMenuText(item.label, lang)}
|
placeholder={getLocalizedMenuText(item.label, lang)}
|
||||||
|
className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||||
|
style={{ objectPosition }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 p-4">
|
<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
|
url?: string
|
||||||
links?: MenuLink[]
|
links?: MenuLink[]
|
||||||
imageUrl?: string | null // รูปที่แสดงเมื่อ Hover ลิงก์
|
imageUrl?: string | null // รูปที่แสดงเมื่อ Hover ลิงก์
|
||||||
|
icon?: string // ชื่อ icon จาก registry (lucide)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuColumn {
|
export interface MenuColumn {
|
||||||
title: Record<string, string> | string
|
title: Record<string, string> | string
|
||||||
links: MenuLink[]
|
links: MenuLink[]
|
||||||
iconUrl?: string | null // รูปประจำคอลัมน์ (1:1)
|
iconUrl?: string | null // รูปประจำคอลัมน์ (1:1)
|
||||||
|
icon?: string // ชื่อ icon จาก registry (lucide)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuFeaturedCard {
|
export interface MenuFeaturedCard {
|
||||||
@@ -19,6 +21,7 @@ export interface MenuFeaturedCard {
|
|||||||
subtitle?: Record<string, string> | string
|
subtitle?: Record<string, string> | string
|
||||||
price?: string
|
price?: string
|
||||||
imageUrl?: string | null
|
imageUrl?: string | null
|
||||||
|
focalPoint?: { x: string; y: string }
|
||||||
url?: string
|
url?: string
|
||||||
ctaLabel?: Record<string, string> | string
|
ctaLabel?: Record<string, string> | string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
```
|
||||||
Reference in new issue
Block a user