From 752451b1d4f5fd91b513265b0b46ec24122d25af Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 14 Jul 2026 04:24:20 -0400 Subject: [PATCH] x --- apps/spa/src/components/DesktopMegaMenu.tsx | 258 +++++++++++++++----- apps/spa/src/store/menuStore.ts | 19 +- 2 files changed, 210 insertions(+), 67 deletions(-) diff --git a/apps/spa/src/components/DesktopMegaMenu.tsx b/apps/spa/src/components/DesktopMegaMenu.tsx index 638527e..7df2b67 100644 --- a/apps/spa/src/components/DesktopMegaMenu.tsx +++ b/apps/spa/src/components/DesktopMegaMenu.tsx @@ -1,7 +1,20 @@ import { useState, useRef } from 'react'; import { Link } from 'react-router-dom'; -import type { MegaMenuPayload } from '../store/menuStore'; +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, +} from 'lucide-react'; interface DesktopMegaMenuProps { items: MegaMenuPayload[]; @@ -10,7 +23,28 @@ interface DesktopMegaMenuProps { const getLocalizedText = (textObj: Record | string | undefined, lang: string) => { if (typeof textObj === 'string') return textObj; if (!textObj) return ''; - return textObj[lang] || textObj['en'] || 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_INLINE_CHILD_LINKS = 3; + +const getMoreLabel = (count: number, lang: string) => { + if (lang === 'th') return `+ อีก ${count} รายการ`; + if (lang === 'ja') return `+ 他${count}件`; + if (lang === 'zh') return `+ 还有 ${count} 项`; + if (lang === 'vi') return `+ ${count} mục khác`; + if (lang === 'ko') return `+ ${count}개 더`; + return `+ ${count} more`; }; // Image block shared between Root and Hover areas @@ -50,17 +84,37 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) { }, 150); }; + const getFirstImageLink = (columns: MenuColumn[] = []) => { + for (const column of columns) { + const found = column.links.find((link) => link.imageUrl); + if (found) return found; + } + return columns[0]?.links[0]; + }; + return (
- {items.map((item) => { - const uniqueId = item.nodeId || item.id || `menu-${item.url}`; + {items.map((item, index) => { + const uniqueId = item.nodeId || item.id || `menu-${item.url || index}`; const isOpen = openId === uniqueId; const hasColumns = item.columns && item.columns.length > 0; - // Determine if any column has an icon OR any link has an image - const hasAnyColumnIcon = item.columns?.some(col => !!col.iconUrl); - const hasAnyLinkImage = item.columns?.some(col => col.links.some(l => !!l.imageUrl)); - const showRightPanel = item.rootImageUrl || hasAnyLinkImage; + const visibleColumns = item.columns?.slice(0, 2) ?? []; + const featuredLink = getFirstImageLink(item.columns); + const featuredConfig = item.featured; + const showFeatured = featuredConfig?.enabled !== false; + const configuredImage = featuredConfig?.imageUrl || null; + const featuredImage = hoveredLinkImage ?? configuredImage ?? featuredLink?.imageUrl ?? item.rootImageUrl; + const featuredHeading = getLocalizedText(featuredConfig?.sectionTitle, lang) || 'Featured'; + const featuredBadge = getLocalizedText(featuredConfig?.badge, lang) || 'Recommended'; + const featuredTitle = getLocalizedText(featuredConfig?.title, lang) + || getLocalizedText(featuredLink?.label, lang) + || getLocalizedText(item.label, lang); + const featuredSubtitle = getLocalizedText(featuredConfig?.subtitle, lang) || (item.columns?.[0] + ? getLocalizedText(item.columns[0].title, lang) + : getLocalizedText(item.label, lang)); + const featuredHref = featuredConfig?.url || featuredLink?.url || item.url || '#'; + const featuredCta = getLocalizedText(featuredConfig?.ctaLabel, lang) || 'View details'; return (
- {/* Dropdown panel — fixed, full-width */} {hasColumns && (
handleOpen(uniqueId)} onMouseLeave={handleClose} > -
-
+
+
+
+
+
+ {visibleColumns.map((col, idx) => ( +
0 ? 'border-l border-pop-black/10 pl-8' : ''}`} + > +

+ {(() => { + const HeadingIcon = MENU_ICONS[idx] ?? Sparkles; + return ; + })()} + {getLocalizedText(col.title, lang)} +

+
    + {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); + const moreChildHref = link.url || link.links?.[MAX_INLINE_CHILD_LINKS]?.url || link.links?.[0]?.url || '#'; - {/* LEFT: Root menu image (1:1) — hidden on smaller screens */} - {item.rootImageUrl && ( -
    - -
    - )} - - {/* CENTER: Columns */} -
    - {item.columns!.map((col, idx) => ( -
    - - {/* Column icon (1:1) — shown only if this column has an iconUrl */} - {(hasAnyColumnIcon) && ( -
    - -
    - )} - - {/* Title and links */} -
    -

    - {getLocalizedText(col.title, lang)} -

    -
      - {col.links.map((link, lIdx) => ( -
    • + return ( +
    • +
      setHoveredLinkImage(link.imageUrl || null)} + onMouseLeave={() => setHoveredLinkImage(null)} + > setHoveredLinkImage(link.imageUrl || null)} - onMouseLeave={() => setHoveredLinkImage(null)} + to={link.url || link.links?.[0]?.url || '#'} + className="group flex items-center gap-4 rounded-md px-1 py-2.5 text-left transition-colors hover:bg-pop-black/5" > - {getLocalizedText(link.label, lang)} + + + + + + {getLocalizedText(link.label, lang)} + + + {getLocalizedText(col.title, lang)} + + + -
    • - ))} -
    -
    -
    - ))} -
    - {/* RIGHT: Hover preview image (1:1) — shown only if any link has an imageUrl */} - {showRightPanel && ( -
    - + {hasNestedLinks && ( +
      + {visibleChildLinks.map((childLink, childIndex) => ( +
    • + setHoveredLinkImage(childLink.imageUrl || link.imageUrl || null)} + > + {getLocalizedText(childLink.label, lang)} + +
    • + ))} + {hiddenChildCount > 0 && ( +
    • + + {getMoreLabel(hiddenChildCount, lang)} + +
    • + )} +
    + )} +
    + + ); + })} +
+ + {getLocalizedText(item.label, lang)} + + +
+ ))} + + {showFeatured && ( +
+

+ + {featuredHeading} +

+ setHoveredLinkImage(configuredImage ?? featuredLink?.imageUrl ?? null)} + onMouseLeave={() => setHoveredLinkImage(null)} + > +
+ +
+
+ {featuredBadge && ( + + {featuredBadge} + + )} +
+

+ {featuredTitle} +

+ {featuredSubtitle && ( +

+ {featuredSubtitle} +

+ )} + {featuredConfig?.price && ( +

+ {featuredConfig.price} +

+ )} +
+ + {featuredCta} + +
+
)} -
diff --git a/apps/spa/src/store/menuStore.ts b/apps/spa/src/store/menuStore.ts index 1645994..95ad752 100644 --- a/apps/spa/src/store/menuStore.ts +++ b/apps/spa/src/store/menuStore.ts @@ -2,25 +2,38 @@ import { create } from 'zustand'; import { API_BASE } from '@homaroma/types'; export interface MenuLink { - label: Record; + label: Record | string; url?: string; links?: MenuLink[]; imageUrl?: string | null; // รูปที่แสดงเมื่อ Hover ลิงก์ } export interface MenuColumn { - title: Record; + title: Record | string; links: MenuLink[]; iconUrl?: string | null; // รูปประจำคอลัมน์ (1:1) } +export interface MenuFeaturedCard { + enabled?: boolean; + sectionTitle?: Record | string; + badge?: Record | string; + title?: Record | string; + subtitle?: Record | string; + price?: string; + imageUrl?: string | null; + url?: string; + ctaLabel?: Record | string; +} + export interface MegaMenuPayload { id?: string; nodeId?: string; - label: Record; + label: Record | string; url?: string; columns?: MenuColumn[]; rootImageUrl?: string | null; // รูปหลักฝั่งซ้ายสุด (1:1) + featured?: MenuFeaturedCard; } interface MenuState {