diff --git a/apps/admin/src/components/MediaLibraryModal.tsx b/apps/admin/src/components/MediaLibraryModal.tsx index 2e3dab0..61dba85 100644 --- a/apps/admin/src/components/MediaLibraryModal.tsx +++ b/apps/admin/src/components/MediaLibraryModal.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { API_BASE } from '@homaroma/types'; import { X, Upload, Loader2, Image as ImageIcon, Video } from 'lucide-react'; @@ -105,7 +106,7 @@ export default function MediaLibraryModal({ isOpen, onClose, onSelect, mediaType if (!isOpen) return null; - return ( + return createPortal(
{/* Header */} @@ -196,6 +197,7 @@ export default function MediaLibraryModal({ isOpen, onClose, onSelect, mediaType )}
- + , + document.body ); } diff --git a/apps/admin/src/components/MenuJsonGuideModal.tsx b/apps/admin/src/components/MenuJsonGuideModal.tsx index 14b59e6..0d9dd08 100644 --- a/apps/admin/src/components/MenuJsonGuideModal.tsx +++ b/apps/admin/src/components/MenuJsonGuideModal.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { createPortal } from 'react-dom' import { Sparkles, Copy, @@ -35,6 +36,8 @@ const GUIDE_OVERVIEW_MARKDOWN = `# คู่มือการใช้ AI/LLM ### 1. Root Items (เมนูแถวบนสุด - Level 1) - **label**: ออบเจกต์ข้อความรองรับ 8 ภาษา \`{ th, en, ja, zh, vi, ko, lo, it }\` - **url**: ลิงก์ปลายทาง เช่น \`/category/reed-diffuser\` หรือ \`/collections/all\` +- **layout**: (ไม่บังคับ) \`'columns'\` (Mega Menu คอลัมน์) | \`'tabbed'\` (Tabbed Catalog แท็บซ้าย) | \`'visual-cards'\` (การ์ดรูปภาพหรูหรา 4 ใบ) | \`'classic'\` (Dropdown แนวตั้งเรียบหรู กะทัดรัด) +- **layoutConfig**: (ไม่บังคับ) ออบเจกต์ตั้งค่าเพิ่มเติม \`{ columnCount: 2 | 3 | 4, showFeatured: boolean }\` - **rootImageUrl**: URL รูปภาพหลักฝั่งซ้ายสุดของเมกะเมนู (อัตราส่วน 1:1 จัตุรัส ขนาดแนะนำ 800x800) - **columns**: อาร์เรย์ของคอลัมน์เมนูย่อย (Level 2) - **featured**: การ์ดสินค้า/ไฮไลต์โปรโมตพิเศษที่แสดงฝั่งขวาสุดของเมกะเมนู @@ -304,7 +307,7 @@ export default function MenuJsonGuideModal({ } } - return ( + return createPortal(
{/* Header */} @@ -491,6 +494,7 @@ export default function MenuJsonGuideModal({
- + , + document.body ) } diff --git a/apps/admin/src/components/ProductJsonGuideModal.tsx b/apps/admin/src/components/ProductJsonGuideModal.tsx index 0060339..2d33073 100644 --- a/apps/admin/src/components/ProductJsonGuideModal.tsx +++ b/apps/admin/src/components/ProductJsonGuideModal.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { createPortal } from 'react-dom' import { Sparkles, Copy, @@ -613,7 +614,7 @@ export default function ProductJsonGuideModal({ isOpen, onClose, onOpenImport }: } } - return ( + return createPortal(
@@ -792,6 +793,7 @@ export default function ProductJsonGuideModal({ isOpen, onClose, onOpenImport }:
- + , + document.body ) } diff --git a/apps/admin/src/components/page-builder/AddSectionModal.tsx b/apps/admin/src/components/page-builder/AddSectionModal.tsx index d4f6575..d554978 100644 --- a/apps/admin/src/components/page-builder/AddSectionModal.tsx +++ b/apps/admin/src/components/page-builder/AddSectionModal.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { X, @@ -852,7 +853,7 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS return matchesCategory && matchesSearch; }); - return ( + return createPortal(
{/* Modal Header */} @@ -978,6 +979,7 @@ export default function AddSectionModal({ isOpen, onClose, onSelectBlock }: AddS )}
- + , + document.body ); } diff --git a/apps/admin/src/components/page-builder/ProductPickerModal.tsx b/apps/admin/src/components/page-builder/ProductPickerModal.tsx index f78247e..16a6070 100644 --- a/apps/admin/src/components/page-builder/ProductPickerModal.tsx +++ b/apps/admin/src/components/page-builder/ProductPickerModal.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { X, Search, Loader2 } from 'lucide-react'; import { API_BASE } from '@homaroma/types'; import { getAdminDisplayString } from '../../utils/localization'; @@ -63,7 +64,7 @@ export default function ProductPickerModal({ onClose(); }; - return ( + return createPortal(
@@ -160,6 +161,7 @@ export default function ProductPickerModal({
- + , + document.body ); } diff --git a/apps/admin/src/index.css b/apps/admin/src/index.css index 4364460..132b3ca 100644 --- a/apps/admin/src/index.css +++ b/apps/admin/src/index.css @@ -116,18 +116,18 @@ /* Animations */ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } + to { opacity: 1; transform: none; } } @keyframes zoomIn { from { opacity: 0; transform: scale(0.95); } - to { opacity: 1; transform: scale(1); } + to { opacity: 1; transform: none; } } .animate-fade-in { - animation: fadeIn 0.4s ease-out forwards; + animation: fadeIn 0.4s ease-out; } .animate-zoom-in { - animation: zoomIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; + animation: zoomIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } diff --git a/apps/admin/src/pages/menu-manager/MenuEditorPage.tsx b/apps/admin/src/pages/menu-manager/MenuEditorPage.tsx index 5960f62..2748454 100644 --- a/apps/admin/src/pages/menu-manager/MenuEditorPage.tsx +++ b/apps/admin/src/pages/menu-manager/MenuEditorPage.tsx @@ -1,9 +1,11 @@ import { useState, useEffect, useRef } from 'react' +import { createPortal } from 'react-dom' import { API_BASE, type Menu } from '@homaroma/types' -import { Save, Loader2, Plus, Trash2, ChevronUp, ChevronDown, Copy, Check, Download, Upload, Star, Pencil, Sparkles, Code2, X } from 'lucide-react' +import { Save, Loader2, Plus, Trash2, ChevronUp, ChevronDown, Copy, Check, Download, Upload, Star, Pencil, Sparkles, Code2, X, LayoutGrid, ListFilter, Layers, Image as ImageIcon } from 'lucide-react' import { ICON_REGISTRY } from '../../utils/iconRegistry' import FocalPointPicker from '../../components/page-builder/FocalPointPicker' import MenuJsonGuideModal from '../../components/MenuJsonGuideModal' +import MediaLibraryModal from '../../components/MediaLibraryModal' const IconSelect = ({ value, onChange }: { value?: string, onChange: (val: string) => void }) => { return ( @@ -30,7 +32,7 @@ type MenuEditorPageProps = { description: string; } -const FeaturedCardEditorFields = ({ featured, onChange }: any) => { +const FeaturedCardEditorFields = ({ featured, onChange, onOpenMediaPicker }: any) => { const [activeTab, setActiveTab] = useState<'content' | 'media'>('content'); const [activeLang, setActiveLang] = useState('th'); @@ -54,8 +56,28 @@ const FeaturedCardEditorFields = ({ featured, onChange }: any) => { ['ctaLabel', 'CTA label', 'View details'], ]; + const isEnabled = featured.enabled !== false; + return (
+
+
+
Enable Featured Card
+
เปิดหรือปิดการแสดงการ์ดโปรโมตใน Mega Menu
+
+ +
+
{[ ['content', 'Content'], @@ -136,22 +158,59 @@ const FeaturedCardEditorFields = ({ featured, onChange }: any) => {
-
+
onOpenMediaPicker && onOpenMediaPicker((url: string) => updateFeatured('imageUrl', url))} + className="aspect-[4/3] relative overflow-hidden rounded-xl border-2 border-dashed border-white/10 hover:border-cyan-glow/50 bg-night-950/40 flex items-center justify-center cursor-pointer group transition-all" + title="คลิกเพื่อเลือกรูปจากคลังภาพ (Media Library)" + > {featured.imageUrl ? ( - Featured preview + <> + Featured preview +
+ + เปลี่ยนรูปจากคลังภาพ +
+ ) : ( - No featured image +
+ + เลือกรูปจากคลังภาพ (Images Lib) + คลิกที่นี่เพื่อเปิดคลังสื่อของระบบ +
)}
- - updateFeatured('imageUrl', e.target.value || null)} - /> +
+ + {onOpenMediaPicker && ( + + )} +
+
+ updateFeatured('imageUrl', e.target.value || null)} + /> + {featured.imageUrl && ( + + )} +
{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 ? ( - preview + <> + preview +
+ +
+ ) : ( - 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