-
-
updateFeatured('imageUrl', e.target.value || null)}
- />
+
+
+ {onOpenMediaPicker && (
+
+ )}
+
+
+ updateFeatured('imageUrl', e.target.value || null)}
+ />
+ {featured.imageUrl && (
+
+ )}
+
@@ -188,7 +247,7 @@ const FeaturedCardEditorFields = ({ featured, onChange }: any) => {
);
}
-const FeaturedCardSidePanel = ({ node, onChange }: any) => {
+const FeaturedCardSidePanel = ({ node, onChange, onOpenMediaPicker }: any) => {
if (!node) {
return (
@@ -223,12 +282,13 @@ const FeaturedCardSidePanel = ({ node, onChange }: any) => {
onChange({ ...node, featured: newFeatured })}
+ onOpenMediaPicker={onOpenMediaPicker}
/>
);
}
-const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured, isFeaturedSelected }: any) => {
+const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured, isFeaturedSelected, onOpenMediaPicker }: any) => {
const childrenKey = type === 'root' ? 'columns' : 'links';
const labelKey = type === 'column' ? 'title' : 'label';
const hasUrl = type !== 'column';
@@ -300,21 +360,56 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured,
{/* Image URL field */}
{/* Preview thumbnail */}
-
+
onOpenMediaPicker && onOpenMediaPicker((url: string) => onChange({ ...node, [imageFieldKey]: url }))}
+ className="w-14 h-14 shrink-0 rounded-lg overflow-hidden border border-white/10 hover:border-cyan-glow/50 bg-white/5 flex items-center justify-center cursor-pointer group relative transition-colors"
+ title="คลิกเพื่อเลือกรูปจากคลังภาพ (Media Library)"
+ >
{imageValue ? (
-

+ <>
+

+
+
+
+ >
) : (
-
No Image
+
+
+ Library
+
)}
-
-
onChange({ ...node, [imageFieldKey]: e.target.value || null })}
- />
+
+
+ {onOpenMediaPicker && (
+
+ )}
+
+
+ onChange({ ...node, [imageFieldKey]: e.target.value || null })}
+ />
+ {imageValue && (
+
+ )}
+
{type !== 'root' && (
{type === 'root' && (
-
-
-
-
-
+
+ {/* Layout Mode Selector */}
+
+
+
+
+ {node.layout === 'classic'
+ ? 'Classic Dropdown'
+ : node.layout === 'tabbed'
+ ? 'Tabbed Hierarchy'
+ : node.layout === 'visual-cards'
+ ? 'Visual Cards'
+ : `Mega Menu (${node.layoutConfig?.columnCount || 2} Cols)`}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Submenu Layout Modifier / Description */}
+ {(!node.layout || node.layout === 'columns') && (
+
+ {/* Column Count */}
+
+
จำนวนคอลัมน์ (Columns)
+
+ {[2, 3, 4].map((count) => {
+ const currentCols = node.layoutConfig?.columnCount || 2;
+ return (
+
+ );
+ })}
+
+
+
+ {/* Toggle Featured Card */}
+
+ แสดง Featured Card
+
+
-
-
Featured card
-
- {featured.enabled === false ? 'Disabled' : featuredTitle}
-
+ )}
+
+ {node.layout === 'tabbed' && (
+
+ 💡 Tabbed Hierarchy: แต่ละ Column จะทำหน้าที่เป็นแท็บหมวดหมู่ฝั่งซ้าย เมื่อเอาเมาส์ Hover แท็บใด รายการ Links ย่อยจะปรากฏฝั่งขวาทันที เหมาะสำหรับร้านค้าที่มีหมวดหมู่แคตตาล็อกลึก (เช่น อะไหล่ยนต์, เครื่องใช้ไฟฟ้า, ไอที)
+
+ )}
+
+ {node.layout === 'visual-cards' && (
+
+ ✨ Visual Image Cards: แสดงเป็นการ์ดรูปถ่าย 4 ใบแนวนอน โดยดึงรูปภาพจาก Column Icon URL หรือ Hover Image ของลิงก์แรก เหมาะสำหรับสินค้าที่เน้นความงามและดีไซน์ (เช่น ก้านไม้หอม, เทียนหอม, คอลเลกชันแฟชั่น)
+
+ )}
+
+ {node.layout === 'classic' && (
+
+ 📄 Classic Dropdown: แสดงเป็นกล่องแนวตั้งกะทัดรัดใต้ปุ่มเมนูบน Navbar เหมาะสำหรับเมนูข้อมูลทั่วไปที่มี 3-7 ลิงก์ (เช่น เกี่ยวกับเรา, ศูนย์บริการ, ข่าวสาร)
+
+ )}
+
+
+ {/* Featured Card editor trigger (only shown when in columns mode and featured card is enabled) */}
+ {(!node.layout || node.layout === 'columns') && (node.layoutConfig?.showFeatured !== false && featured.enabled !== false) && (
+
+
+
+
+
+
+
+
Featured card
+
+ {featuredTitle}
+
+
+
+
-
-
+ )}
)}
@@ -383,6 +641,7 @@ const TreeNodeEditor = ({ node, type, depth, onChange, onRemove, onEditFeatured,
depth={depth + 1}
onChange={(newChild: any) => handleUpdateChild(i, newChild)}
onRemove={() => handleRemoveChild(i)}
+ onOpenMediaPicker={onOpenMediaPicker}
/>
))}
@@ -406,6 +665,13 @@ export default function MenuEditorPage({ menuName, title, description }: MenuEdi
const [pastedJsonText, setPastedJsonText] = useState('')
const [pasteError, setPasteError] = useState
(null)
const fileInputRef = useRef(null)
+ const [isMediaModalOpen, setIsMediaModalOpen] = useState(false)
+ const [mediaSelectCallback, setMediaSelectCallback] = useState<((url: string) => void) | null>(null)
+
+ const handleOpenMediaPicker = (onSelectUrl: (url: string) => void) => {
+ setMediaSelectCallback(() => onSelectUrl)
+ setIsMediaModalOpen(true)
+ }
useEffect(() => {
Promise.all([
@@ -708,6 +974,7 @@ export default function MenuEditorPage({ menuName, title, description }: MenuEdi
}}
onEditFeatured={() => setSelectedFeaturedIndex(idx)}
isFeaturedSelected={safeFeaturedIndex === idx}
+ onOpenMediaPicker={handleOpenMediaPicker}
/>
))
)}
@@ -829,6 +1096,7 @@ export default function MenuEditorPage({ menuName, title, description }: MenuEdi
) : (
@@ -884,7 +1152,7 @@ export default function MenuEditorPage({ menuName, title, description }: MenuEdi
)}
{/* Paste JSON Modal */}
- {showPasteModal && (
+ {showPasteModal && createPortal(
@@ -932,7 +1200,8 @@ export default function MenuEditorPage({ menuName, title, description }: MenuEdi
-
+
,
+ document.body
)}
{/* LLM Mega Menu Guide Modal */}
@@ -942,6 +1211,23 @@ export default function MenuEditorPage({ menuName, title, description }: MenuEdi
onOpenPasteJson={() => setShowPasteModal(true)}
onOpenImportFile={() => fileInputRef.current?.click()}
/>
+
+ {/* Media Library Modal */}
+ {
+ setIsMediaModalOpen(false)
+ setMediaSelectCallback(null)
+ }}
+ onSelect={(url) => {
+ if (mediaSelectCallback) {
+ mediaSelectCallback(url)
+ }
+ setIsMediaModalOpen(false)
+ setMediaSelectCallback(null)
+ }}
+ mediaType="image"
+ />
)
}
diff --git a/apps/spa/src/components/DesktopMegaMenu.tsx b/apps/spa/src/components/DesktopMegaMenu.tsx
index c8c1af6..39448ba 100644
--- a/apps/spa/src/components/DesktopMegaMenu.tsx
+++ b/apps/spa/src/components/DesktopMegaMenu.tsx
@@ -60,6 +60,7 @@ function MenuImage({ src, alt, placeholder, className, style }: { src?: string |
export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
const [openId, setOpenId] = useState
(null);
const [hoveredLinkImage, setHoveredLinkImage] = useState(null);
+ const [activeTabByMenu, setActiveTabByMenu] = useState>({});
const closeTimer = useRef | null>(null);
const location = useLocation();
const { i18n } = useTranslation();
@@ -110,10 +111,17 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
const isOpen = openId === uniqueId;
const hasColumns = item.columns && item.columns.length > 0;
- const visibleColumns = item.columns?.slice(0, 2) ?? [];
+ const layout = item.layout || 'columns';
+ const isClassic = layout === 'classic';
+ const isTabbed = layout === 'tabbed';
+ const isVisualCards = layout === 'visual-cards';
+ const configuredCols = item.layoutConfig?.columnCount || (item.columns?.length ? Math.min(Math.max(item.columns.length, 2), 4) : 2);
+ const visibleColumns = item.columns?.slice(0, configuredCols) ?? [];
const featuredLink = getFirstImageLink(item.columns);
const featuredConfig = item.featured;
- const showFeatured = featuredConfig?.enabled !== false;
+ const showFeatured = (item.layoutConfig?.showFeatured !== false) && (featuredConfig?.enabled !== false) && !isClassic && !isVisualCards && !isTabbed && (
+ !!(featuredConfig?.imageUrl || featuredConfig?.title || featuredConfig?.sectionTitle || featuredLink?.imageUrl)
+ );
const configuredImage = featuredConfig?.imageUrl || null;
const featuredImage = hoveredLinkImage ?? configuredImage ?? featuredLink?.imageUrl ?? item.rootImageUrl;
const focalPoint = featuredConfig?.focalPoint;
@@ -129,6 +137,28 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
const featuredHref = featuredConfig?.url || featuredLink?.url || item.url || '#';
const featuredCta = getLocalizedText(featuredConfig?.ctaLabel, lang) || 'View details';
+ const getMegaContainerClass = (cols: number, hasFeatured: boolean) => {
+ if (hasFeatured) {
+ if (cols >= 4) return 'w-[min(100vw-2rem,1260px)]';
+ if (cols === 3) return 'w-[min(100vw-2rem,1120px)]';
+ return 'w-[min(100vw-2rem,980px)]';
+ }
+ if (cols >= 4) return 'w-[min(100vw-2rem,1180px)]';
+ if (cols === 3) return 'w-[min(100vw-2rem,960px)]';
+ return 'w-[min(100vw-2rem,720px)]';
+ };
+
+ const getMegaGridColsClass = (cols: number, hasFeatured: boolean) => {
+ if (hasFeatured) {
+ if (cols >= 4) return 'grid-cols-[1fr_1fr_1fr_1fr_260px]';
+ if (cols === 3) return 'grid-cols-[1fr_1fr_1fr_270px]';
+ return 'grid-cols-[1fr_1fr_270px]';
+ }
+ if (cols >= 4) return 'grid-cols-4';
+ if (cols === 3) return 'grid-cols-3';
+ return 'grid-cols-2';
+ };
+
return (
- {hasColumns && (
+ {hasColumns && isClassic && (
handleOpen(uniqueId)}
+ onMouseLeave={handleClose}
+ >
+
+ {item.columns?.map((col, colIdx) => (
+
0 ? 'mt-2 border-t border-pop-black/10 pt-2' : ''}>
+ {getLocalizedText(col.title, lang) && item.columns && item.columns.length > 1 && (
+
+ {getLocalizedText(col.title, lang)}
+
+ )}
+
+ {col.links.map((link, linkIdx) => (
+ -
+
+
+ {link.icon && (
+
+ )}
+ {getLocalizedText(link.label, lang)}
+
+ {link.links?.length ? (
+
+ ) : null}
+
+ {link.links && link.links.length > 0 && (
+
+ {link.links.map((child, childIdx) => (
+ -
+
+ {getLocalizedText(child.label, lang)}
+
+
+ ))}
+
+ )}
+
+ ))}
+
+
+ ))}
+
+
+ )}
+
+ {hasColumns && isTabbed && (
+
handleOpen(uniqueId)}
+ onMouseLeave={handleClose}
+ >
+
+
+
+ {/* Left Category Tabs Sidebar */}
+
+
+ {lang === 'th' ? 'หมวดหมู่หลัก' : 'Categories'}
+
+ {item.columns?.map((column, colIdx) => {
+ const activeIdx = activeTabByMenu[uniqueId] ?? 0;
+ const isSelected = activeIdx === colIdx;
+ return (
+
+ );
+ })}
+
+
+ {/* Right Content Panel for Selected Tab */}
+ {(() => {
+ const activeIdx = activeTabByMenu[uniqueId] ?? 0;
+ const activeCol = item.columns?.[activeIdx] || item.columns?.[0];
+ if (!activeCol) return null;
+
+ return (
+
+
+
+
+
+
+ {getLocalizedText(activeCol.title, lang)}
+
+
+
+
{lang === 'th' ? 'ดูรายการทั้งหมด' : 'View all'}
+
+
+
+
+
+ {activeCol.links.map((link, lIdx) => (
+
setHoveredLinkImage(link.imageUrl || null)}
+ onMouseLeave={() => setHoveredLinkImage(null)}
+ >
+
+
+
+
+
+ {getLocalizedText(link.label, lang)}
+
+
+
+ {link.links && link.links.length > 0 && (
+
+ {link.links.slice(0, 3).map((child, cIdx) => (
+
+ {getLocalizedText(child.label, lang)}
+
+ ))}
+
+ )}
+
+ ))}
+
+
+
+
+
{item.columns?.length || 0} {lang === 'th' ? 'หมวดหมู่ย่อย' : 'categories'}
+
+
{getLocalizedText(item.label, lang)}
+
+
+
+
+ );
+ })()}
+
+
+
+ )}
+
+ {hasColumns && isVisualCards && (
+
handleOpen(uniqueId)}
+ onMouseLeave={handleClose}
+ >
+
+
+
+
+ {getLocalizedText(item.label, lang)}
+
+
+
{lang === 'th' ? 'ดูทั้งหมด' : 'Explore all'}
+
+
+
+
+
+ {item.columns?.slice(0, 4).map((column, colIdx) => {
+ const cardImg = column.iconUrl || column.links?.[0]?.imageUrl || configuredImage || item.rootImageUrl;
+ const cardHref = column.links?.[0]?.url || item.url || '#';
+ const cardTitle = getLocalizedText(column.title, lang);
+ const firstSubLink = column.links?.[0];
+ const subText = firstSubLink ? getLocalizedText(firstSubLink.label, lang) : null;
+
+ return (
+
+
+
+
+
+
+ {cardTitle}
+
+ {subText && (
+
+ {subText}
+
+
+ )}
+
+
+
+ );
+ })}
+
+
+
+ )}
+
+ {hasColumns && !isClassic && !isTabbed && !isVisualCards && (
+
-
+
{visibleColumns.map((col, idx) => (
| string;
}
+export type SubmenuLayoutType = 'columns' | 'classic' | 'tabbed' | 'visual-cards';
+
+export interface SubmenuLayoutConfig {
+ columnCount?: 2 | 3 | 4;
+ showFeatured?: boolean;
+ itemStyle?: 'icon' | 'thumbnail' | 'text-only';
+}
+
export interface MegaMenuPayload {
id?: string;
nodeId?: string;
label: Record
| string;
url?: string;
+ layout?: SubmenuLayoutType;
+ layoutConfig?: SubmenuLayoutConfig;
columns?: MenuColumn[];
rootImageUrl?: string | null; // รูปหลักฝั่งซ้ายสุด (1:1)
featured?: MenuFeaturedCard;
diff --git a/apps/website/src/components/navigation/DesktopMegaMenu.tsx b/apps/website/src/components/navigation/DesktopMegaMenu.tsx
index 2049ee5..11c9ac1 100644
--- a/apps/website/src/components/navigation/DesktopMegaMenu.tsx
+++ b/apps/website/src/components/navigation/DesktopMegaMenu.tsx
@@ -84,6 +84,7 @@ function MenuImage({
export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuProps) {
const [openId, setOpenId] = useState(null)
const [hoveredLinkImage, setHoveredLinkImage] = useState(null)
+ const [activeTabByMenu, setActiveTabByMenu] = useState>({})
const closeTimer = useRef | null>(null)
// Automatically close mega menu when route changes
@@ -128,10 +129,17 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
const hasColumns = !!item.columns?.length
const isActive = item.url ? currentPath === item.url : false
- const visibleColumns = item.columns?.slice(0, 2) ?? []
+ const layout = item.layout || 'columns'
+ const isClassic = layout === 'classic'
+ const isTabbed = layout === 'tabbed'
+ const isVisualCards = layout === 'visual-cards'
+ const configuredCols = item.layoutConfig?.columnCount || (item.columns?.length ? Math.min(Math.max(item.columns.length, 2), 4) : 2)
+ const visibleColumns = item.columns?.slice(0, configuredCols) ?? []
const featuredLink = getFirstImageLink(item.columns)
const featuredConfig = item.featured
- const showFeatured = featuredConfig?.enabled !== false
+ const showFeatured = (item.layoutConfig?.showFeatured !== false) && (featuredConfig?.enabled !== false) && !isClassic && !isVisualCards && !isTabbed && (
+ !!(featuredConfig?.imageUrl || featuredConfig?.title || featuredConfig?.sectionTitle || featuredLink?.imageUrl)
+ )
const configuredImage = featuredConfig?.imageUrl || null
const featuredImage = hoveredLinkImage ?? configuredImage ?? featuredLink?.imageUrl ?? item.rootImageUrl
const focalPoint = featuredConfig?.focalPoint
@@ -147,6 +155,28 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
const featuredHref = featuredConfig?.url || featuredLink?.url || item.url || '#'
const featuredCta = getLocalizedMenuText(featuredConfig?.ctaLabel, lang) || 'View details'
+ const getMegaContainerClass = (cols: number, hasFeatured: boolean) => {
+ if (hasFeatured) {
+ if (cols >= 4) return 'w-[min(100vw-2rem,1260px)]'
+ if (cols === 3) return 'w-[min(100vw-2rem,1120px)]'
+ return 'w-[min(100vw-2rem,980px)]'
+ }
+ if (cols >= 4) return 'w-[min(100vw-2rem,1180px)]'
+ if (cols === 3) return 'w-[min(100vw-2rem,960px)]'
+ return 'w-[min(100vw-2rem,720px)]'
+ }
+
+ const getMegaGridColsClass = (cols: number, hasFeatured: boolean) => {
+ if (hasFeatured) {
+ if (cols >= 4) return 'grid-cols-[1fr_1fr_1fr_1fr_260px]'
+ if (cols === 3) return 'grid-cols-[1fr_1fr_1fr_270px]'
+ return 'grid-cols-[1fr_1fr_270px]'
+ }
+ if (cols >= 4) return 'grid-cols-4'
+ if (cols === 3) return 'grid-cols-3'
+ return 'grid-cols-2'
+ }
+
return (
- {hasColumns && (
+ {hasColumns && isClassic && (
handleOpen(uniqueId)}
+ onMouseLeave={handleClose}
+ >
+
+ {item.columns?.map((column, columnIndex) => (
+
0 ? 'mt-2 border-t border-line-primary/10 pt-2' : ''}>
+ {getLocalizedMenuText(column.title, lang) && item.columns && item.columns.length > 1 && (
+
+ {getLocalizedMenuText(column.title, lang)}
+
+ )}
+
+ {column.links.map((link, linkIndex) => (
+ -
+
+
+ {link.icon && (
+
+ )}
+ {getLocalizedMenuText(link.label, lang)}
+
+ {link.links?.length ? (
+
+ ) : null}
+
+ {link.links && link.links.length > 0 && (
+
+ {link.links.map((child, childIdx) => (
+ -
+
+ {getLocalizedMenuText(child.label, lang)}
+
+
+ ))}
+
+ )}
+
+ ))}
+
+
+ ))}
+
+
+ )}
+
+ {hasColumns && isTabbed && (
+
handleOpen(uniqueId)}
+ onMouseLeave={handleClose}
+ >
+
+
+
+ {/* Left Category Tabs Sidebar */}
+
+
+ {lang === 'th' ? 'หมวดหมู่หลัก' : 'Categories'}
+
+ {item.columns?.map((column, colIdx) => {
+ const activeIdx = activeTabByMenu[uniqueId] ?? 0
+ const isSelected = activeIdx === colIdx
+ return (
+
+ )
+ })}
+
+
+ {/* Right Content Panel for Selected Tab */}
+ {(() => {
+ const activeIdx = activeTabByMenu[uniqueId] ?? 0
+ const activeCol = item.columns?.[activeIdx] || item.columns?.[0]
+ if (!activeCol) return null
+
+ return (
+
+
+
+
+
+
+ {getLocalizedMenuText(activeCol.title, lang)}
+
+
+
+
{lang === 'th' ? 'ดูรายการทั้งหมด' : 'View all'}
+
+
+
+
+
+ {activeCol.links.map((link, lIdx) => (
+
setHoveredLinkImage(link.imageUrl || null)}
+ onMouseLeave={() => setHoveredLinkImage(null)}
+ >
+
+
+
+
+
+ {getLocalizedMenuText(link.label, lang)}
+
+
+
+ {link.links && link.links.length > 0 && (
+
+ {link.links.slice(0, 3).map((child, cIdx) => (
+
+ {getLocalizedMenuText(child.label, lang)}
+
+ ))}
+
+ )}
+
+ ))}
+
+
+
+
+
{item.columns?.length || 0} {lang === 'th' ? 'หมวดหมู่ย่อย' : 'categories'}
+
+
{getLocalizedMenuText(item.label, lang)}
+
+
+
+
+ )
+ })()}
+
+
+
+ )}
+
+ {hasColumns && isVisualCards && (
+
handleOpen(uniqueId)}
+ onMouseLeave={handleClose}
+ >
+
+
+
+
+ {getLocalizedMenuText(item.label, lang)}
+
+
+
{lang === 'th' ? 'ดูทั้งหมด' : 'Explore all'}
+
+
+
+
+
+ {item.columns?.slice(0, 4).map((column, colIdx) => {
+ const cardImg = column.iconUrl || column.links?.[0]?.imageUrl || configuredImage || item.rootImageUrl
+ const cardHref = column.links?.[0]?.url || item.url || '#'
+ const cardTitle = getLocalizedMenuText(column.title, lang)
+ const firstSubLink = column.links?.[0]
+ const subText = firstSubLink ? getLocalizedMenuText(firstSubLink.label, lang) : null
+
+ return (
+
+
+
+
+
+
+ {cardTitle}
+
+ {subText && (
+
+ {subText}
+
+
+ )}
+
+
+
+ )
+ })}
+
+
+
+ )}
+
+ {hasColumns && !isClassic && !isTabbed && !isVisualCards && (
+
-
+
{visibleColumns.map((column, columnIndex) => (
| string
}
+export type SubmenuLayoutType = 'columns' | 'classic' | 'tabbed' | 'visual-cards'
+
+export interface SubmenuLayoutConfig {
+ columnCount?: 2 | 3 | 4
+ showFeatured?: boolean
+ itemStyle?: 'icon' | 'thumbnail' | 'text-only'
+}
+
export interface MegaMenuPayload {
id?: string
nodeId?: string
label: Record | string
url?: string
+ layout?: SubmenuLayoutType
+ layoutConfig?: SubmenuLayoutConfig
columns?: MenuColumn[]
rootImageUrl?: string | null // รูปหลักฝั่งซ้ายสุด (1:1)
featured?: MenuFeaturedCard