diff --git a/components/shopify/header.tsx b/components/shopify/header.tsx index 411ced1..09aa31c 100644 --- a/components/shopify/header.tsx +++ b/components/shopify/header.tsx @@ -6,6 +6,7 @@ import { useCartStore } from '@/hooks/use-shopify-cart'; import CartDrawer from '@/components/shopify/cart-drawer'; import SearchDialog from '@/components/shopify/search-dialog'; import AccountMenu from '@/components/shopify/account-menu'; +import ShopMenu from '@/components/shopify/shop-menu'; import { RiShoppingBagLine, RiCloseLine, RiMenu3Line } from '@remixicon/react'; import { Button } from '@/components/ui/button'; import Logo from '@/components/logo'; @@ -88,6 +89,7 @@ const Header: React.FC = ({ {/* Desktop Navigation */}
+ {links.map((link, index) => ( = ({ {menuOpen && (
+ setMenuOpen(false)} /> {links.map((link, index) => ( void; +} + +const ShopMenu: React.FC = ({ + label = 'Shop', + mobile = false, + onNavigate, +}) => { + const [open, setOpen] = useState(false); + const { collections, loading, error, load } = useCollectionsOnDemand(); + + // The collection list is only worth fetching once someone opens the menu. + const toggle = () => { + const next = !open; + setOpen(next); + if (next) load(); + }; + + const close = () => { + setOpen(false); + onNavigate?.(); + }; + + useEffect(() => { + if (!open) return; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') setOpen(false); + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [open]); + + const itemClasses = cn( + 'block text-sm text-foreground hover:bg-accent transition-colors', + mobile ? 'px-3 py-2' : 'px-4 py-2' + ); + + const body = ( + <> + {loading && + Array.from({ length: 5 }).map((_, index) => ( +
+
+
+ ))} + + {error && ( +
+

{error}

+ +
+ )} + + {!loading && !error && collections.length === 0 && ( +

+ No collections yet. +

+ )} + + {collections.map((collection) => ( + + {collection.title} + + ))} + + {collections.length > 0 && ( + <> +
+ + View all collections + + + )} + + ); + + const trigger = ( + + ); + + if (mobile) { + return ( +
+ {trigger} + {open && ( +
+ {body} +
+ )} +
+ ); + } + + return ( +
+ {trigger} + + {open && ( + <> + {/* Catches the click that dismisses the panel. */} +
setOpen(false)} /> +
+ {body} +
+ + )} +
+ ); +}; + +export default ShopMenu; diff --git a/hooks/use-shopify-collections.ts b/hooks/use-shopify-collections.ts index af3aecc..756225e 100644 --- a/hooks/use-shopify-collections.ts +++ b/hooks/use-shopify-collections.ts @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { getCollections, getCollectionProducts, @@ -60,6 +60,36 @@ export function useCollections(first = 50) { return { collections, loading, error, refetch: fetchCollections }; } +// Deferred variant of useCollections: nothing is requested until `load` runs, +// so a menu can hold off until it's actually opened. The fetch happens once — +// re-opening reuses what's already in state. +export function useCollectionsOnDemand(first = 50) { + const [collections, setCollections] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const requested = useRef(false); + + const load = useCallback(async () => { + if (requested.current) return; + requested.current = true; + + try { + setLoading(true); + setError(null); + setCollections(await getCollections(first)); + } catch (err) { + console.error('Error fetching collections:', err); + // Let the next open (or a retry) try again. + requested.current = false; + setError(err instanceof Error ? err.message : 'Failed to load collections'); + } finally { + setLoading(false); + } + }, [first]); + + return { collections, loading, error, load }; +} + // Hook for fetching products in a collection export function useCollectionProducts( handle: string | null,