Template
Add a Shop menu that lists the store's collections
The header now carries a "Shop" dropdown that fetches the collection list the first time it's opened and links each entry to its collection page. Fetching is deferred so the request only happens on open: useCollections loads on mount, which would put a collections query on every page. The new useCollectionsOnDemand holds off until load() runs, keeps the result for later opens, and clears its guard on failure so a retry can go through. Rendered in the desktop nav as a floating panel and in the mobile menu as an inline section, following the hand-rolled pattern in account-menu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D1vsTA4MX6qFd7nqSAKtdV
This commit is contained in:
co-authored by
Claude Opus 5
parent
ff4adba2fb
commit
95b191eeb8
@@ -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<HeaderProps> = ({
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center gap-x-8 text-sm">
|
||||
<ShopMenu />
|
||||
{links.map((link, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
@@ -127,6 +129,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
{menuOpen && (
|
||||
<div className="md:hidden bg-background">
|
||||
<div className="max-w-screen-2xl mx-auto px-8 pb-6 flex flex-col gap-y-4 text-sm">
|
||||
<ShopMenu mobile onNavigate={() => setMenuOpen(false)} />
|
||||
{links.map((link, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { RiArrowDownSLine } from '@remixicon/react';
|
||||
import { useCollectionsOnDemand } from '@/hooks/use-shopify-collections';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ShopMenuProps {
|
||||
label?: string;
|
||||
/** Renders inline inside the mobile menu instead of as a floating panel. */
|
||||
mobile?: boolean;
|
||||
/** Fires after a collection is picked, so the mobile menu can close itself. */
|
||||
onNavigate?: () => void;
|
||||
}
|
||||
|
||||
const ShopMenu: React.FC<ShopMenuProps> = ({
|
||||
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) => (
|
||||
<div key={index} className={cn(itemClasses, 'py-2.5')}>
|
||||
<div className="h-3 w-2/3 animate-pulse bg-muted" />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{error && (
|
||||
<div className={cn(itemClasses, 'hover:bg-transparent')}>
|
||||
<p className="text-muted-foreground">{error}</p>
|
||||
<button
|
||||
onClick={load}
|
||||
className="mt-1 underline underline-offset-2 hover:text-muted-foreground"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && collections.length === 0 && (
|
||||
<p className={cn(itemClasses, 'text-muted-foreground hover:bg-transparent')}>
|
||||
No collections yet.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{collections.map((collection) => (
|
||||
<Link
|
||||
key={collection.id}
|
||||
href={`/collections/${collection.handle}`}
|
||||
onClick={close}
|
||||
className={itemClasses}
|
||||
>
|
||||
{collection.title}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{collections.length > 0 && (
|
||||
<>
|
||||
<div className="my-1 h-px bg-border" />
|
||||
<Link
|
||||
href="/collections"
|
||||
onClick={close}
|
||||
className={cn(itemClasses, 'text-muted-foreground')}
|
||||
>
|
||||
View all collections
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const trigger = (
|
||||
<button
|
||||
onClick={toggle}
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
'flex items-center gap-x-1 text-foreground hover:text-muted-foreground transition-colors',
|
||||
mobile && 'w-full justify-between'
|
||||
)}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<RiArrowDownSLine
|
||||
className={cn('size-4 transition-transform', open && 'rotate-180')}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (mobile) {
|
||||
return (
|
||||
<div>
|
||||
{trigger}
|
||||
{open && (
|
||||
<div className="mt-2 flex flex-col border-l border-border pl-1">
|
||||
{body}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{trigger}
|
||||
|
||||
{open && (
|
||||
<>
|
||||
{/* Catches the click that dismisses the panel. */}
|
||||
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
||||
<div className="absolute left-0 top-full z-50 mt-2 max-h-[70vh] w-64 overflow-y-auto rounded-md border border-border bg-background py-1 shadow-md">
|
||||
{body}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShopMenu;
|
||||
Reference in New Issue
Block a user