mirror of
https://github.com/vercel/commerce.git
synced 2025-05-08 18:57:51 +00:00
feat(cart): cart is now based on store configuration
This commit is contained in:
parent
e20594e95f
commit
86ecab7c3f
@ -102,7 +102,7 @@ export default async function Page({ params }: { params: { ContentLandingPage: s
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="basis-full lg:basis-2/6">
|
<div className="basis-full lg:basis-2/6">
|
||||||
<ProductDescription product={instance.product} />
|
<ProductDescription product={instance.product} store={instance.store} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RelatedProducts id={instance.product.id} store={instance.store} />
|
<RelatedProducts id={instance.product.id} store={instance.store} />
|
||||||
|
12
app/page.tsx
12
app/page.tsx
@ -1,7 +1,3 @@
|
|||||||
import { Carousel } from 'components/carousel';
|
|
||||||
import { ThreeItemGrid } from 'components/grid/three-items';
|
|
||||||
import Footer from 'components/layout/footer';
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
description: 'High-performance ecommerce store built with Next.js, Vercel, and Shopif.',
|
description: 'High-performance ecommerce store built with Next.js, Vercel, and Shopif.',
|
||||||
openGraph: {
|
openGraph: {
|
||||||
@ -10,11 +6,5 @@ export const metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
return (
|
return <></>;
|
||||||
<>
|
|
||||||
<ThreeItemGrid />
|
|
||||||
<Carousel />
|
|
||||||
<Footer />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { getCollections, getPages, getProducts } from 'lib/shopify';
|
|
||||||
import { validateEnvironmentVariables } from 'lib/utils';
|
import { validateEnvironmentVariables } from 'lib/utils';
|
||||||
import { MetadataRoute } from 'next';
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
@ -16,6 +15,7 @@ export const dynamic = 'force-dynamic';
|
|||||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
validateEnvironmentVariables();
|
validateEnvironmentVariables();
|
||||||
|
|
||||||
|
/*
|
||||||
const routesMap = [''].map((route) => ({
|
const routesMap = [''].map((route) => ({
|
||||||
url: `${baseUrl}${route}`,
|
url: `${baseUrl}${route}`,
|
||||||
lastModified: new Date().toISOString()
|
lastModified: new Date().toISOString()
|
||||||
@ -51,4 +51,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [...routesMap, ...fetchedRoutes];
|
return [...routesMap, ...fetchedRoutes];
|
||||||
|
*/
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { getCollectionProducts } from 'lib/shopify';
|
import { getCollectionProducts } from 'lib/shopify';
|
||||||
|
import { Store } from 'lib/shopify/types';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { GridTileImage } from './grid/tile';
|
import { GridTileImage } from './grid/tile';
|
||||||
|
|
||||||
export async function Carousel() {
|
export async function Carousel(store: Store) {
|
||||||
// Collections that start with `hidden-*` are hidden from the search page.
|
// Collections that start with `hidden-*` are hidden from the search page.
|
||||||
const products = await getCollectionProducts({ collection: 'hidden-homepage-carousel' });
|
const products = await getCollectionProducts({ store, collection: 'hidden-homepage-carousel' });
|
||||||
|
|
||||||
if (!products?.length) return null;
|
if (!products?.length) return null;
|
||||||
|
|
||||||
|
@ -2,36 +2,42 @@
|
|||||||
|
|
||||||
import { TAGS } from 'lib/constants';
|
import { TAGS } from 'lib/constants';
|
||||||
import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/shopify';
|
import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/shopify';
|
||||||
|
import { Store } from 'lib/shopify/types';
|
||||||
import { revalidateTag } from 'next/cache';
|
import { revalidateTag } from 'next/cache';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
export async function addItem(prevState: any, selectedVariantId: string | undefined) {
|
export async function addItem(
|
||||||
|
prevState: any,
|
||||||
|
payload: { selectedVariantId: string | undefined; store: Store }
|
||||||
|
) {
|
||||||
let cartId = cookies().get('cartId')?.value;
|
let cartId = cookies().get('cartId')?.value;
|
||||||
let cart;
|
let cart;
|
||||||
|
|
||||||
if (cartId) {
|
if (cartId) {
|
||||||
cart = await getCart(cartId);
|
cart = await getCart(payload.store, cartId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cartId || !cart) {
|
if (!cartId || !cart) {
|
||||||
cart = await createCart();
|
cart = await createCart(payload.store);
|
||||||
cartId = cart.id;
|
cartId = cart.id;
|
||||||
cookies().set('cartId', cartId);
|
cookies().set('cartId', cartId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedVariantId) {
|
if (!payload.selectedVariantId) {
|
||||||
return 'Missing product variant ID';
|
return 'Missing product variant ID';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await addToCart(cartId, [{ merchandiseId: selectedVariantId, quantity: 1 }]);
|
await addToCart(payload.store, cartId, [
|
||||||
|
{ merchandiseId: payload.selectedVariantId, quantity: 1 }
|
||||||
|
]);
|
||||||
revalidateTag(TAGS.cart);
|
revalidateTag(TAGS.cart);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'Error adding item to cart';
|
return 'Error adding item to cart';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeItem(prevState: any, lineId: string) {
|
export async function removeItem(prevState: any, payload: { lineId: string; store: Store }) {
|
||||||
const cartId = cookies().get('cartId')?.value;
|
const cartId = cookies().get('cartId')?.value;
|
||||||
|
|
||||||
if (!cartId) {
|
if (!cartId) {
|
||||||
@ -39,7 +45,7 @@ export async function removeItem(prevState: any, lineId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await removeFromCart(cartId, [lineId]);
|
await removeFromCart(payload.store, cartId, [payload.lineId]);
|
||||||
revalidateTag(TAGS.cart);
|
revalidateTag(TAGS.cart);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'Error removing item from cart';
|
return 'Error removing item from cart';
|
||||||
@ -52,6 +58,7 @@ export async function updateItemQuantity(
|
|||||||
lineId: string;
|
lineId: string;
|
||||||
variantId: string;
|
variantId: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
store: Store;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const cartId = cookies().get('cartId')?.value;
|
const cartId = cookies().get('cartId')?.value;
|
||||||
@ -60,16 +67,16 @@ export async function updateItemQuantity(
|
|||||||
return 'Missing cart ID';
|
return 'Missing cart ID';
|
||||||
}
|
}
|
||||||
|
|
||||||
const { lineId, variantId, quantity } = payload;
|
const { lineId, variantId, quantity, store } = payload;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (quantity === 0) {
|
if (quantity === 0) {
|
||||||
await removeFromCart(cartId, [lineId]);
|
await removeFromCart(store, cartId, [lineId]);
|
||||||
revalidateTag(TAGS.cart);
|
revalidateTag(TAGS.cart);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateCart(cartId, [
|
await updateCart(store, cartId, [
|
||||||
{
|
{
|
||||||
id: lineId,
|
id: lineId,
|
||||||
merchandiseId: variantId,
|
merchandiseId: variantId,
|
||||||
|
@ -4,7 +4,7 @@ import { PlusIcon } from '@heroicons/react/24/outline';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { addItem } from 'components/cart/actions';
|
import { addItem } from 'components/cart/actions';
|
||||||
import LoadingDots from 'components/loading-dots';
|
import LoadingDots from 'components/loading-dots';
|
||||||
import { ProductVariant } from 'lib/shopify/types';
|
import { ProductVariant, Store } from 'lib/shopify/types';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useFormState, useFormStatus } from 'react-dom';
|
import { useFormState, useFormStatus } from 'react-dom';
|
||||||
|
|
||||||
@ -64,9 +64,11 @@ function SubmitButton({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AddToCart({
|
export function AddToCart({
|
||||||
|
store,
|
||||||
variants,
|
variants,
|
||||||
availableForSale
|
availableForSale
|
||||||
}: {
|
}: {
|
||||||
|
store: Store;
|
||||||
variants: ProductVariant[];
|
variants: ProductVariant[];
|
||||||
availableForSale: boolean;
|
availableForSale: boolean;
|
||||||
}) {
|
}) {
|
||||||
@ -79,7 +81,7 @@ export function AddToCart({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
const selectedVariantId = variant?.id || defaultVariantId;
|
const selectedVariantId = variant?.id || defaultVariantId;
|
||||||
const actionWithVariant = formAction.bind(null, selectedVariantId);
|
const actionWithVariant = formAction.bind(null, { selectedVariantId, store });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form action={actionWithVariant}>
|
<form action={actionWithVariant}>
|
||||||
|
@ -4,7 +4,7 @@ import { XMarkIcon } from '@heroicons/react/24/outline';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { removeItem } from 'components/cart/actions';
|
import { removeItem } from 'components/cart/actions';
|
||||||
import LoadingDots from 'components/loading-dots';
|
import LoadingDots from 'components/loading-dots';
|
||||||
import type { CartItem } from 'lib/shopify/types';
|
import type { CartItem, Store } from 'lib/shopify/types';
|
||||||
import { useFormState, useFormStatus } from 'react-dom';
|
import { useFormState, useFormStatus } from 'react-dom';
|
||||||
|
|
||||||
function SubmitButton() {
|
function SubmitButton() {
|
||||||
@ -34,10 +34,10 @@ function SubmitButton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DeleteItemButton({ item }: { item: CartItem }) {
|
export function DeleteItemButton({ item, store }: { item: CartItem; store: Store }) {
|
||||||
const [message, formAction] = useFormState(removeItem, null);
|
const [message, formAction] = useFormState(removeItem, null);
|
||||||
const itemId = item.id;
|
const itemId = item.id;
|
||||||
const actionWithVariant = formAction.bind(null, itemId);
|
const actionWithVariant = formAction.bind(null, { lineId: itemId, store });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form action={actionWithVariant}>
|
<form action={actionWithVariant}>
|
||||||
|
@ -4,7 +4,7 @@ import { MinusIcon, PlusIcon } from '@heroicons/react/24/outline';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { updateItemQuantity } from 'components/cart/actions';
|
import { updateItemQuantity } from 'components/cart/actions';
|
||||||
import LoadingDots from 'components/loading-dots';
|
import LoadingDots from 'components/loading-dots';
|
||||||
import type { CartItem } from 'lib/shopify/types';
|
import type { CartItem, Store } from 'lib/shopify/types';
|
||||||
import { useFormState, useFormStatus } from 'react-dom';
|
import { useFormState, useFormStatus } from 'react-dom';
|
||||||
|
|
||||||
function SubmitButton({ type }: { type: 'plus' | 'minus' }) {
|
function SubmitButton({ type }: { type: 'plus' | 'minus' }) {
|
||||||
@ -37,12 +37,21 @@ function SubmitButton({ type }: { type: 'plus' | 'minus' }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditItemQuantityButton({ item, type }: { item: CartItem; type: 'plus' | 'minus' }) {
|
export function EditItemQuantityButton({
|
||||||
|
item,
|
||||||
|
type,
|
||||||
|
store
|
||||||
|
}: {
|
||||||
|
item: CartItem;
|
||||||
|
type: 'plus' | 'minus';
|
||||||
|
store: Store;
|
||||||
|
}) {
|
||||||
const [message, formAction] = useFormState(updateItemQuantity, null);
|
const [message, formAction] = useFormState(updateItemQuantity, null);
|
||||||
const payload = {
|
const payload = {
|
||||||
lineId: item.id,
|
lineId: item.id,
|
||||||
variantId: item.merchandise.id,
|
variantId: item.merchandise.id,
|
||||||
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1
|
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1,
|
||||||
|
store
|
||||||
};
|
};
|
||||||
const actionWithVariant = formAction.bind(null, payload);
|
const actionWithVariant = formAction.bind(null, payload);
|
||||||
|
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { getCart } from 'lib/shopify';
|
import { getCart } from 'lib/shopify';
|
||||||
|
import { Store } from 'lib/shopify/types';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import CartModal from './modal';
|
import CartModal from './modal';
|
||||||
|
|
||||||
export default async function Cart() {
|
export default async function Cart(store: Store) {
|
||||||
const cartId = cookies().get('cartId')?.value;
|
const cartId = cookies().get('cartId')?.value;
|
||||||
let cart;
|
let cart;
|
||||||
|
|
||||||
if (cartId) {
|
if (cartId) {
|
||||||
cart = await getCart(cartId);
|
cart = await getCart(store, cartId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <CartModal cart={cart} />;
|
return <CartModal cart={cart} store={store} />;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { Dialog, Transition } from '@headlessui/react';
|
|||||||
import { ShoppingCartIcon } from '@heroicons/react/24/outline';
|
import { ShoppingCartIcon } from '@heroicons/react/24/outline';
|
||||||
import Price from 'components/price';
|
import Price from 'components/price';
|
||||||
import { DEFAULT_OPTION } from 'lib/constants';
|
import { DEFAULT_OPTION } from 'lib/constants';
|
||||||
import type { Cart } from 'lib/shopify/types';
|
import type { Cart, Store } from 'lib/shopify/types';
|
||||||
import { createUrl } from 'lib/utils';
|
import { createUrl } from 'lib/utils';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@ -18,7 +18,7 @@ type MerchandiseSearchParams = {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
export default function CartModal({ cart, store }: { cart: Cart | undefined; store: Store }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const quantityRef = useRef(cart?.totalQuantity);
|
const quantityRef = useRef(cart?.totalQuantity);
|
||||||
const openCart = () => setIsOpen(true);
|
const openCart = () => setIsOpen(true);
|
||||||
@ -102,7 +102,7 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
|||||||
>
|
>
|
||||||
<div className="relative flex w-full flex-row justify-between px-1 py-4">
|
<div className="relative flex w-full flex-row justify-between px-1 py-4">
|
||||||
<div className="absolute z-40 -mt-2 ml-[55px]">
|
<div className="absolute z-40 -mt-2 ml-[55px]">
|
||||||
<DeleteItemButton item={item} />
|
<DeleteItemButton item={item} store={store} />
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href={merchandiseUrl}
|
href={merchandiseUrl}
|
||||||
@ -140,11 +140,11 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
|||||||
currencyCode={item.cost.totalAmount.currencyCode}
|
currencyCode={item.cost.totalAmount.currencyCode}
|
||||||
/>
|
/>
|
||||||
<div className="ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-200 dark:border-neutral-700">
|
<div className="ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-200 dark:border-neutral-700">
|
||||||
<EditItemQuantityButton item={item} type="minus" />
|
<EditItemQuantityButton item={item} store={store} type="minus" />
|
||||||
<p className="w-6 text-center">
|
<p className="w-6 text-center">
|
||||||
<span className="w-full text-sm">{item.quantity}</span>
|
<span className="w-full text-sm">{item.quantity}</span>
|
||||||
</p>
|
</p>
|
||||||
<EditItemQuantityButton item={item} type="plus" />
|
<EditItemQuantityButton item={item} store={store} type="plus" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { GridTileImage } from 'components/grid/tile';
|
import { GridTileImage } from 'components/grid/tile';
|
||||||
import { getCollectionProducts } from 'lib/shopify';
|
import { getCollectionProducts } from 'lib/shopify';
|
||||||
import type { Product } from 'lib/shopify/types';
|
import type { Product, Store } from 'lib/shopify/types';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
function ThreeItemGridItem({
|
function ThreeItemGridItem({
|
||||||
@ -37,9 +37,10 @@ function ThreeItemGridItem({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ThreeItemGrid() {
|
export async function ThreeItemGrid({ store }: { store: Store }) {
|
||||||
// Collections that start with `hidden-*` are hidden from the search page.
|
// Collections that start with `hidden-*` are hidden from the search page.
|
||||||
const homepageItems = await getCollectionProducts({
|
const homepageItems = await getCollectionProducts({
|
||||||
|
store,
|
||||||
collection: 'hidden-homepage-featured-items'
|
collection: 'hidden-homepage-featured-items'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import FooterMenu from 'components/layout/footer-menu';
|
|
||||||
import LogoSquare from 'components/logo-square';
|
import LogoSquare from 'components/logo-square';
|
||||||
import { getMenu } from 'lib/shopify';
|
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
const { COMPANY_NAME, SITE_NAME } = process.env;
|
const { COMPANY_NAME, SITE_NAME } = process.env;
|
||||||
@ -11,7 +9,6 @@ export default async function Footer() {
|
|||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : '');
|
const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : '');
|
||||||
const skeleton = 'w-full h-6 animate-pulse rounded bg-neutral-200 dark:bg-neutral-700';
|
const skeleton = 'w-full h-6 animate-pulse rounded bg-neutral-200 dark:bg-neutral-700';
|
||||||
const menu = await getMenu('next-js-frontend-footer-menu');
|
|
||||||
const copyrightName = COMPANY_NAME || SITE_NAME || '';
|
const copyrightName = COMPANY_NAME || SITE_NAME || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -34,9 +31,7 @@ export default async function Footer() {
|
|||||||
<div className={skeleton} />
|
<div className={skeleton} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
></Suspense>
|
||||||
<FooterMenu menu={menu} />
|
|
||||||
</Suspense>
|
|
||||||
<div className="md:ml-auto">
|
<div className="md:ml-auto">
|
||||||
<a
|
<a
|
||||||
className="flex h-8 w-max flex-none items-center justify-center rounded-md border border-neutral-200 bg-white text-xs text-black dark:border-neutral-700 dark:bg-black dark:text-white"
|
className="flex h-8 w-max flex-none items-center justify-center rounded-md border border-neutral-200 bg-white text-xs text-black dark:border-neutral-700 dark:bg-black dark:text-white"
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
import Cart from 'components/cart';
|
|
||||||
import OpenCart from 'components/cart/open-cart';
|
|
||||||
import LogoSquare from 'components/logo-square';
|
|
||||||
import { getMenu } from 'lib/shopify';
|
|
||||||
import { Menu } from 'lib/shopify/types';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { Suspense } from 'react';
|
|
||||||
import MobileMenu from './mobile-menu';
|
|
||||||
import Search, { SearchSkeleton } from './search';
|
|
||||||
const { SITE_NAME } = process.env;
|
|
||||||
|
|
||||||
export default async function Navbar() {
|
|
||||||
const menu = await getMenu('next-js-frontend-header-menu');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className="relative flex items-center justify-between p-4 lg:px-6">
|
|
||||||
<div className="block flex-none md:hidden">
|
|
||||||
<Suspense fallback={null}>
|
|
||||||
<MobileMenu menu={menu} />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full items-center">
|
|
||||||
<div className="flex w-full md:w-1/3">
|
|
||||||
<Link href="/" className="mr-2 flex w-full items-center justify-center md:w-auto lg:mr-6">
|
|
||||||
<LogoSquare />
|
|
||||||
<div className="ml-2 flex-none text-sm font-medium uppercase md:hidden lg:block">
|
|
||||||
{SITE_NAME}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
{menu.length ? (
|
|
||||||
<ul className="hidden gap-6 text-sm md:flex md:items-center">
|
|
||||||
{menu.map((item: Menu) => (
|
|
||||||
<li key={item.title}>
|
|
||||||
<Link
|
|
||||||
href={item.path}
|
|
||||||
className="text-neutral-500 underline-offset-4 hover:text-black hover:underline dark:text-neutral-400 dark:hover:text-neutral-300"
|
|
||||||
>
|
|
||||||
{item.title}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="hidden justify-center md:flex md:w-1/3">
|
|
||||||
<Suspense fallback={<SearchSkeleton />}>
|
|
||||||
<Search />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end md:w-1/3">
|
|
||||||
<Suspense fallback={<OpenCart />}>
|
|
||||||
<Cart />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Dialog, Transition } from '@headlessui/react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { usePathname, useSearchParams } from 'next/navigation';
|
|
||||||
import { Fragment, Suspense, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
|
|
||||||
import { Menu } from 'lib/shopify/types';
|
|
||||||
import Search, { SearchSkeleton } from './search';
|
|
||||||
|
|
||||||
export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
|
||||||
const pathname = usePathname();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const openMobileMenu = () => setIsOpen(true);
|
|
||||||
const closeMobileMenu = () => setIsOpen(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
if (window.innerWidth > 768) {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
return () => window.removeEventListener('resize', handleResize);
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsOpen(false);
|
|
||||||
}, [pathname, searchParams]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={openMobileMenu}
|
|
||||||
aria-label="Open mobile menu"
|
|
||||||
className="flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors md:hidden dark:border-neutral-700 dark:text-white"
|
|
||||||
>
|
|
||||||
<Bars3Icon className="h-4" />
|
|
||||||
</button>
|
|
||||||
<Transition show={isOpen}>
|
|
||||||
<Dialog onClose={closeMobileMenu} className="relative z-50">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition-all ease-in-out duration-300"
|
|
||||||
enterFrom="opacity-0 backdrop-blur-none"
|
|
||||||
enterTo="opacity-100 backdrop-blur-[.5px]"
|
|
||||||
leave="transition-all ease-in-out duration-200"
|
|
||||||
leaveFrom="opacity-100 backdrop-blur-[.5px]"
|
|
||||||
leaveTo="opacity-0 backdrop-blur-none"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
|
|
||||||
</Transition.Child>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition-all ease-in-out duration-300"
|
|
||||||
enterFrom="translate-x-[-100%]"
|
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transition-all ease-in-out duration-200"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="translate-x-[-100%]"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="fixed bottom-0 left-0 right-0 top-0 flex h-full w-full flex-col bg-white pb-6 dark:bg-black">
|
|
||||||
<div className="p-4">
|
|
||||||
<button
|
|
||||||
className="mb-4 flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white"
|
|
||||||
onClick={closeMobileMenu}
|
|
||||||
aria-label="Close mobile menu"
|
|
||||||
>
|
|
||||||
<XMarkIcon className="h-6" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="mb-4 w-full">
|
|
||||||
<Suspense fallback={<SearchSkeleton />}>
|
|
||||||
<Search />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
{menu.length ? (
|
|
||||||
<ul className="flex w-full flex-col">
|
|
||||||
{menu.map((item: Menu) => (
|
|
||||||
<li
|
|
||||||
className="py-2 text-xl text-black transition-colors hover:text-neutral-500 dark:text-white"
|
|
||||||
key={item.title}
|
|
||||||
>
|
|
||||||
<Link href={item.path} onClick={closeMobileMenu}>
|
|
||||||
{item.title}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</Dialog>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
|
||||||
import { createUrl } from 'lib/utils';
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
|
||||||
|
|
||||||
export default function Search() {
|
|
||||||
const router = useRouter();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const val = e.target as HTMLFormElement;
|
|
||||||
const search = val.search as HTMLInputElement;
|
|
||||||
const newParams = new URLSearchParams(searchParams.toString());
|
|
||||||
|
|
||||||
if (search.value) {
|
|
||||||
newParams.set('q', search.value);
|
|
||||||
} else {
|
|
||||||
newParams.delete('q');
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push(createUrl('/search', newParams));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={onSubmit} className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
|
|
||||||
<input
|
|
||||||
key={searchParams?.get('q')}
|
|
||||||
type="text"
|
|
||||||
name="search"
|
|
||||||
placeholder="Search for products..."
|
|
||||||
autoComplete="off"
|
|
||||||
defaultValue={searchParams?.get('q') || ''}
|
|
||||||
className="w-full rounded-lg border bg-white px-4 py-2 text-sm text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
|
|
||||||
<MagnifyingGlassIcon className="h-4" />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SearchSkeleton() {
|
|
||||||
return (
|
|
||||||
<form className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
|
|
||||||
<input
|
|
||||||
placeholder="Search for products..."
|
|
||||||
className="w-full rounded-lg border bg-white px-4 py-2 text-sm text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400"
|
|
||||||
/>
|
|
||||||
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
|
|
||||||
<MagnifyingGlassIcon className="h-4" />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
@ -2,10 +2,11 @@ import clsx from 'clsx';
|
|||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
import { getCollections } from 'lib/shopify';
|
import { getCollections } from 'lib/shopify';
|
||||||
|
import { Store } from 'lib/shopify/types';
|
||||||
import FilterList from './filter';
|
import FilterList from './filter';
|
||||||
|
|
||||||
async function CollectionList() {
|
async function CollectionList({ store }: { store: Store }) {
|
||||||
const collections = await getCollections();
|
const collections = await getCollections(store);
|
||||||
return <FilterList list={collections} title="Collections" />;
|
return <FilterList list={collections} title="Collections" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ const skeleton = 'mb-3 h-4 w-5/6 animate-pulse rounded';
|
|||||||
const activeAndTitles = 'bg-neutral-800 dark:bg-neutral-300';
|
const activeAndTitles = 'bg-neutral-800 dark:bg-neutral-300';
|
||||||
const items = 'bg-neutral-400 dark:bg-neutral-700';
|
const items = 'bg-neutral-400 dark:bg-neutral-700';
|
||||||
|
|
||||||
export default function Collections() {
|
export default function Collections(store: Store) {
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
@ -31,7 +32,7 @@ export default function Collections() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CollectionList />
|
<CollectionList store={store} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { AddToCart } from 'components/cart/add-to-cart';
|
import { AddToCart } from 'components/cart/add-to-cart';
|
||||||
import Price from 'components/price';
|
import Price from 'components/price';
|
||||||
import Prose from 'components/prose';
|
import Prose from 'components/prose';
|
||||||
import { Product } from 'lib/shopify/types';
|
import { Product, Store } from 'lib/shopify/types';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { VariantSelector } from './variant-selector';
|
import { VariantSelector } from './variant-selector';
|
||||||
|
|
||||||
export function ProductDescription({ product }: { product: Product }) {
|
export function ProductDescription({ product, store }: { product: Product; store: Store }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-6 flex flex-col border-b pb-6 dark:border-neutral-700">
|
<div className="mb-6 flex flex-col border-b pb-6 dark:border-neutral-700">
|
||||||
@ -29,7 +29,11 @@ export function ProductDescription({ product }: { product: Product }) {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<AddToCart variants={product.variants} availableForSale={product.availableForSale} />
|
<AddToCart
|
||||||
|
variants={product.variants}
|
||||||
|
availableForSale={product.availableForSale}
|
||||||
|
store={store}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user