diff --git a/apps/admin/src/pages/MenuManagerPage.tsx b/apps/admin/src/pages/MenuManagerPage.tsx index 3a08a69..9666022 100644 --- a/apps/admin/src/pages/MenuManagerPage.tsx +++ b/apps/admin/src/pages/MenuManagerPage.tsx @@ -1,26 +1,123 @@ import { useState, useEffect } from 'react' import { API_BASE, type Menu } from '@homaroma/types' -import { Save, Loader2, Plus, Trash2, GripVertical } from 'lucide-react' +import { Save, Loader2, Plus, Trash2, GripVertical, Copy, Check } from 'lucide-react' + +const TreeNodeEditor = ({ node, type, depth, onChange, onRemove }: any) => { + const childrenKey = type === 'root' ? 'columns' : 'links'; + const labelKey = type === 'column' ? 'title' : 'label'; + const hasUrl = type !== 'column'; + + const children = node[childrenKey] || []; + + const handleAddChild = () => { + const newChildType = type === 'root' ? 'column' : 'link'; + const newChild = newChildType === 'column' + ? { title: { en: 'New Column' }, links: [] } + : { label: { en: 'New Link' }, url: '/', links: [] }; + onChange({ ...node, [childrenKey]: [...children, newChild] }); + }; + + const handleUpdateChild = (idx: number, newChild: any) => { + const newChildren = [...children]; + newChildren[idx] = newChild; + onChange({ ...node, [childrenKey]: newChildren }); + }; + + const handleRemoveChild = (idx: number) => { + onChange({ ...node, [childrenKey]: children.filter((_: any, i: number) => i !== idx) }); + }; + + const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko']; + const nodeLabel = node[labelKey] || {}; + + return ( +
0 ? 'ml-6 border-l-cyan-500/30' : ''}`}> +
+
+
+ +
+ {languages.map(lang => ( +
+ {lang} + onChange({ ...node, [labelKey]: { ...nodeLabel, [lang]: e.target.value } })} + /> +
+ ))} +
+
+
+ {hasUrl && ( +
+ + onChange({ ...node, url: e.target.value })} + /> +
+ )} +
+ + +
+
+ + {children.length > 0 && ( +
+ {children.map((child: any, i: number) => ( + handleUpdateChild(i, newChild)} + onRemove={() => handleRemoveChild(i)} + /> + ))} +
+ )} +
+ ); +} + export default function MenuManagerPage() { const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [menus, setMenus] = useState([]) + const [pages, setPages] = useState([]) + const [copiedSlug, setCopiedSlug] = useState(null) // We'll manage multiple menus, e.g., 'main' and 'footer-explore' const [selectedMenuName, setSelectedMenuName] = useState('main') useEffect(() => { - fetch(`${API_BASE}/menus`) - .then(r => r.json()) - .then((data: Menu[]) => { - setMenus(data) + Promise.all([ + fetch(`${API_BASE}/menus`).then(r => r.json()), + fetch(`${API_BASE}/pages`).then(r => r.json()) + ]) + .then(([menusData, pagesData]) => { + setMenus(menusData) + setPages(pagesData) }) .catch(console.error) .finally(() => setLoading(false)) }, []) - const currentMenu = menus.find(m => m.name === selectedMenuName) || { id: 0, name: selectedMenuName, items: [] } + const currentMenu = menus.find(m => m.name === selectedMenuName) || { id: 0, name: selectedMenuName, items: [], payload: [] } + + const handleUpdatePayload = (newPayload: any[]) => { + setMenus(menus.map(m => m.name === selectedMenuName ? { ...m, payload: newPayload } : m)) + } const handleAddItem = () => { const newItems = [...currentMenu.items, { id: Date.now(), label: 'New Link', url: '/', order: currentMenu.items.length }] @@ -47,10 +144,14 @@ export default function MenuManagerPage() { setSaving(true) try { + const body = selectedMenuName === 'main' + ? { items: currentMenu.items || [], payload: currentMenu.payload || [] } + : { items: currentMenu.items || [] } + const res = await fetch(`${API_BASE}/menus/${selectedMenuName}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ items: currentMenu.items }) + body: JSON.stringify(body) }) if (res.ok) { alert('Menu saved successfully!') @@ -74,8 +175,9 @@ export default function MenuManagerPage() { } return ( -
-
+
+
+

Menu Manager

Manage navigation links for the main navbar and footer.

@@ -100,15 +202,56 @@ export default function MenuManagerPage() {

Menu Items ({selectedMenuName})

- + {selectedMenuName !== 'main' && ( + + )}
+ {selectedMenuName === 'main' ? ( +
+
+

Mega Menu payload builder

+ +
+ + {(currentMenu.payload || []).length === 0 ? ( +

No items in payload.

+ ) : ( + (currentMenu.payload || []).map((node: any, idx: number) => ( + { + const p = [...(currentMenu.payload || [])]; + p[idx] = newNode; + handleUpdatePayload(p); + }} + onRemove={() => { + const p = (currentMenu.payload || []).filter((_: any, i: number) => i !== idx); + handleUpdatePayload(p); + }} + /> + )) + )} +
+ ) : (
{currentMenu.items.length === 0 ? (

No items in this menu.

@@ -185,6 +328,7 @@ export default function MenuManagerPage() { )) )}
+ )}
+ + {/* Sidebar for Dynamic Pages */} +
+
+
+

+ Dynamic Pages +

+

Click to copy URL path

+
+ +
+ {pages.length === 0 ? ( +

No dynamic pages found.

+ ) : ( + pages.map((page) => { + const titleStr = typeof page.title === 'string' ? page.title : (page.title?.en || 'Untitled') + return ( + + ) + }) + )} +
+
+
+ + {/* Toast Notification */} + {copiedSlug && ( +
+ + Copied URL: /{copiedSlug} +
+ )}
) } diff --git a/apps/backend/prisma/migrations/20260615221753_add_menu_payload/migration.sql b/apps/backend/prisma/migrations/20260615221753_add_menu_payload/migration.sql new file mode 100644 index 0000000..743d912 --- /dev/null +++ b/apps/backend/prisma/migrations/20260615221753_add_menu_payload/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Menu" ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true, +ADD COLUMN "payload" JSONB; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index d5891a5..17082f9 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -589,6 +589,8 @@ model NewsletterSubscription { model Menu { id Int @id @default(autoincrement()) name String @unique // e.g., "header", "footer" + payload Json? + isActive Boolean @default(true) items MenuItem[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/apps/backend/src/routes/menus.ts b/apps/backend/src/routes/menus.ts index 406939c..1be4b23 100644 --- a/apps/backend/src/routes/menus.ts +++ b/apps/backend/src/routes/menus.ts @@ -24,7 +24,7 @@ export const menusRoute = new Elysia({ prefix: "/menus" }) } }); if (!menu) { - return { name, items: [] }; + return { name, items: [], payload: null, isActive: false }; } return menu; }) @@ -59,11 +59,21 @@ export const menusRoute = new Elysia({ prefix: "/menus" }) }) .put("/:name", async ({ params: { name }, body, set }) => { try { - const { items } = body as { items: { id?: number; label: string; url: string; order: number; translations?: { langCode: string; label: string }[] }[] }; + const { items, payload, isActive } = body as { + payload?: any; + isActive?: boolean; + items: { id?: number; label: string; url: string; order: number; translations?: { langCode: string; label: string }[] }[] + }; let menu = await prisma.menu.findUnique({ where: { name } }); + const updateData: any = {}; + if (payload !== undefined) updateData.payload = payload; + if (isActive !== undefined) updateData.isActive = isActive; + if (!menu) { - menu = await prisma.menu.create({ data: { name } }); + menu = await prisma.menu.create({ data: { name, ...updateData } }); + } else if (Object.keys(updateData).length > 0) { + menu = await prisma.menu.update({ where: { name }, data: updateData }); } await prisma.$transaction(async (tx: any) => { @@ -121,6 +131,8 @@ export const menusRoute = new Elysia({ prefix: "/menus" }) } }, { body: t.Object({ + payload: t.Optional(t.Any()), + isActive: t.Optional(t.Boolean()), items: t.Array(t.Object({ id: t.Optional(t.Number()), label: t.String(), diff --git a/apps/spa/src/components/DesktopMegaMenu.tsx b/apps/spa/src/components/DesktopMegaMenu.tsx new file mode 100644 index 0000000..1c983f5 --- /dev/null +++ b/apps/spa/src/components/DesktopMegaMenu.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import type { MegaMenuPayload } from '../store/menuStore'; +import { useTranslation } from 'react-i18next'; + +interface DesktopMegaMenuProps { + items: MegaMenuPayload[]; +} + +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] || ''; +}; + +export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) { + const [openId, setOpenId] = useState(null); + const { i18n } = useTranslation(); + const lang = i18n.language ? i18n.language.substring(0, 2) : 'en'; + + return ( +
+ {items.map((item) => ( +
setOpenId(item.id)} + onMouseLeave={() => setOpenId(null)} + > + + {getLocalizedText(item.label, lang)} + {item.columns && item.columns.length > 0 && ( + + + + )} + + + {item.columns && item.columns.length > 0 && openId === item.id && ( +
+
+ {item.columns.map((col, idx) => ( +
+

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

+
    + {col.links.map((link, lIdx) => ( +
  • + + {getLocalizedText(link.label, lang)} + +
  • + ))} +
+
+ ))} +
+
+ )} +
+ ))} +
+ ); +} diff --git a/apps/spa/src/components/MobileDrawer.tsx b/apps/spa/src/components/MobileDrawer.tsx new file mode 100644 index 0000000..8ad6ec9 --- /dev/null +++ b/apps/spa/src/components/MobileDrawer.tsx @@ -0,0 +1,254 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import type { MegaMenuPayload, MenuColumn, MenuLink } from '../store/menuStore'; +import { X, ChevronRight, ChevronLeft, Globe } from 'lucide-react'; +import { useAuthStore } from '../store/authStore'; +import { loginLiff, logoutLiff } from '../lib/liff'; +import { useTranslation } from 'react-i18next'; + +interface MobileDrawerProps { + isOpen: boolean; + onClose: () => void; + items: MegaMenuPayload[]; + onOpenLang: () => void; +} + +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] || ''; +}; + +type MenuNode = + | { type: 'root'; data: MegaMenuPayload[]; title: string | Record } + | { type: 'mega'; data: MenuColumn[]; title: string | Record } + | { type: 'links'; data: MenuLink[]; title: string | Record }; + +export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawerProps) { + const { customer } = useAuthStore(); + const navigate = useNavigate(); + const { i18n } = useTranslation(); + const lang = i18n.language ? i18n.language.substring(0, 2) : 'en'; + + const [stack, setStack] = useState([{ type: 'root', data: items, title: 'Menu' }]); + + useEffect(() => { + if (!isOpen) { + setTimeout(() => { + setStack([{ type: 'root', data: items, title: 'Menu' }]); + }, 300); + } else { + if (stack.length === 1 && stack[0].data !== items) { + setStack([{ type: 'root', data: items, title: 'Menu' }]); + } + } + }, [isOpen, items]); + + const handleLinkClick = (url: string) => { + navigate(url); + onClose(); + }; + + const pushView = (node: MenuNode) => { + setStack((prev) => [...prev, node]); + }; + + const popView = () => { + setStack((prev) => (prev.length > 1 ? prev.slice(0, -1) : prev)); + }; + + const renderView = (view: MenuNode) => { + if (view.type === 'root') { + return ( +
+ {view.data.map((item) => ( +
+ {item.columns && item.columns.length > 0 ? ( + + ) : ( + + )} +
+ ))} + {customer && ( +
+ +
+ )} +
+ ); + } + + if (view.type === 'mega') { + return ( +
+ {view.data.map((col, idx) => ( +
+

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

+
+ {col.links.map((link, lIdx) => ( +
+ {link.links && link.links.length > 0 ? ( + + ) : ( + + )} +
+ ))} +
+
+ ))} +
+ ); + } + + if (view.type === 'links') { + return ( +
+ {view.data.map((link, lIdx) => ( +
+ {link.links && link.links.length > 0 ? ( + + ) : ( + + )} +
+ ))} +
+ ); + } + + return null; + }; + + return ( +
+
+
+ {/* Static Top Header */} +
+ Menu +
+ + +
+
+ + {/* Sliding Views Container */} +
+ {stack.map((view, index) => { + const isCurrent = index === stack.length - 1; + const isPast = index < stack.length - 1; + + return ( +
+ {/* Back Button for nested views */} + {index > 0 && ( + + )} + + {/* Optional View Title Header for nested views to emphasize context */} + {index > 0 && ( +
+

{getLocalizedText(view.title, lang)}

+
+ )} + + {/* View Content */} +
+ {renderView(view)} +
+
+ ); + })} +
+ + {/* Static Bottom Footer */} +
+ + {customer ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/apps/spa/src/components/Navbar.tsx b/apps/spa/src/components/Navbar.tsx index 3a0f45b..9234415 100644 --- a/apps/spa/src/components/Navbar.tsx +++ b/apps/spa/src/components/Navbar.tsx @@ -1,27 +1,31 @@ -import { Link, NavLink, useNavigate } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' import { useState, useEffect } from 'react' -import { API_BASE, type Category, type MenuItem } from '../types' -import { ShoppingBag, MapPin, Menu as MenuIcon, X, Heart, Globe, Check } from 'lucide-react' +import { type Category } from '../types' +import { ShoppingBag, Menu as MenuIcon, X, Heart, Globe, Check } from 'lucide-react' import { useCartStore } from '../store/cartStore' import { useWishlistStore } from '../store/wishlistStore' import { useAuthStore } from '../store/authStore' import { logoutLiff, loginLiff } from '../lib/liff' import { useTranslation } from 'react-i18next' +import { useMediaQuery } from '../hooks/useMediaQuery' +import { DesktopMegaMenu } from './DesktopMegaMenu' +import { MobileDrawer } from './MobileDrawer' +import { useMenuStore } from '../store/menuStore' interface NavbarProps { categories?: Category[] topOffsetClassName?: string } -export default function Navbar({ categories = [], topOffsetClassName = 'top-0' }: NavbarProps) { +export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) { + const isDesktop = useMediaQuery('(min-width: 768px)') + const [searchOpen, setSearchOpen] = useState(false) const [mobileOpen, setMobileOpen] = useState(false) const [langSheetOpen, setLangSheetOpen] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [scrolled, setScrolled] = useState(false) - const [localOrdersCount, setLocalOrdersCount] = useState(0) const [hasAnnouncement, setHasAnnouncement] = useState(false) - const [menuItems, setMenuItems] = useState([]) const navigate = useNavigate() const { toggleCart, getCartCount } = useCartStore() @@ -29,6 +33,8 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' } const wishlistCount = useWishlistStore(state => state.items.length) const { customer } = useAuthStore() const { t, i18n } = useTranslation() + + const { megaMenu, fetchMegaMenu } = useMenuStore() const LANGUAGES = [ { code: 'th', label: 'เน„เธ—เธข', short: 'TH' }, @@ -45,35 +51,13 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' } setLangSheetOpen(false) } - const translateMenuLabel = (item: MenuItem) => { - const lang = localStorage.getItem('homaroma_lang') || 'en' - if (lang !== 'en' && item.translations && item.translations.length > 0) { - const trans = item.translations.find((t: any) => t.langCode === lang) - if (trans && trans.label) { - return trans.label - } - } - - const map: Record = { - 'Home': 'nav.home', - 'Shop': 'nav.shop', - 'Scent Guide': 'nav.scentGuide', - 'About Us': 'nav.about', - 'FAQ': 'nav.faq', - 'Contact': 'nav.contact', - 'Contact Us': 'nav.contact', - 'Track Order': 'nav.trackOrder', - 'Delivery & Returns': 'nav.delivery' - } - return map[item.label] ? t(map[item.label]) : item.label - } - // Detect scroll and announcement bar presence useEffect(() => { + fetchMegaMenu() + const checkAnnouncement = () => { const el = document.querySelector('.announcement-bar') setHasAnnouncement(!!el) } - checkAnnouncement() const timer = setTimeout(checkAnnouncement, 300) @@ -83,34 +67,13 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' } } window.addEventListener('scroll', onScroll, { passive: true }) - // Check local orders periodically (or just on mount) - const stored = localStorage.getItem('homaroma_local_orders') - if (stored) { - try { setLocalOrdersCount(JSON.parse(stored).length) } catch (e) {} - } - - // Fetch dynamic menu - fetch(`${API_BASE}/menus/main`) - .then(res => { - if (!res.ok) throw new Error('Menu not found') - return res.json() - }) - .then(data => { - if (data && data.items) { - setMenuItems(data.items.sort((a: any, b: any) => a.order - b.order)) - } - }) - .catch(console.error) - return () => { window.removeEventListener('scroll', onScroll) clearTimeout(timer) } - }, []) + }, [fetchMegaMenu]) - // Close mobile menu on route change useEffect(() => { setMobileOpen(false) }, [navigate]) - // Prevent body scroll when overlays open useEffect(() => { document.body.style.overflow = (searchOpen || mobileOpen || langSheetOpen) ? 'hidden' : '' return () => { document.body.style.overflow = '' } @@ -126,9 +89,6 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' } } } - const navLinkClass = ({ isActive }: { isActive: boolean }) => - isActive ? 'text-spa-forest font-semibold' : 'text-spa-stone hover:text-spa-forest transition-colors' - const computedOffset = topOffsetClassName !== 'top-0' ? topOffsetClassName : (hasAnnouncement ? 'top-[32px] md:top-[40px]' : 'top-0') @@ -137,96 +97,33 @@ export default function Navbar({ categories = [], topOffsetClassName = 'top-0' } return ( <> - {/* โ”€โ”€ NAVBAR BAR โ”€โ”€ */} - {/* โ”€โ”€ MOBILE DRAWER โ”€โ”€ */} -
- {/* Backdrop */} -
setMobileOpen(false)} + {!isDesktop && ( + setMobileOpen(false)} + items={megaMenu} + onOpenLang={() => setLangSheetOpen(true)} /> - {/* Drawer */} -
- {/* Drawer Header */} -
- {t('nav.menu')} -
- {customer && ( - - )} - - -
-
+ )} - {/* Drawer Nav Links */} -
- {(menuItems.length > 0 ? [ - ...menuItems.map(item => ({ to: item.url, label: translateMenuLabel(item), end: item.url === '/' })), - ...(customer ? [{ to: '/profile', label: `๐Ÿ‘ค ${t('nav.profile') || 'My Profile'}` }] : []), - ] : [ - { to: '/', label: t('nav.home'), end: true }, - { to: '/shop', label: t('nav.shop') }, - ...(customer ? [{ to: '/profile', label: `๐Ÿ‘ค ${t('nav.profile') || 'My Profile'}` }] : []), - { to: '/coupons', label: `๐Ÿ”ฅ ${t('nav.promotions') || 'Promotions'}` }, - { to: '/scent-guide', label: `โœจ ${t('nav.scentGuide')}` }, - { to: '/about', label: t('nav.about') }, - { to: '/faq', label: t('nav.faq') }, - { to: '/contact', label: t('nav.contact') }, - ]).map((item: any) => ( - setMobileOpen(false)} - className={({ isActive }) => - `block px-4 py-3 rounded-xl text-sm font-medium tracking-wide transition-all duration-200 ${ - isActive ? 'bg-spa-forest text-white' : 'text-spa-stone hover:bg-spa-sage hover:text-spa-forest' - }` - } - > - {item.label} - - ))} - - {/* Collections sub-section */} - {categories.length > 0 && ( -
-

{t('nav.collections')}

- {categories.map(cat => ( - setMobileOpen(false)} - className="flex items-center justify-between px-4 py-2.5 rounded-xl text-sm text-spa-stone hover:text-spa-forest hover:bg-spa-sage transition-all" - > - {typeof cat.name === 'string' ? cat.name : (cat.name as any)?.[i18n.language] || (cat.name as any)?.en || ''} - - - - - ))} -
- )} -
- - {/* Drawer Footer CTA */} -
- setMobileOpen(false)} - className="block w-full py-3 bg-spa-forest text-white font-bold text-center rounded-xl text-xs uppercase tracking-widest hover:bg-spa-stone transition-colors duration-300" - > - {t('nav.shopNow')} - - setMobileOpen(false)} - className="block w-full py-3 text-center rounded-xl text-xs font-bold uppercase tracking-widest hover:bg-spa-sage transition-colors border-2 border-spa-forest text-spa-forest" - > - {t('nav.findMyScent')} - - - {customer ? ( - - ) : ( - - )} -
-
-
- - {/* โ”€โ”€ SEARCH OVERLAY โ”€โ”€ */} {searchOpen && (
- -
-

Quick Links

-
- {[ - ...categories.map(c => ({ label: typeof c.name === 'string' ? c.name : (c.name as any)?.[i18n.language] || (c.name as any)?.en || '', to: `/category/${c.id}` })), - { label: 'โœจ Scent Guide', to: '/scent-guide' }, - { label: 'All Products', to: '/shop' }, - ].map(item => ( - setSearchOpen(false)} - className="px-4 py-1.5 rounded-full text-xs font-medium text-spa-stone hover:bg-spa-forest hover:text-white transition-all border border-spa-stone/20 bg-white" - > - {item.label} - - ))} -
-
)} - {/* โ”€โ”€ MOBILE LANGUAGE BOTTOM SHEET โ”€โ”€ */} +
diff --git a/apps/spa/src/hooks/useMediaQuery.ts b/apps/spa/src/hooks/useMediaQuery.ts new file mode 100644 index 0000000..0140ee4 --- /dev/null +++ b/apps/spa/src/hooks/useMediaQuery.ts @@ -0,0 +1,17 @@ +import { useState, useEffect } from 'react'; + +export function useMediaQuery(query: string): boolean { + const [matches, setMatches] = useState(false); + + useEffect(() => { + const media = window.matchMedia(query); + if (media.matches !== matches) { + setMatches(media.matches); + } + const listener = () => setMatches(media.matches); + media.addEventListener('change', listener); + return () => media.removeEventListener('change', listener); + }, [matches, query]); + + return matches; +} diff --git a/apps/spa/src/store/menuStore.ts b/apps/spa/src/store/menuStore.ts new file mode 100644 index 0000000..4c2e50d --- /dev/null +++ b/apps/spa/src/store/menuStore.ts @@ -0,0 +1,42 @@ +import { create } from 'zustand'; +import { API_BASE } from '@homaroma/types'; + +export interface MenuLink { + label: Record; + url?: string; + links?: MenuLink[]; +} + +export interface MenuColumn { + title: Record; + links: MenuLink[]; +} + +export interface MegaMenuPayload { + id: string; + label: Record; + url?: string; + columns?: MenuColumn[]; +} + +interface MenuState { + megaMenu: MegaMenuPayload[]; + isLoading: boolean; + fetchMegaMenu: () => Promise; +} + +export const useMenuStore = create((set) => ({ + megaMenu: [], + isLoading: false, + fetchMegaMenu: async () => { + set({ isLoading: true }); + try { + const response = await fetch(`${API_BASE}/menus/main`); + const data = await response.json(); + set({ megaMenu: data.payload || [], isLoading: false }); + } catch (error) { + console.error('Failed to fetch mega menu', error); + set({ isLoading: false }); + } + } +}));