Template
Compare commits
4
Commits
ebdb0bee2f
...
95b191eeb8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95b191eeb8 | ||
|
|
ff4adba2fb | ||
|
|
8602e03079 | ||
|
|
3442f19816 |
@@ -1,5 +1,3 @@
|
|||||||
import Header from '@/components/shopify/header';
|
|
||||||
import Footer from '@/components/shopify/footer';
|
|
||||||
import AccountForm from '@/components/shopify/account-form';
|
import AccountForm from '@/components/shopify/account-form';
|
||||||
|
|
||||||
export const metadata = { title: 'Activate your account — Shop' };
|
export const metadata = { title: 'Activate your account — Shop' };
|
||||||
@@ -13,8 +11,6 @@ export default async function Page({
|
|||||||
const { id, token } = await params;
|
const { id, token } = await params;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Header storeName="Shop" logoUrl="" />
|
|
||||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||||
<AccountForm
|
<AccountForm
|
||||||
title="Activate your account"
|
title="Activate your account"
|
||||||
@@ -33,7 +29,5 @@ export default async function Page({
|
|||||||
redirectTo="/account"
|
redirectTo="/account"
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
<Footer storeName="Shop" copyright="© 2026 Shop. All rights reserved." />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import Header from '@/components/shopify/header';
|
|
||||||
import Footer from '@/components/shopify/footer';
|
|
||||||
import AccountForm, { AccountFormLink } from '@/components/shopify/account-form';
|
import AccountForm, { AccountFormLink } from '@/components/shopify/account-form';
|
||||||
|
|
||||||
export const metadata = { title: 'Sign in — Shop' };
|
export const metadata = { title: 'Sign in — Shop' };
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Header storeName="Shop" logoUrl="" />
|
|
||||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||||
<AccountForm
|
<AccountForm
|
||||||
title="Sign in"
|
title="Sign in"
|
||||||
@@ -38,7 +34,5 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
<Footer storeName="Shop" copyright="© 2026 Shop. All rights reserved." />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
import Header from '@/components/shopify/header';
|
|
||||||
import Footer from '@/components/shopify/footer';
|
|
||||||
import OrderHistory from '@/components/shopify/order-history';
|
import OrderHistory from '@/components/shopify/order-history';
|
||||||
import { getSessionToken } from '@/services/shopify/session';
|
import { getSessionToken } from '@/services/shopify/session';
|
||||||
import { getCustomer } from '@/services/shopify/customer';
|
import { getCustomer } from '@/services/shopify/customer';
|
||||||
@@ -16,9 +14,6 @@ export default async function Page() {
|
|||||||
if (!customer) redirect('/account/login');
|
if (!customer) redirect('/account/login');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Header storeName="Shop" logoUrl="" />
|
|
||||||
|
|
||||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-12">
|
<main className="max-w-screen-2xl mx-auto w-full px-8 py-12">
|
||||||
<h1 className="text-2xl md:text-3xl font-normal text-foreground">
|
<h1 className="text-2xl md:text-3xl font-normal text-foreground">
|
||||||
Order history
|
Order history
|
||||||
@@ -31,8 +26,5 @@ export default async function Page() {
|
|||||||
<OrderHistory customer={customer} />
|
<OrderHistory customer={customer} />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer storeName="Shop" copyright="© 2026 Shop. All rights reserved." />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import Header from '@/components/shopify/header';
|
|
||||||
import Footer from '@/components/shopify/footer';
|
|
||||||
import AccountForm, { AccountFormLink } from '@/components/shopify/account-form';
|
import AccountForm, { AccountFormLink } from '@/components/shopify/account-form';
|
||||||
|
|
||||||
export const metadata = { title: 'Reset password — Shop' };
|
export const metadata = { title: 'Reset password — Shop' };
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Header storeName="Shop" logoUrl="" />
|
|
||||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||||
<AccountForm
|
<AccountForm
|
||||||
title="Reset password"
|
title="Reset password"
|
||||||
@@ -23,7 +19,5 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
<Footer storeName="Shop" copyright="© 2026 Shop. All rights reserved." />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import Header from '@/components/shopify/header';
|
|
||||||
import Footer from '@/components/shopify/footer';
|
|
||||||
import AccountForm, { AccountFormLink } from '@/components/shopify/account-form';
|
import AccountForm, { AccountFormLink } from '@/components/shopify/account-form';
|
||||||
|
|
||||||
export const metadata = { title: 'Create account — Shop' };
|
export const metadata = { title: 'Create account — Shop' };
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Header storeName="Shop" logoUrl="" />
|
|
||||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||||
<AccountForm
|
<AccountForm
|
||||||
title="Create account"
|
title="Create account"
|
||||||
@@ -43,7 +39,5 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
<Footer storeName="Shop" copyright="© 2026 Shop. All rights reserved." />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import Header from '@/components/shopify/header';
|
|
||||||
import Footer from '@/components/shopify/footer';
|
|
||||||
import AccountForm from '@/components/shopify/account-form';
|
import AccountForm from '@/components/shopify/account-form';
|
||||||
|
|
||||||
export const metadata = { title: 'Set a new password — Shop' };
|
export const metadata = { title: 'Set a new password — Shop' };
|
||||||
@@ -13,8 +11,6 @@ export default async function Page({
|
|||||||
const { id, token } = await params;
|
const { id, token } = await params;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Header storeName="Shop" logoUrl="" />
|
|
||||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||||
<AccountForm
|
<AccountForm
|
||||||
title="Set a new password"
|
title="Set a new password"
|
||||||
@@ -32,7 +28,5 @@ export default async function Page({
|
|||||||
redirectTo="/account"
|
redirectTo="/account"
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
<Footer storeName="Shop" copyright="© 2026 Shop. All rights reserved." />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,5 @@
|
|||||||
import Header from '@/components/shopify/header';
|
|
||||||
import Footer from '@/components/shopify/footer';
|
|
||||||
import CollectionDetail from '@/components/shopify/collection-detail';
|
import CollectionDetail from '@/components/shopify/collection-detail';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return <CollectionDetail />;
|
||||||
<>
|
|
||||||
<Header
|
|
||||||
storeName="Shop"
|
|
||||||
logoUrl=""
|
|
||||||
links={[
|
|
||||||
{ label: 'Products', url: '/' },
|
|
||||||
{ label: 'Collections', url: '/collections' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<CollectionDetail />
|
|
||||||
<Footer
|
|
||||||
storeName="Shop"
|
|
||||||
copyright="© 2026 Shop. All rights reserved."
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,5 @@
|
|||||||
import Header from '@/components/shopify/header';
|
|
||||||
import Footer from '@/components/shopify/footer';
|
|
||||||
import Collections from '@/components/shopify/collections';
|
import Collections from '@/components/shopify/collections';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return <Collections title="Our Collections" />;
|
||||||
<>
|
|
||||||
<Header
|
|
||||||
storeName="Shop"
|
|
||||||
logoUrl=""
|
|
||||||
links={[
|
|
||||||
{ label: 'Products', url: '/' },
|
|
||||||
{ label: 'Collections', url: '/collections' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Collections title="Our Collections" />
|
|
||||||
<Footer
|
|
||||||
storeName="Shop"
|
|
||||||
copyright="© 2026 Shop. All rights reserved."
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ export default function Error({
|
|||||||
reset: () => void;
|
reset: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
<div className="text-lg font-medium text-black font-sans whitespace-nowrap">
|
<div className="text-lg font-medium text-black font-sans whitespace-nowrap">
|
||||||
Something went wrong
|
Something went wrong
|
||||||
|
|||||||
+13
-2
@@ -1,7 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
import { Geist, Geist_Mono } from 'next/font/google';
|
import { Geist, Geist_Mono } from 'next/font/google';
|
||||||
|
import Header from '@/components/shopify/header';
|
||||||
|
import Footer from '@/components/shopify/footer';
|
||||||
import StoreAssistant from '@/components/shopify/store-assistant';
|
import StoreAssistant from '@/components/shopify/store-assistant';
|
||||||
|
import { site } from '@/config/site';
|
||||||
|
|
||||||
const geist = Geist({
|
const geist = Geist({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
@@ -20,8 +23,16 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={`${geist.variable} ${geistMono.variable}`}>
|
<html lang="en" className={`${geist.variable} ${geistMono.variable}`}>
|
||||||
<body className="font-body antialiased bg-background text-foreground m-0 p-0">
|
<body className="font-body antialiased bg-background text-foreground m-0 p-0 flex min-h-screen flex-col">
|
||||||
{children}
|
{/* Header owns the cart drawer, search and account menu, so every
|
||||||
|
route gets them by rendering here rather than page by page. */}
|
||||||
|
<Header
|
||||||
|
storeName={site.storeName}
|
||||||
|
logoUrl={site.logoUrl}
|
||||||
|
links={site.navLinks}
|
||||||
|
/>
|
||||||
|
<div className="flex-1">{children}</div>
|
||||||
|
<Footer storeName={site.storeName} copyright={site.copyright} />
|
||||||
<StoreAssistant />
|
<StoreAssistant />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import Link from 'next/link';
|
|||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
<div className="text-lg font-medium text-black font-sans whitespace-nowrap">
|
<div className="text-lg font-medium text-black font-sans whitespace-nowrap">
|
||||||
404
|
404
|
||||||
|
|||||||
@@ -1,26 +1,10 @@
|
|||||||
import Header from '@/components/shopify/header';
|
|
||||||
import Footer from '@/components/shopify/footer';
|
|
||||||
import Products from '@/components/shopify/products';
|
import Products from '@/components/shopify/products';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Header
|
|
||||||
storeName="Shop"
|
|
||||||
logoUrl=""
|
|
||||||
links={[
|
|
||||||
{ label: 'Products', url: '/' },
|
|
||||||
{ label: 'Collections', url: '/collections' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Products
|
<Products
|
||||||
title="Shopify Hydrogen Storefront"
|
title="Shopify Hydrogen Storefront"
|
||||||
subtitle="An agent-friendly Shopify storefront built with Next.js and Hydrogen."
|
subtitle="An agent-friendly Shopify storefront built with Next.js and Hydrogen."
|
||||||
/>
|
/>
|
||||||
<Footer
|
|
||||||
storeName="Shop"
|
|
||||||
copyright="© 2026 Shop. All rights reserved."
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import Header from '@/components/shopify/header';
|
|
||||||
import Footer from '@/components/shopify/footer';
|
|
||||||
import { getShopPolicy, POLICY_HANDLES } from '@/hooks/use-shopify-policies';
|
import { getShopPolicy, POLICY_HANDLES } from '@/hooks/use-shopify-policies';
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
@@ -33,16 +31,6 @@ export default async function PolicyPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Header
|
|
||||||
storeName="Shop"
|
|
||||||
logoUrl=""
|
|
||||||
links={[
|
|
||||||
{ label: 'Products', url: '/' },
|
|
||||||
{ label: 'Collections', url: '/collections' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<main className="max-w-screen-2xl mx-auto w-full px-5 lg:px-10 py-16">
|
<main className="max-w-screen-2xl mx-auto w-full px-5 lg:px-10 py-16">
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="max-w-2xl mx-auto">
|
||||||
<h1 className="text-3xl md:text-4xl font-normal text-foreground">
|
<h1 className="text-3xl md:text-4xl font-normal text-foreground">
|
||||||
@@ -55,11 +43,5 @@ export default async function PolicyPage({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer
|
|
||||||
storeName="Shop"
|
|
||||||
copyright="© 2026 Shop. All rights reserved."
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,11 @@
|
|||||||
import Header from '@/components/shopify/header';
|
|
||||||
import Footer from '@/components/shopify/footer';
|
|
||||||
import ProductDetail from '@/components/shopify/product-detail';
|
import ProductDetail from '@/components/shopify/product-detail';
|
||||||
import ProductRecommendations from '@/components/shopify/product-recommendations';
|
import ProductRecommendations from '@/components/shopify/product-recommendations';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header
|
|
||||||
storeName="Shop"
|
|
||||||
logoUrl=""
|
|
||||||
links={[
|
|
||||||
{ label: 'Products', url: '/' },
|
|
||||||
{ label: 'Collections', url: '/collections' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<ProductDetail addToCartLabel="Add to Cart" />
|
<ProductDetail addToCartLabel="Add to Cart" />
|
||||||
<ProductRecommendations title="You May Also Like" />
|
<ProductRecommendations title="You May Also Like" />
|
||||||
<Footer
|
|
||||||
storeName="Shop"
|
|
||||||
copyright="© 2026 Shop. All rights reserved."
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-19
@@ -1,6 +1,4 @@
|
|||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import Header from '@/components/shopify/header';
|
|
||||||
import Footer from '@/components/shopify/footer';
|
|
||||||
import SearchResults from '@/components/shopify/search-results';
|
import SearchResults from '@/components/shopify/search-results';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
@@ -9,25 +7,9 @@ export const metadata = {
|
|||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<>
|
// useSearchParams needs a Suspense boundary to prerender this route.
|
||||||
<Header
|
|
||||||
storeName="Shop"
|
|
||||||
logoUrl=""
|
|
||||||
links={[
|
|
||||||
{ label: 'Products', url: '/' },
|
|
||||||
{ label: 'Collections', url: '/collections' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* useSearchParams needs a Suspense boundary to prerender this route. */}
|
|
||||||
<Suspense fallback={<div className="py-10" />}>
|
<Suspense fallback={<div className="py-10" />}>
|
||||||
<SearchResults />
|
<SearchResults />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<Footer
|
|
||||||
storeName="Shop"
|
|
||||||
copyright="© 2026 Shop. All rights reserved."
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,5 @@
|
|||||||
import Header from '@/components/shopify/header';
|
|
||||||
import Footer from '@/components/shopify/footer';
|
|
||||||
import Collections from '@/components/shopify/collections';
|
import Collections from '@/components/shopify/collections';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return <Collections title="Our Collections" />;
|
||||||
<>
|
|
||||||
<Header
|
|
||||||
storeName="Shop"
|
|
||||||
logoUrl=""
|
|
||||||
links={[
|
|
||||||
{ label: 'Products', url: '/' },
|
|
||||||
{ label: 'Collections', url: '/shop/collections' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Collections title="Our Collections" />
|
|
||||||
<Footer
|
|
||||||
storeName="Shop"
|
|
||||||
copyright="© 2026 Shop. All rights reserved."
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useCartStore } from '@/hooks/use-shopify-cart';
|
|||||||
import CartDrawer from '@/components/shopify/cart-drawer';
|
import CartDrawer from '@/components/shopify/cart-drawer';
|
||||||
import SearchDialog from '@/components/shopify/search-dialog';
|
import SearchDialog from '@/components/shopify/search-dialog';
|
||||||
import AccountMenu from '@/components/shopify/account-menu';
|
import AccountMenu from '@/components/shopify/account-menu';
|
||||||
|
import ShopMenu from '@/components/shopify/shop-menu';
|
||||||
import { RiShoppingBagLine, RiCloseLine, RiMenu3Line } from '@remixicon/react';
|
import { RiShoppingBagLine, RiCloseLine, RiMenu3Line } from '@remixicon/react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import Logo from '@/components/logo';
|
import Logo from '@/components/logo';
|
||||||
@@ -88,6 +89,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
|
|
||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
<div className="hidden md:flex items-center gap-x-8 text-sm">
|
<div className="hidden md:flex items-center gap-x-8 text-sm">
|
||||||
|
<ShopMenu />
|
||||||
{links.map((link, index) => (
|
{links.map((link, index) => (
|
||||||
<Link
|
<Link
|
||||||
key={index}
|
key={index}
|
||||||
@@ -127,6 +129,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
{menuOpen && (
|
{menuOpen && (
|
||||||
<div className="md:hidden bg-background">
|
<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">
|
<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) => (
|
{links.map((link, index) => (
|
||||||
<Link
|
<Link
|
||||||
key={index}
|
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;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { NavLink } from '@/components/shopify/header';
|
||||||
|
|
||||||
|
/** Chrome shared by every route — rendered once in the root layout. */
|
||||||
|
export const site = {
|
||||||
|
storeName: 'Shop',
|
||||||
|
logoUrl: '',
|
||||||
|
copyright: '© 2026 Shop. All rights reserved.',
|
||||||
|
navLinks: [
|
||||||
|
{ label: 'Products', url: '/' },
|
||||||
|
{ label: 'Collections', url: '/collections' },
|
||||||
|
] as NavLink[],
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
getCollections,
|
getCollections,
|
||||||
getCollectionProducts,
|
getCollectionProducts,
|
||||||
@@ -60,6 +60,36 @@ export function useCollections(first = 50) {
|
|||||||
return { collections, loading, error, refetch: fetchCollections };
|
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<Collection[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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
|
// Hook for fetching products in a collection
|
||||||
export function useCollectionProducts(
|
export function useCollectionProducts(
|
||||||
handle: string | null,
|
handle: string | null,
|
||||||
|
|||||||
Reference in New Issue
Block a user