Template
Compare commits
4
Commits
| 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';
|
||||
|
||||
export const metadata = { title: 'Activate your account — Shop' };
|
||||
@@ -13,27 +11,23 @@ export default async function Page({
|
||||
const { id, token } = await params;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header storeName="Shop" logoUrl="" />
|
||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||
<AccountForm
|
||||
title="Activate your account"
|
||||
description="Choose a password to finish setting up your account."
|
||||
fields={[
|
||||
{
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: 'password',
|
||||
autoComplete: 'new-password',
|
||||
},
|
||||
]}
|
||||
submitLabel="Activate account"
|
||||
endpoint="/api/account/activate"
|
||||
extraPayload={{ id, activationToken: token }}
|
||||
redirectTo="/account"
|
||||
/>
|
||||
</main>
|
||||
<Footer storeName="Shop" copyright="© 2026 Shop. All rights reserved." />
|
||||
</>
|
||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||
<AccountForm
|
||||
title="Activate your account"
|
||||
description="Choose a password to finish setting up your account."
|
||||
fields={[
|
||||
{
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: 'password',
|
||||
autoComplete: 'new-password',
|
||||
},
|
||||
]}
|
||||
submitLabel="Activate account"
|
||||
endpoint="/api/account/activate"
|
||||
extraPayload={{ id, activationToken: token }}
|
||||
redirectTo="/account"
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
+29
-35
@@ -1,44 +1,38 @@
|
||||
import Header from '@/components/shopify/header';
|
||||
import Footer from '@/components/shopify/footer';
|
||||
import AccountForm, { AccountFormLink } from '@/components/shopify/account-form';
|
||||
|
||||
export const metadata = { title: 'Sign in — Shop' };
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Header storeName="Shop" logoUrl="" />
|
||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||
<AccountForm
|
||||
title="Sign in"
|
||||
fields={[
|
||||
{ name: 'email', label: 'Email', type: 'email', autoComplete: 'email' },
|
||||
{
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: 'password',
|
||||
autoComplete: 'current-password',
|
||||
},
|
||||
]}
|
||||
submitLabel="Sign in"
|
||||
endpoint="/api/account/login"
|
||||
redirectTo="/account"
|
||||
footer={
|
||||
<>
|
||||
<span>
|
||||
New here?{' '}
|
||||
<AccountFormLink href="/account/register">
|
||||
Create an account
|
||||
</AccountFormLink>
|
||||
</span>
|
||||
<AccountFormLink href="/account/recover">
|
||||
Forgot your password?
|
||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||
<AccountForm
|
||||
title="Sign in"
|
||||
fields={[
|
||||
{ name: 'email', label: 'Email', type: 'email', autoComplete: 'email' },
|
||||
{
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: 'password',
|
||||
autoComplete: 'current-password',
|
||||
},
|
||||
]}
|
||||
submitLabel="Sign in"
|
||||
endpoint="/api/account/login"
|
||||
redirectTo="/account"
|
||||
footer={
|
||||
<>
|
||||
<span>
|
||||
New here?{' '}
|
||||
<AccountFormLink href="/account/register">
|
||||
Create an account
|
||||
</AccountFormLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</main>
|
||||
<Footer storeName="Shop" copyright="© 2026 Shop. All rights reserved." />
|
||||
</>
|
||||
</span>
|
||||
<AccountFormLink href="/account/recover">
|
||||
Forgot your password?
|
||||
</AccountFormLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
+11
-19
@@ -1,6 +1,4 @@
|
||||
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 { getSessionToken } from '@/services/shopify/session';
|
||||
import { getCustomer } from '@/services/shopify/customer';
|
||||
@@ -16,23 +14,17 @@ export default async function Page() {
|
||||
if (!customer) redirect('/account/login');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header storeName="Shop" logoUrl="" />
|
||||
<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">
|
||||
Order history
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{customer.displayName} · {customer.email}
|
||||
</p>
|
||||
|
||||
<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">
|
||||
Order history
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{customer.displayName} · {customer.email}
|
||||
</p>
|
||||
|
||||
<div className="mt-10">
|
||||
<OrderHistory customer={customer} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer storeName="Shop" copyright="© 2026 Shop. All rights reserved." />
|
||||
</>
|
||||
<div className="mt-10">
|
||||
<OrderHistory customer={customer} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
import Header from '@/components/shopify/header';
|
||||
import Footer from '@/components/shopify/footer';
|
||||
import AccountForm, { AccountFormLink } from '@/components/shopify/account-form';
|
||||
|
||||
export const metadata = { title: 'Reset password — Shop' };
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Header storeName="Shop" logoUrl="" />
|
||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||
<AccountForm
|
||||
title="Reset password"
|
||||
description="Enter your email and we'll send you a link to set a new password."
|
||||
fields={[
|
||||
{ name: 'email', label: 'Email', type: 'email', autoComplete: 'email' },
|
||||
]}
|
||||
submitLabel="Send reset link"
|
||||
endpoint="/api/account/recover"
|
||||
successMessage="If that email has an account, a reset link is on its way."
|
||||
footer={
|
||||
<AccountFormLink href="/account/login">Back to sign in</AccountFormLink>
|
||||
}
|
||||
/>
|
||||
</main>
|
||||
<Footer storeName="Shop" copyright="© 2026 Shop. All rights reserved." />
|
||||
</>
|
||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||
<AccountForm
|
||||
title="Reset password"
|
||||
description="Enter your email and we'll send you a link to set a new password."
|
||||
fields={[
|
||||
{ name: 'email', label: 'Email', type: 'email', autoComplete: 'email' },
|
||||
]}
|
||||
submitLabel="Send reset link"
|
||||
endpoint="/api/account/recover"
|
||||
successMessage="If that email has an account, a reset link is on its way."
|
||||
footer={
|
||||
<AccountFormLink href="/account/login">Back to sign in</AccountFormLink>
|
||||
}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,43 @@
|
||||
import Header from '@/components/shopify/header';
|
||||
import Footer from '@/components/shopify/footer';
|
||||
import AccountForm, { AccountFormLink } from '@/components/shopify/account-form';
|
||||
|
||||
export const metadata = { title: 'Create account — Shop' };
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Header storeName="Shop" logoUrl="" />
|
||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||
<AccountForm
|
||||
title="Create account"
|
||||
fields={[
|
||||
{
|
||||
name: 'firstName',
|
||||
label: 'First name',
|
||||
autoComplete: 'given-name',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'lastName',
|
||||
label: 'Last name',
|
||||
autoComplete: 'family-name',
|
||||
required: false,
|
||||
},
|
||||
{ name: 'email', label: 'Email', type: 'email', autoComplete: 'email' },
|
||||
{
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: 'password',
|
||||
autoComplete: 'new-password',
|
||||
},
|
||||
]}
|
||||
submitLabel="Create account"
|
||||
endpoint="/api/account/register"
|
||||
redirectTo="/account"
|
||||
footer={
|
||||
<span>
|
||||
Already have an account?{' '}
|
||||
<AccountFormLink href="/account/login">Sign in</AccountFormLink>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</main>
|
||||
<Footer storeName="Shop" copyright="© 2026 Shop. All rights reserved." />
|
||||
</>
|
||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||
<AccountForm
|
||||
title="Create account"
|
||||
fields={[
|
||||
{
|
||||
name: 'firstName',
|
||||
label: 'First name',
|
||||
autoComplete: 'given-name',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'lastName',
|
||||
label: 'Last name',
|
||||
autoComplete: 'family-name',
|
||||
required: false,
|
||||
},
|
||||
{ name: 'email', label: 'Email', type: 'email', autoComplete: 'email' },
|
||||
{
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: 'password',
|
||||
autoComplete: 'new-password',
|
||||
},
|
||||
]}
|
||||
submitLabel="Create account"
|
||||
endpoint="/api/account/register"
|
||||
redirectTo="/account"
|
||||
footer={
|
||||
<span>
|
||||
Already have an account?{' '}
|
||||
<AccountFormLink href="/account/login">Sign in</AccountFormLink>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import Header from '@/components/shopify/header';
|
||||
import Footer from '@/components/shopify/footer';
|
||||
import AccountForm from '@/components/shopify/account-form';
|
||||
|
||||
export const metadata = { title: 'Set a new password — Shop' };
|
||||
@@ -13,26 +11,22 @@ export default async function Page({
|
||||
const { id, token } = await params;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header storeName="Shop" logoUrl="" />
|
||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||
<AccountForm
|
||||
title="Set a new password"
|
||||
fields={[
|
||||
{
|
||||
name: 'password',
|
||||
label: 'New password',
|
||||
type: 'password',
|
||||
autoComplete: 'new-password',
|
||||
},
|
||||
]}
|
||||
submitLabel="Save password"
|
||||
endpoint="/api/account/reset"
|
||||
extraPayload={{ id, resetToken: token }}
|
||||
redirectTo="/account"
|
||||
/>
|
||||
</main>
|
||||
<Footer storeName="Shop" copyright="© 2026 Shop. All rights reserved." />
|
||||
</>
|
||||
<main className="max-w-screen-2xl mx-auto w-full px-8 py-16">
|
||||
<AccountForm
|
||||
title="Set a new password"
|
||||
fields={[
|
||||
{
|
||||
name: 'password',
|
||||
label: 'New password',
|
||||
type: 'password',
|
||||
autoComplete: 'new-password',
|
||||
},
|
||||
]}
|
||||
submitLabel="Save password"
|
||||
endpoint="/api/account/reset"
|
||||
extraPayload={{ id, resetToken: token }}
|
||||
redirectTo="/account"
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
import Header from '@/components/shopify/header';
|
||||
import Footer from '@/components/shopify/footer';
|
||||
import CollectionDetail from '@/components/shopify/collection-detail';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
storeName="Shop"
|
||||
logoUrl=""
|
||||
links={[
|
||||
{ label: 'Products', url: '/' },
|
||||
{ label: 'Collections', url: '/collections' },
|
||||
]}
|
||||
/>
|
||||
<CollectionDetail />
|
||||
<Footer
|
||||
storeName="Shop"
|
||||
copyright="© 2026 Shop. All rights reserved."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
return <CollectionDetail />;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
import Header from '@/components/shopify/header';
|
||||
import Footer from '@/components/shopify/footer';
|
||||
import Collections from '@/components/shopify/collections';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<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."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
return <Collections title="Our Collections" />;
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ export default function Error({
|
||||
reset: () => void;
|
||||
}) {
|
||||
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="text-lg font-medium text-black font-sans whitespace-nowrap">
|
||||
Something went wrong
|
||||
|
||||
+13
-2
@@ -1,7 +1,10 @@
|
||||
import React from 'react';
|
||||
import './globals.css';
|
||||
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 { site } from '@/config/site';
|
||||
|
||||
const geist = Geist({
|
||||
subsets: ['latin'],
|
||||
@@ -20,8 +23,16 @@ export default function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={`${geist.variable} ${geistMono.variable}`}>
|
||||
<body className="font-body antialiased bg-background text-foreground m-0 p-0">
|
||||
{children}
|
||||
<body className="font-body antialiased bg-background text-foreground m-0 p-0 flex min-h-screen flex-col">
|
||||
{/* 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 />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import Link from 'next/link';
|
||||
|
||||
export default function NotFound() {
|
||||
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="text-lg font-medium text-black font-sans whitespace-nowrap">
|
||||
404
|
||||
|
||||
+4
-20
@@ -1,26 +1,10 @@
|
||||
import Header from '@/components/shopify/header';
|
||||
import Footer from '@/components/shopify/footer';
|
||||
import Products from '@/components/shopify/products';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
storeName="Shop"
|
||||
logoUrl=""
|
||||
links={[
|
||||
{ label: 'Products', url: '/' },
|
||||
{ label: 'Collections', url: '/collections' },
|
||||
]}
|
||||
/>
|
||||
<Products
|
||||
title="Shopify Hydrogen Storefront"
|
||||
subtitle="An agent-friendly Shopify storefront built with Next.js and Hydrogen."
|
||||
/>
|
||||
<Footer
|
||||
storeName="Shop"
|
||||
copyright="© 2026 Shop. All rights reserved."
|
||||
/>
|
||||
</>
|
||||
<Products
|
||||
title="Shopify Hydrogen Storefront"
|
||||
subtitle="An agent-friendly Shopify storefront built with Next.js and Hydrogen."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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';
|
||||
|
||||
export function generateStaticParams() {
|
||||
@@ -33,33 +31,17 @@ export default async function PolicyPage({
|
||||
}
|
||||
|
||||
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">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h1 className="text-3xl md:text-4xl font-normal text-foreground">
|
||||
{policy.title}
|
||||
</h1>
|
||||
|
||||
<main className="max-w-screen-2xl mx-auto w-full px-5 lg:px-10 py-16">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h1 className="text-3xl md:text-4xl font-normal text-foreground">
|
||||
{policy.title}
|
||||
</h1>
|
||||
|
||||
<div
|
||||
className="policy-body mt-8 text-[15px] leading-7 text-foreground"
|
||||
dangerouslySetInnerHTML={{ __html: policy.body }}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer
|
||||
storeName="Shop"
|
||||
copyright="© 2026 Shop. All rights reserved."
|
||||
/>
|
||||
</>
|
||||
<div
|
||||
className="policy-body mt-8 text-[15px] leading-7 text-foreground"
|
||||
dangerouslySetInnerHTML={{ __html: policy.body }}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,11 @@
|
||||
import Header from '@/components/shopify/header';
|
||||
import Footer from '@/components/shopify/footer';
|
||||
import ProductDetail from '@/components/shopify/product-detail';
|
||||
import ProductRecommendations from '@/components/shopify/product-recommendations';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
storeName="Shop"
|
||||
logoUrl=""
|
||||
links={[
|
||||
{ label: 'Products', url: '/' },
|
||||
{ label: 'Collections', url: '/collections' },
|
||||
]}
|
||||
/>
|
||||
<ProductDetail addToCartLabel="Add to Cart" />
|
||||
<ProductRecommendations title="You May Also Like" />
|
||||
<Footer
|
||||
storeName="Shop"
|
||||
copyright="© 2026 Shop. All rights reserved."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-22
@@ -1,6 +1,4 @@
|
||||
import { Suspense } from 'react';
|
||||
import Header from '@/components/shopify/header';
|
||||
import Footer from '@/components/shopify/footer';
|
||||
import SearchResults from '@/components/shopify/search-results';
|
||||
|
||||
export const metadata = {
|
||||
@@ -9,25 +7,9 @@ export const metadata = {
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<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" />}>
|
||||
<SearchResults />
|
||||
</Suspense>
|
||||
|
||||
<Footer
|
||||
storeName="Shop"
|
||||
copyright="© 2026 Shop. All rights reserved."
|
||||
/>
|
||||
</>
|
||||
// useSearchParams needs a Suspense boundary to prerender this route.
|
||||
<Suspense fallback={<div className="py-10" />}>
|
||||
<SearchResults />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
import Header from '@/components/shopify/header';
|
||||
import Footer from '@/components/shopify/footer';
|
||||
import Collections from '@/components/shopify/collections';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<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."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
return <Collections title="Our Collections" />;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
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<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
|
||||
export function useCollectionProducts(
|
||||
handle: string | null,
|
||||
|
||||
Reference in New Issue
Block a user