refactoring

This commit is contained in:
Timur Suleymanov 2024-06-03 18:51:37 +05:00
parent 34845604e5
commit e496235adc
161 changed files with 1353 additions and 5468 deletions

View File

@ -1,40 +0,0 @@
import { getCollectionProducts } from 'lib/shopify';
import Link from 'next/link';
import { GridTileImage } from './grid/tile';
export async function Carousel() {
// Collections that start with `hidden-*` are hidden from the search page.
const products = await getCollectionProducts({ collection: 'hidden-homepage-carousel' });
if (!products?.length) return null;
// Purposefully duplicating products to make the carousel loop and not run out of products on wide screens.
const carouselProducts = [...products, ...products, ...products];
return (
<div className=" w-full overflow-x-auto pb-6 pt-1">
<ul className="flex animate-carousel gap-4">
{carouselProducts.map((product, i) => (
<li
key={`${product.handle}${i}`}
className="relative aspect-square h-[30vh] max-h-[275px] w-2/3 max-w-[475px] flex-none md:w-1/3"
>
<Link href={`/product/${product.handle}`} className="relative h-full w-full">
<GridTileImage
alt={product.title}
label={{
title: product.title,
amount: product.priceRange.maxVariantPrice.amount,
currencyCode: product.priceRange.maxVariantPrice.currencyCode
}}
src={product.featuredImage?.url}
fill
sizes="(min-width: 1024px) 25vw, (min-width: 768px) 33vw, 50vw"
/>
</Link>
</li>
))}
</ul>
</div>
);
}

View File

@ -1,83 +0,0 @@
'use server';
import { TAGS } from 'lib/constants';
import { addToCart, createCart, getCart, removeFromCart, updateCart } from 'lib/shopify';
import { revalidateTag } from 'next/cache';
import { cookies } from 'next/headers';
export async function addItem(prevState: any, selectedVariantId: string | undefined) {
let cartId = cookies().get('cartId')?.value;
let cart;
if (cartId) {
cart = await getCart(cartId);
}
if (!cartId || !cart) {
cart = await createCart();
cartId = cart.id;
cookies().set('cartId', cartId);
}
if (!selectedVariantId) {
return 'Missing product variant ID';
}
try {
await addToCart(cartId, [{ merchandiseId: selectedVariantId, quantity: 1 }]);
revalidateTag(TAGS.cart);
} catch (e) {
return 'Error adding item to cart';
}
}
export async function removeItem(prevState: any, lineId: string) {
const cartId = cookies().get('cartId')?.value;
if (!cartId) {
return 'Missing cart ID';
}
try {
await removeFromCart(cartId, [lineId]);
revalidateTag(TAGS.cart);
} catch (e) {
return 'Error removing item from cart';
}
}
export async function updateItemQuantity(
prevState: any,
payload: {
lineId: string;
variantId: string;
quantity: number;
}
) {
const cartId = cookies().get('cartId')?.value;
if (!cartId) {
return 'Missing cart ID';
}
const { lineId, variantId, quantity } = payload;
try {
if (quantity === 0) {
await removeFromCart(cartId, [lineId]);
revalidateTag(TAGS.cart);
return;
}
await updateCart(cartId, [
{
id: lineId,
merchandiseId: variantId,
quantity
}
]);
revalidateTag(TAGS.cart);
} catch (e) {
return 'Error updating item quantity';
}
}

View File

@ -1,92 +0,0 @@
'use client';
import { PlusIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import { addItem } from 'components/cart/actions';
import LoadingDots from 'components/loading-dots';
import { ProductVariant } from 'lib/shopify/types';
import { useSearchParams } from 'next/navigation';
import { useFormState, useFormStatus } from 'react-dom';
function SubmitButton({
availableForSale,
selectedVariantId
}: {
availableForSale: boolean;
selectedVariantId: string | undefined;
}) {
const { pending } = useFormStatus();
const buttonClasses =
'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white';
const disabledClasses = 'cursor-not-allowed opacity-60 hover:opacity-60';
if (!availableForSale) {
return (
<button aria-disabled className={clsx(buttonClasses, disabledClasses)}>
Out Of Stock
</button>
);
}
if (!selectedVariantId) {
return (
<button
aria-label="Please select an option"
aria-disabled
className={clsx(buttonClasses, disabledClasses)}
>
<div className="absolute left-0 ml-4">
<PlusIcon className="h-5" />
</div>
Add To Cart
</button>
);
}
return (
<button
onClick={(e: React.FormEvent<HTMLButtonElement>) => {
if (pending) e.preventDefault();
}}
aria-label="Add to cart"
aria-disabled={pending}
className={clsx(buttonClasses, {
'hover:opacity-90': true,
[disabledClasses]: pending
})}
>
<div className="absolute left-0 ml-4">
{pending ? <LoadingDots className="mb-3 bg-white" /> : <PlusIcon className="h-5" />}
</div>
Add To Cart
</button>
);
}
export function AddToCart({
variants,
availableForSale
}: {
variants: ProductVariant[];
availableForSale: boolean;
}) {
const [message, formAction] = useFormState(addItem, null);
const searchParams = useSearchParams();
const defaultVariantId = variants.length === 1 ? variants[0]?.id : undefined;
const variant = variants.find((variant: ProductVariant) =>
variant.selectedOptions.every(
(option) => option.value === searchParams.get(option.name.toLowerCase())
)
);
const selectedVariantId = variant?.id || defaultVariantId;
const actionWithVariant = formAction.bind(null, selectedVariantId);
return (
<form action={actionWithVariant}>
<SubmitButton availableForSale={availableForSale} selectedVariantId={selectedVariantId} />
<p aria-live="polite" className="sr-only" role="status">
{message}
</p>
</form>
);
}

View File

@ -1,10 +0,0 @@
import { XMarkIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
export default function CloseCart({ className }: { className?: string }) {
return (
<div className="relative 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">
<XMarkIcon className={clsx('h-6 transition-all ease-in-out hover:scale-110 ', className)} />
</div>
);
}

View File

@ -1,50 +0,0 @@
'use client';
import { XMarkIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import { removeItem } from 'components/cart/actions';
import LoadingDots from 'components/loading-dots';
import type { CartItem } from 'lib/shopify/types';
import { useFormState, useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
onClick={(e: React.FormEvent<HTMLButtonElement>) => {
if (pending) e.preventDefault();
}}
aria-label="Remove cart item"
aria-disabled={pending}
className={clsx(
'ease flex h-[17px] w-[17px] items-center justify-center rounded-full bg-neutral-500 transition-all duration-200',
{
'cursor-not-allowed px-0': pending
}
)}
>
{pending ? (
<LoadingDots className="bg-white" />
) : (
<XMarkIcon className="hover:text-accent-3 mx-[1px] h-4 w-4 text-white dark:text-black" />
)}
</button>
);
}
export function DeleteItemButton({ item }: { item: CartItem }) {
const [message, formAction] = useFormState(removeItem, null);
const itemId = item.id;
const actionWithVariant = formAction.bind(null, itemId);
return (
<form action={actionWithVariant}>
<SubmitButton />
<p aria-live="polite" className="sr-only" role="status">
{message}
</p>
</form>
);
}

View File

@ -1,57 +0,0 @@
'use client';
import { MinusIcon, PlusIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import { updateItemQuantity } from 'components/cart/actions';
import LoadingDots from 'components/loading-dots';
import type { CartItem } from 'lib/shopify/types';
import { useFormState, useFormStatus } from 'react-dom';
function SubmitButton({ type }: { type: 'plus' | 'minus' }) {
const { pending } = useFormStatus();
return (
<button
type="submit"
onClick={(e: React.FormEvent<HTMLButtonElement>) => {
if (pending) e.preventDefault();
}}
aria-label={type === 'plus' ? 'Increase item quantity' : 'Reduce item quantity'}
aria-disabled={pending}
className={clsx(
'ease flex h-full min-w-[36px] max-w-[36px] flex-none items-center justify-center rounded-full px-2 transition-all duration-200 hover:border-neutral-800 hover:opacity-80',
{
'cursor-not-allowed': pending,
'ml-auto': type === 'minus'
}
)}
>
{pending ? (
<LoadingDots className="bg-black dark:bg-white" />
) : type === 'plus' ? (
<PlusIcon className="h-4 w-4 dark:text-neutral-500" />
) : (
<MinusIcon className="h-4 w-4 dark:text-neutral-500" />
)}
</button>
);
}
export function EditItemQuantityButton({ item, type }: { item: CartItem; type: 'plus' | 'minus' }) {
const [message, formAction] = useFormState(updateItemQuantity, null);
const payload = {
lineId: item.id,
variantId: item.merchandise.id,
quantity: type === 'plus' ? item.quantity + 1 : item.quantity - 1
};
const actionWithVariant = formAction.bind(null, payload);
return (
<form action={actionWithVariant}>
<SubmitButton type={type} />
<p aria-live="polite" className="sr-only" role="status">
{message}
</p>
</form>
);
}

View File

@ -1,14 +0,0 @@
import { getCart } from 'lib/shopify';
import { cookies } from 'next/headers';
import CartModal from './modal';
export default async function Cart() {
const cartId = cookies().get('cartId')?.value;
let cart;
if (cartId) {
cart = await getCart(cartId);
}
return <CartModal cart={cart} />;
}

View File

@ -1,191 +0,0 @@
'use client';
import { Dialog, Transition } from '@headlessui/react';
import { ShoppingCartIcon } from '@heroicons/react/24/outline';
import Price from 'components/price';
import { DEFAULT_OPTION } from 'lib/constants';
import type { Cart } from 'lib/shopify/types';
import { createUrl } from 'lib/utils';
import Image from 'next/image';
import Link from 'next/link';
import { Fragment, useEffect, useRef, useState } from 'react';
import CloseCart from './close-cart';
import { DeleteItemButton } from './delete-item-button';
import { EditItemQuantityButton } from './edit-item-quantity-button';
import OpenCart from './open-cart';
type MerchandiseSearchParams = {
[key: string]: string;
};
export default function CartModal({ cart }: { cart: Cart | undefined }) {
const [isOpen, setIsOpen] = useState(false);
const quantityRef = useRef(cart?.totalQuantity);
const openCart = () => setIsOpen(true);
const closeCart = () => setIsOpen(false);
useEffect(() => {
// Open cart modal when quantity changes.
if (cart?.totalQuantity !== quantityRef.current) {
// But only if it's not already open (quantity also changes when editing items in cart).
if (!isOpen) {
setIsOpen(true);
}
// Always update the quantity reference
quantityRef.current = cart?.totalQuantity;
}
}, [isOpen, cart?.totalQuantity, quantityRef]);
return (
<>
<button aria-label="Open cart" onClick={openCart}>
<OpenCart quantity={cart?.totalQuantity} />
</button>
<Transition show={isOpen}>
<Dialog onClose={closeCart} 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-full"
enterTo="translate-x-0"
leave="transition-all ease-in-out duration-200"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<Dialog.Panel className="fixed bottom-0 right-0 top-0 flex h-full w-full flex-col border-l border-neutral-200 bg-white/80 p-6 text-black backdrop-blur-xl md:w-[390px] dark:border-neutral-700 dark:bg-black/80 dark:text-white">
<div className="flex items-center justify-between">
<p className="text-lg font-semibold">My Cart</p>
<button aria-label="Close cart" onClick={closeCart}>
<CloseCart />
</button>
</div>
{!cart || cart.lines.length === 0 ? (
<div className="mt-20 flex w-full flex-col items-center justify-center overflow-hidden">
<ShoppingCartIcon className="h-16" />
<p className="mt-6 text-center text-2xl font-bold">Your cart is empty.</p>
</div>
) : (
<div className="flex h-full flex-col justify-between overflow-hidden p-1">
<ul className="flex-grow overflow-auto py-4">
{cart.lines.map((item, i) => {
const merchandiseSearchParams = {} as MerchandiseSearchParams;
item.merchandise.selectedOptions.forEach(({ name, value }) => {
if (value !== DEFAULT_OPTION) {
merchandiseSearchParams[name.toLowerCase()] = value;
}
});
const merchandiseUrl = createUrl(
`/product/${item.merchandise.product.handle}`,
new URLSearchParams(merchandiseSearchParams)
);
return (
<li
key={i}
className="flex w-full flex-col border-b border-neutral-300 dark:border-neutral-700"
>
<div className="relative flex w-full flex-row justify-between px-1 py-4">
<div className="absolute z-40 -mt-2 ml-[55px]">
<DeleteItemButton item={item} />
</div>
<Link
href={merchandiseUrl}
onClick={closeCart}
className="z-30 flex flex-row space-x-4"
>
<div className="relative h-16 w-16 cursor-pointer overflow-hidden rounded-md border border-neutral-300 bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800">
<Image
className="h-full w-full object-cover"
width={64}
height={64}
alt={
item.merchandise.product.featuredImage.altText ||
item.merchandise.product.title
}
src={item.merchandise.product.featuredImage.url}
/>
</div>
<div className="flex flex-1 flex-col text-base">
<span className="leading-tight">
{item.merchandise.product.title}
</span>
{item.merchandise.title !== DEFAULT_OPTION ? (
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{item.merchandise.title}
</p>
) : null}
</div>
</Link>
<div className="flex h-16 flex-col justify-between">
<Price
className="flex justify-end space-y-2 text-right text-sm"
amount={item.cost.totalAmount.amount}
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">
<EditItemQuantityButton item={item} type="minus" />
<p className="w-6 text-center">
<span className="w-full text-sm">{item.quantity}</span>
</p>
<EditItemQuantityButton item={item} type="plus" />
</div>
</div>
</div>
</li>
);
})}
</ul>
<div className="py-4 text-sm text-neutral-500 dark:text-neutral-400">
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 dark:border-neutral-700">
<p>Taxes</p>
<Price
className="text-right text-base text-black dark:text-white"
amount={cart.cost.totalTaxAmount.amount}
currencyCode={cart.cost.totalTaxAmount.currencyCode}
/>
</div>
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700">
<p>Shipping</p>
<p className="text-right">Calculated at checkout</p>
</div>
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700">
<p>Total</p>
<Price
className="text-right text-base text-black dark:text-white"
amount={cart.cost.totalAmount.amount}
currencyCode={cart.cost.totalAmount.currencyCode}
/>
</div>
</div>
<a
href={cart.checkoutUrl}
className="block w-full rounded-full bg-blue-600 p-3 text-center text-sm font-medium text-white opacity-90 hover:opacity-100"
>
Proceed to Checkout
</a>
</div>
)}
</Dialog.Panel>
</Transition.Child>
</Dialog>
</Transition>
</>
);
}

View File

@ -1,24 +0,0 @@
import { ShoppingCartIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
export default function OpenCart({
className,
quantity
}: {
className?: string;
quantity?: number;
}) {
return (
<div className="relative 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">
<ShoppingCartIcon
className={clsx('h-4 transition-all ease-in-out hover:scale-110 ', className)}
/>
{quantity ? (
<div className="absolute right-0 top-0 -mr-2 -mt-2 h-4 w-4 rounded bg-blue-600 text-[11px] font-medium text-white">
{quantity}
</div>
) : null}
</div>
);
}

View File

@ -1,21 +0,0 @@
import clsx from 'clsx';
function Grid(props: React.ComponentProps<'ul'>) {
return (
<ul {...props} className={clsx('grid grid-flow-row gap-4', props.className)}>
{props.children}
</ul>
);
}
function GridItem(props: React.ComponentProps<'li'>) {
return (
<li {...props} className={clsx('aspect-square transition-opacity', props.className)}>
{props.children}
</li>
);
}
Grid.Item = GridItem;
export default Grid;

View File

@ -1,57 +0,0 @@
import { GridTileImage } from 'components/grid/tile';
import { getCollectionProducts } from 'lib/shopify';
import type { Product } from 'lib/shopify/types';
import Link from 'next/link';
function ThreeItemGridItem({
item,
size,
priority
}: {
item: Product;
size: 'full' | 'half';
priority?: boolean;
}) {
return (
<div
className={size === 'full' ? 'md:col-span-4 md:row-span-2' : 'md:col-span-2 md:row-span-1'}
>
<Link className="relative block aspect-square h-full w-full" href={`/product/${item.handle}`}>
<GridTileImage
src={item.featuredImage.url}
fill
sizes={
size === 'full' ? '(min-width: 768px) 66vw, 100vw' : '(min-width: 768px) 33vw, 100vw'
}
priority={priority}
alt={item.title}
label={{
position: size === 'full' ? 'center' : 'bottom',
title: item.title as string,
amount: item.priceRange.maxVariantPrice.amount,
currencyCode: item.priceRange.maxVariantPrice.currencyCode
}}
/>
</Link>
</div>
);
}
export async function ThreeItemGrid() {
// Collections that start with `hidden-*` are hidden from the search page.
const homepageItems = await getCollectionProducts({
collection: 'hidden-homepage-featured-items'
});
if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null;
const [firstProduct, secondProduct, thirdProduct] = homepageItems;
return (
<section className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2">
<ThreeItemGridItem size="full" item={firstProduct} priority={true} />
<ThreeItemGridItem size="half" item={secondProduct} priority={true} />
<ThreeItemGridItem size="half" item={thirdProduct} />
</section>
);
}

View File

@ -1,50 +0,0 @@
import clsx from 'clsx';
import Image from 'next/image';
import Label from '../label';
export function GridTileImage({
isInteractive = true,
active,
label,
...props
}: {
isInteractive?: boolean;
active?: boolean;
label?: {
title: string;
amount: string;
currencyCode: string;
position?: 'bottom' | 'center';
};
} & React.ComponentProps<typeof Image>) {
return (
<div
className={clsx(
'group flex h-full w-full items-center justify-center overflow-hidden rounded-lg border bg-white hover:border-blue-600 dark:bg-black',
{
relative: label,
'border-2 border-blue-600': active,
'border-neutral-200 dark:border-neutral-800': !active
}
)}
>
{props.src ? (
// eslint-disable-next-line jsx-a11y/alt-text -- `alt` is inherited from `props`, which is being enforced with TypeScript
<Image
className={clsx('relative h-full w-full object-contain', {
'transition duration-300 ease-in-out group-hover:scale-105': isInteractive
})}
{...props}
/>
) : null}
{label ? (
<Label
title={label.title}
amount={label.amount}
currencyCode={label.currencyCode}
position={label.position}
/>
) : null}
</div>
);
}

View File

@ -1,16 +0,0 @@
import clsx from 'clsx';
export default function LogoIcon(props: React.ComponentProps<'svg'>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
aria-label={`${process.env.SITE_NAME} logo`}
viewBox="0 0 32 28"
{...props}
className={clsx('h-4 w-4 fill-black dark:fill-white', props.className)}
>
<path d="M21.5758 9.75769L16 0L0 28H11.6255L21.5758 9.75769Z" />
<path d="M26.2381 17.9167L20.7382 28H32L26.2381 17.9167Z" />
</svg>
);
}

View File

@ -1,34 +0,0 @@
import clsx from 'clsx';
import Price from './price';
const Label = ({
title,
amount,
currencyCode,
position = 'bottom'
}: {
title: string;
amount: string;
currencyCode: string;
position?: 'bottom' | 'center';
}) => {
return (
<div
className={clsx('absolute bottom-0 left-0 flex w-full px-4 pb-4 @container/label', {
'lg:px-20 lg:pb-[35%]': position === 'center'
})}
>
<div className="flex items-center rounded-full border bg-white/70 p-1 text-xs font-semibold text-black backdrop-blur-md dark:border-neutral-800 dark:bg-black/70 dark:text-white">
<h3 className="mr-4 line-clamp-2 flex-grow pl-2 leading-none tracking-tight">{title}</h3>
<Price
className="flex-none rounded-full bg-blue-600 p-2 text-white"
amount={amount}
currencyCode={currencyCode}
currencyCodeClassName="hidden @[275px]/label:inline"
/>
</div>
</div>
);
};
export default Label;

View File

@ -1,46 +0,0 @@
'use client';
import clsx from 'clsx';
import { Menu } from 'lib/shopify/types';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
const FooterMenuItem = ({ item }: { item: Menu }) => {
const pathname = usePathname();
const [active, setActive] = useState(pathname === item.path);
useEffect(() => {
setActive(pathname === item.path);
}, [pathname, item.path]);
return (
<li>
<Link
href={item.path}
className={clsx(
'block p-2 text-lg underline-offset-4 hover:text-black hover:underline md:inline-block md:text-sm dark:hover:text-neutral-300',
{
'text-black dark:text-neutral-300': active
}
)}
>
{item.title}
</Link>
</li>
);
};
export default function FooterMenu({ menu }: { menu: Menu[] }) {
if (!menu.length) return null;
return (
<nav>
<ul>
{menu.map((item: Menu) => {
return <FooterMenuItem key={item.title} item={item} />;
})}
</ul>
</nav>
);
}

View File

@ -1,69 +0,0 @@
import Link from 'next/link';
import FooterMenu from 'components/layout/footer-menu';
import LogoSquare from 'components/logo-square';
import { getMenu } from 'lib/shopify';
import { Suspense } from 'react';
const { COMPANY_NAME, SITE_NAME } = process.env;
export default async function Footer() {
const currentYear = new Date().getFullYear();
const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : '');
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 || '';
return (
<footer className="text-sm text-neutral-500 dark:text-neutral-400">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6 border-t border-neutral-200 px-6 py-12 text-sm md:flex-row md:gap-12 md:px-4 min-[1320px]:px-0 dark:border-neutral-700">
<div>
<Link className="flex items-center gap-2 text-black md:pt-1 dark:text-white" href="/">
<LogoSquare size="sm" />
<span className="uppercase">{SITE_NAME}</span>
</Link>
</div>
<Suspense
fallback={
<div className="flex h-[188px] w-[200px] flex-col gap-2">
<div className={skeleton} />
<div className={skeleton} />
<div className={skeleton} />
<div className={skeleton} />
<div className={skeleton} />
<div className={skeleton} />
</div>
}
>
<FooterMenu menu={menu} />
</Suspense>
<div className="md:ml-auto">
<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"
aria-label="Deploy on Vercel"
href="https://vercel.com/templates/next.js/nextjs-commerce"
>
<span className="px-3"></span>
<hr className="h-full border-r border-neutral-200 dark:border-neutral-700" />
<span className="px-3">Deploy</span>
</a>
</div>
</div>
<div className="border-t border-neutral-200 py-6 text-sm dark:border-neutral-700">
<div className="mx-auto flex w-full max-w-7xl flex-col items-center gap-1 px-4 md:flex-row md:gap-0 md:px-4 min-[1320px]:px-0">
<p>
&copy; {copyrightDate} {copyrightName}
{copyrightName.length && !copyrightName.endsWith('.') ? '.' : ''} All rights reserved.
</p>
<hr className="mx-4 hidden h-4 w-[1px] border-l border-neutral-400 md:inline-block" />
<p>Designed in California</p>
<p className="md:ml-auto">
<a href="https://vercel.com" className="text-black dark:text-white">
Crafted by Vercel
</a>
</p>
</div>
</div>
</footer>
);
}

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -1,28 +0,0 @@
import Grid from 'components/grid';
import { GridTileImage } from 'components/grid/tile';
import { Product } from 'lib/shopify/types';
import Link from 'next/link';
export default function ProductGridItems({ products }: { products: Product[] }) {
return (
<>
{products.map((product) => (
<Grid.Item key={product.handle} className="animate-fadeIn">
<Link className="relative inline-block h-full w-full" href={`/product/${product.handle}`}>
<GridTileImage
alt={product.title}
label={{
title: product.title,
amount: product.priceRange.maxVariantPrice.amount,
currencyCode: product.priceRange.maxVariantPrice.currencyCode
}}
src={product.featuredImage?.url}
fill
sizes="(min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw"
/>
</Link>
</Grid.Item>
))}
</>
);
}

View File

@ -1,37 +0,0 @@
import clsx from 'clsx';
import { Suspense } from 'react';
import { getCollections } from 'lib/shopify';
import FilterList from './filter';
async function CollectionList() {
const collections = await getCollections();
return <FilterList list={collections} title="Collections" />;
}
const skeleton = 'mb-3 h-4 w-5/6 animate-pulse rounded';
const activeAndTitles = 'bg-neutral-800 dark:bg-neutral-300';
const items = 'bg-neutral-400 dark:bg-neutral-700';
export default function Collections() {
return (
<Suspense
fallback={
<div className="col-span-2 hidden h-[400px] w-full flex-none py-4 lg:block">
<div className={clsx(skeleton, activeAndTitles)} />
<div className={clsx(skeleton, activeAndTitles)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
<div className={clsx(skeleton, items)} />
</div>
}
>
<CollectionList />
</Suspense>
);
}

View File

@ -1,64 +0,0 @@
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { ChevronDownIcon } from '@heroicons/react/24/outline';
import type { ListItem } from '.';
import { FilterItem } from './item';
export default function FilterItemDropdown({ list }: { list: ListItem[] }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const [active, setActive] = useState('');
const [openSelect, setOpenSelect] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
setOpenSelect(false);
}
};
window.addEventListener('click', handleClickOutside);
return () => window.removeEventListener('click', handleClickOutside);
}, []);
useEffect(() => {
list.forEach((listItem: ListItem) => {
if (
('path' in listItem && pathname === listItem.path) ||
('slug' in listItem && searchParams.get('sort') === listItem.slug)
) {
setActive(listItem.title);
}
});
}, [pathname, list, searchParams]);
return (
<div className="relative" ref={ref}>
<div
onClick={() => {
setOpenSelect(!openSelect);
}}
className="flex w-full items-center justify-between rounded border border-black/30 px-4 py-2 text-sm dark:border-white/30"
>
<div>{active}</div>
<ChevronDownIcon className="h-4" />
</div>
{openSelect && (
<div
onClick={() => {
setOpenSelect(false);
}}
className="absolute z-40 w-full rounded-b-md bg-white p-4 shadow-md dark:bg-black"
>
{list.map((item: ListItem, i) => (
<FilterItem key={i} item={item} />
))}
</div>
)}
</div>
);
}

View File

@ -1,41 +0,0 @@
import { SortFilterItem } from 'lib/constants';
import { Suspense } from 'react';
import FilterItemDropdown from './dropdown';
import { FilterItem } from './item';
export type ListItem = SortFilterItem | PathFilterItem;
export type PathFilterItem = { title: string; path: string };
function FilterItemList({ list }: { list: ListItem[] }) {
return (
<>
{list.map((item: ListItem, i) => (
<FilterItem key={i} item={item} />
))}
</>
);
}
export default function FilterList({ list, title }: { list: ListItem[]; title?: string }) {
return (
<>
<nav>
{title ? (
<h3 className="hidden text-xs text-neutral-500 md:block dark:text-neutral-400">
{title}
</h3>
) : null}
<ul className="hidden md:block">
<Suspense fallback={null}>
<FilterItemList list={list} />
</Suspense>
</ul>
<ul className="md:hidden">
<Suspense fallback={null}>
<FilterItemDropdown list={list} />
</Suspense>
</ul>
</nav>
</>
);
}

View File

@ -1,67 +0,0 @@
'use client';
import clsx from 'clsx';
import type { SortFilterItem } from 'lib/constants';
import { createUrl } from 'lib/utils';
import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation';
import type { ListItem, PathFilterItem } from '.';
function PathFilterItem({ item }: { item: PathFilterItem }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const active = pathname === item.path;
const newParams = new URLSearchParams(searchParams.toString());
const DynamicTag = active ? 'p' : Link;
newParams.delete('q');
return (
<li className="mt-2 flex text-black dark:text-white" key={item.title}>
<DynamicTag
href={createUrl(item.path, newParams)}
className={clsx(
'w-full text-sm underline-offset-4 hover:underline dark:hover:text-neutral-100',
{
'underline underline-offset-4': active
}
)}
>
{item.title}
</DynamicTag>
</li>
);
}
function SortFilterItem({ item }: { item: SortFilterItem }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const active = searchParams.get('sort') === item.slug;
const q = searchParams.get('q');
const href = createUrl(
pathname,
new URLSearchParams({
...(q && { q }),
...(item.slug && item.slug.length && { sort: item.slug })
})
);
const DynamicTag = active ? 'p' : Link;
return (
<li className="mt-2 flex text-sm text-black dark:text-white" key={item.title}>
<DynamicTag
prefetch={!active ? false : undefined}
href={href}
className={clsx('w-full hover:underline hover:underline-offset-4', {
'underline underline-offset-4': active
})}
>
{item.title}
</DynamicTag>
</li>
);
}
export function FilterItem({ item }: { item: ListItem }) {
return 'path' in item ? <PathFilterItem item={item} /> : <SortFilterItem item={item} />;
}

View File

@ -1,15 +0,0 @@
import clsx from 'clsx';
const dots = 'mx-[1px] inline-block h-1 w-1 animate-blink rounded-md';
const LoadingDots = ({ className }: { className: string }) => {
return (
<span className="mx-2 inline-flex items-center">
<span className={clsx(dots, className)} />
<span className={clsx(dots, 'animation-delay-[200ms]', className)} />
<span className={clsx(dots, 'animation-delay-[400ms]', className)} />
</span>
);
};
export default LoadingDots;

View File

@ -1,23 +0,0 @@
import clsx from 'clsx';
import LogoIcon from './icons/logo';
export default function LogoSquare({ size }: { size?: 'sm' | undefined }) {
return (
<div
className={clsx(
'flex flex-none items-center justify-center border border-neutral-200 bg-white dark:border-neutral-700 dark:bg-black',
{
'h-[40px] w-[40px] rounded-xl': !size,
'h-[30px] w-[30px] rounded-lg': size === 'sm'
}
)}
>
<LogoIcon
className={clsx({
'h-[16px] w-[16px]': !size,
'h-[10px] w-[10px]': size === 'sm'
})}
/>
</div>
);
}

View File

@ -1,40 +0,0 @@
import { ImageResponse } from 'next/og';
import LogoIcon from './icons/logo';
export type Props = {
title?: string;
};
export default async function OpengraphImage(props?: Props): Promise<ImageResponse> {
const { title } = {
...{
title: process.env.SITE_NAME
},
...props
};
return new ImageResponse(
(
<div tw="flex h-full w-full flex-col items-center justify-center bg-black">
<div tw="flex flex-none items-center justify-center border border-neutral-700 h-[160px] w-[160px] rounded-3xl">
<LogoIcon width="64" height="58" fill="white" />
</div>
<p tw="mt-12 text-6xl font-bold text-white">{title}</p>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: await fetch(new URL('../fonts/Inter-Bold.ttf', import.meta.url)).then((res) =>
res.arrayBuffer()
),
style: 'normal',
weight: 700
}
]
}
);
}

View File

@ -1,24 +0,0 @@
import clsx from 'clsx';
const Price = ({
amount,
className,
currencyCode = 'USD',
currencyCodeClassName
}: {
amount: string;
className?: string;
currencyCode: string;
currencyCodeClassName?: string;
} & React.ComponentProps<'p'>) => (
<p suppressHydrationWarning={true} className={className}>
{`${new Intl.NumberFormat(undefined, {
style: 'currency',
currency: currencyCode,
currencyDisplay: 'narrowSymbol'
}).format(parseFloat(amount))}`}
<span className={clsx('ml-1 inline', currencyCodeClassName)}>{`${currencyCode}`}</span>
</p>
);
export default Price;

View File

@ -1,99 +0,0 @@
'use client';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
import { GridTileImage } from 'components/grid/tile';
import { createUrl } from 'lib/utils';
import Image from 'next/image';
import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation';
export function Gallery({ images }: { images: { src: string; altText: string }[] }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const imageSearchParam = searchParams.get('image');
const imageIndex = imageSearchParam ? parseInt(imageSearchParam) : 0;
const nextSearchParams = new URLSearchParams(searchParams.toString());
const nextImageIndex = imageIndex + 1 < images.length ? imageIndex + 1 : 0;
nextSearchParams.set('image', nextImageIndex.toString());
const nextUrl = createUrl(pathname, nextSearchParams);
const previousSearchParams = new URLSearchParams(searchParams.toString());
const previousImageIndex = imageIndex === 0 ? images.length - 1 : imageIndex - 1;
previousSearchParams.set('image', previousImageIndex.toString());
const previousUrl = createUrl(pathname, previousSearchParams);
const buttonClassName =
'h-full px-6 transition-all ease-in-out hover:scale-110 hover:text-black dark:hover:text-white flex items-center justify-center';
return (
<>
<div className="relative aspect-square h-full max-h-[550px] w-full overflow-hidden">
{images[imageIndex] && (
<Image
className="h-full w-full object-contain"
fill
sizes="(min-width: 1024px) 66vw, 100vw"
alt={images[imageIndex]?.altText as string}
src={images[imageIndex]?.src as string}
priority={true}
/>
)}
{images.length > 1 ? (
<div className="absolute bottom-[15%] flex w-full justify-center">
<div className="mx-auto flex h-11 items-center rounded-full border border-white bg-neutral-50/80 text-neutral-500 backdrop-blur dark:border-black dark:bg-neutral-900/80">
<Link
aria-label="Previous product image"
href={previousUrl}
className={buttonClassName}
scroll={false}
>
<ArrowLeftIcon className="h-5" />
</Link>
<div className="mx-1 h-6 w-px bg-neutral-500"></div>
<Link
aria-label="Next product image"
href={nextUrl}
className={buttonClassName}
scroll={false}
>
<ArrowRightIcon className="h-5" />
</Link>
</div>
</div>
) : null}
</div>
{images.length > 1 ? (
<ul className="my-12 flex items-center justify-center gap-2 overflow-auto py-1 lg:mb-0">
{images.map((image, index) => {
const isActive = index === imageIndex;
const imageSearchParams = new URLSearchParams(searchParams.toString());
imageSearchParams.set('image', index.toString());
return (
<li key={image.src} className="h-20 w-20">
<Link
aria-label="Enlarge product image"
href={createUrl(pathname, imageSearchParams)}
scroll={false}
className="h-full w-full"
>
<GridTileImage
alt={image.altText}
src={image.src}
width={80}
height={80}
active={isActive}
/>
</Link>
</li>
);
})}
</ul>
) : null}
</>
);
}

View File

@ -1,36 +0,0 @@
import { AddToCart } from 'components/cart/add-to-cart';
import Price from 'components/price';
import Prose from 'components/prose';
import { Product } from 'lib/shopify/types';
import { Suspense } from 'react';
import { VariantSelector } from './variant-selector';
export function ProductDescription({ product }: { product: Product }) {
return (
<>
<div className="mb-6 flex flex-col border-b pb-6 dark:border-neutral-700">
<h1 className="mb-2 text-5xl font-medium">{product.title}</h1>
<div className="mr-auto w-auto rounded-full bg-blue-600 p-2 text-sm text-white">
<Price
amount={product.priceRange.maxVariantPrice.amount}
currencyCode={product.priceRange.maxVariantPrice.currencyCode}
/>
</div>
</div>
<Suspense fallback={null}>
<VariantSelector options={product.options} variants={product.variants} />
</Suspense>
{product.descriptionHtml ? (
<Prose
className="mb-6 text-sm leading-tight dark:text-white/[60%]"
html={product.descriptionHtml}
/>
) : null}
<Suspense fallback={null}>
<AddToCart variants={product.variants} availableForSale={product.availableForSale} />
</Suspense>
</>
);
}

View File

@ -1,106 +0,0 @@
'use client';
import clsx from 'clsx';
import { ProductOption, ProductVariant } from 'lib/shopify/types';
import { createUrl } from 'lib/utils';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
type Combination = {
id: string;
availableForSale: boolean;
[key: string]: string | boolean; // ie. { color: 'Red', size: 'Large', ... }
};
export function VariantSelector({
options,
variants
}: {
options: ProductOption[];
variants: ProductVariant[];
}) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const hasNoOptionsOrJustOneOption =
!options.length || (options.length === 1 && options[0]?.values.length === 1);
if (hasNoOptionsOrJustOneOption) {
return null;
}
const combinations: Combination[] = variants.map((variant) => ({
id: variant.id,
availableForSale: variant.availableForSale,
// Adds key / value pairs for each variant (ie. "color": "Black" and "size": 'M").
...variant.selectedOptions.reduce(
(accumulator, option) => ({ ...accumulator, [option.name.toLowerCase()]: option.value }),
{}
)
}));
return options.map((option) => (
<dl className="mb-8" key={option.id}>
<dt className="mb-4 text-sm uppercase tracking-wide">{option.name}</dt>
<dd className="flex flex-wrap gap-3">
{option.values.map((value) => {
const optionNameLowerCase = option.name.toLowerCase();
// Base option params on current params so we can preserve any other param state in the url.
const optionSearchParams = new URLSearchParams(searchParams.toString());
// Update the option params using the current option to reflect how the url *would* change,
// if the option was clicked.
optionSearchParams.set(optionNameLowerCase, value);
const optionUrl = createUrl(pathname, optionSearchParams);
// In order to determine if an option is available for sale, we need to:
//
// 1. Filter out all other param state
// 2. Filter out invalid options
// 3. Check if the option combination is available for sale
//
// This is the "magic" that will cross check possible variant combinations and preemptively
// disable combinations that are not available. For example, if the color gray is only available in size medium,
// then all other sizes should be disabled.
const filtered = Array.from(optionSearchParams.entries()).filter(([key, value]) =>
options.find(
(option) => option.name.toLowerCase() === key && option.values.includes(value)
)
);
const isAvailableForSale = combinations.find((combination) =>
filtered.every(
([key, value]) => combination[key] === value && combination.availableForSale
)
);
// The option is active if it's in the url params.
const isActive = searchParams.get(optionNameLowerCase) === value;
return (
<button
key={value}
aria-disabled={!isAvailableForSale}
disabled={!isAvailableForSale}
onClick={() => {
router.replace(optionUrl, { scroll: false });
}}
title={`${option.name} ${value}${!isAvailableForSale ? ' (Out of Stock)' : ''}`}
className={clsx(
'flex min-w-[48px] items-center justify-center rounded-full border bg-neutral-100 px-2 py-1 text-sm dark:border-neutral-800 dark:bg-neutral-900',
{
'cursor-default ring-2 ring-blue-600': isActive,
'ring-1 ring-transparent transition duration-300 ease-in-out hover:scale-110 hover:ring-blue-600 ':
!isActive && isAvailableForSale,
'relative z-10 cursor-not-allowed overflow-hidden bg-neutral-100 text-neutral-500 ring-1 ring-neutral-300 before:absolute before:inset-x-0 before:-z-10 before:h-px before:-rotate-45 before:bg-neutral-300 before:transition-transform dark:bg-neutral-900 dark:text-neutral-400 dark:ring-neutral-700 before:dark:bg-neutral-700':
!isAvailableForSale
}
)}
>
{value}
</button>
);
})}
</dd>
</dl>
));
}

View File

@ -1,21 +0,0 @@
import clsx from 'clsx';
import type { FunctionComponent } from 'react';
interface TextProps {
html: string;
className?: string;
}
const Prose: FunctionComponent<TextProps> = ({ html, className }) => {
return (
<div
className={clsx(
'prose mx-auto max-w-6xl text-base leading-7 text-black prose-headings:mt-8 prose-headings:font-semibold prose-headings:tracking-wide prose-headings:text-black prose-h1:text-5xl prose-h2:text-4xl prose-h3:text-3xl prose-h4:text-2xl prose-h5:text-xl prose-h6:text-lg prose-a:text-black prose-a:underline hover:prose-a:text-neutral-300 prose-strong:text-black prose-ol:mt-8 prose-ol:list-decimal prose-ol:pl-6 prose-ul:mt-8 prose-ul:list-disc prose-ul:pl-6 dark:text-white dark:prose-headings:text-white dark:prose-a:text-white dark:prose-strong:text-white',
className
)}
dangerouslySetInnerHTML={{ __html: html as string }}
/>
);
};
export default Prose;

23
app/diseases/page.tsx Normal file
View File

@ -0,0 +1,23 @@
import DiseasesAndConditions from 'components/diseases/diseases-and-conditions';
import Specialities from 'components/diseases/specialities';
import spree from '@commerce/index';
async function getTaxons() {
const res = await spree.taxons.list({});
if (res.isFail()) throw new Error('Failed to fetch data');
return res.success().data;
}
export default async function DiseasesPage() {
const taxons = await getTaxons();
return (
<div>
<Specialities taxons={taxons} />
<DiseasesAndConditions />
</div>
);
}

View File

@ -1,42 +1,15 @@
import Footer from 'components/layout/footer';
import Navbar from 'components/layout/navbar';
import { GeistSans } from 'geist/font/sans';
import { ensureStartsWith } from 'lib/utils';
import { ReactNode } from 'react';
import './globals.css';
const { TWITTER_CREATOR, TWITTER_SITE, SITE_NAME } = process.env;
const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: 'http://localhost:3000';
const twitterCreator = TWITTER_CREATOR ? ensureStartsWith(TWITTER_CREATOR, '@') : undefined;
const twitterSite = TWITTER_SITE ? ensureStartsWith(TWITTER_SITE, 'https://') : undefined;
export const metadata = {
metadataBase: new URL(baseUrl),
title: {
default: SITE_NAME!,
template: `%s | ${SITE_NAME}`
},
robots: {
follow: true,
index: true
},
...(twitterCreator &&
twitterSite && {
twitter: {
card: 'summary_large_image',
creator: twitterCreator,
site: twitterSite
}
})
};
export default async function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" className={GeistSans.variable}>
<body className="bg-neutral-50 text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white">
<html lang="en">
<body>
<Navbar />
<main>{children}</main>
<Footer />
</body>
</html>
);

11
app/ondemand/layout.tsx Normal file
View File

@ -0,0 +1,11 @@
import Navbar from 'components/ondemand/navbar';
import { ReactNode } from 'react';
export default function OndemandLayout({ children }: { children: ReactNode }) {
return (
<>
<Navbar />
<main>{children}</main>;
</>
);
}

3
app/ondemand/page.tsx Normal file
View File

@ -0,0 +1,3 @@
export default async function OndemandPage() {
return <h3>FUCK page</h3>;
}

View File

View File

@ -1,17 +0,0 @@
import spreeClient from 'lib/spree';
export default async function Products() {
const products = await (await spreeClient.products.list({})).success().data;
console.log('FUCK', products);
return (
<div>
{products.map((product) => (
<div className="md:ml-auto" key={product.id}>
{product.attributes.name}
</div>
))}
</div>
);
}

View File

@ -1,11 +1,17 @@
import { Carousel } from 'components/carousel';
import Footer from 'components/layout/footer';
import ContactUs from 'components/contact-us';
import Quicklinks from 'components/home/quicklinks';
import LatestNews from 'components/latest-news';
import Hero from 'components/layout/hero/hero-1';
import ScienceInnovation from 'components/science-innovation';
export default async function HomePage() {
return (
<>
<Carousel />
<Footer />
<Hero />
<Quicklinks />
<LatestNews />
<ScienceInnovation />
<ContactUs />
</>
);
}

10
app/tests/[id]/page.tsx Normal file
View File

@ -0,0 +1,10 @@
const TestPage = () => {
return (
<div>
<h1>Test Detail for ID:</h1>
{/* Render test details based on the ID */}
</div>
);
};
export default TestPage;

24
app/tests/page.tsx Normal file
View File

@ -0,0 +1,24 @@
import spree from '@commerce/index';
import Alphabet from 'components/tests/alphabet';
import TestsTable from 'components/tests/tests-table';
async function getProducts() {
const res = await spree.products.list({});
if (!res.isSuccess()) {
throw new Error('Failed to fetch data');
}
return res.success().data;
}
export default async function TestsPage() {
const products = await getProducts();
return (
<div>
<Alphabet />
<TestsTable products={products} />
</div>
);
}

View File

@ -0,0 +1,5 @@
const ContactUs = () => {
return <div>Contact Us Block</div>;
};
export default ContactUs;

View File

@ -0,0 +1,13 @@
const DiseasesAndConditions = ({ taxons = [] }) => {
return (
<ul>
{taxons.map((taxon) => (
<li key={taxon.id}>
<Link href="/">{taxon.attributes.name}</Link>
</li>
))}
</ul>
);
};
export default DiseasesAndConditions;

View File

@ -0,0 +1,15 @@
import Link from 'next/link';
const Specialities = ({ taxons = [] }) => {
return (
<ul>
{taxons.map((taxon) => (
<li key={taxon.id}>
<Link href="/">{taxon.attributes.name}</Link>
</li>
))}
</ul>
);
};
export default Specialities;

View File

@ -0,0 +1,11 @@
import Link from 'next/link';
export default function Quicklinks() {
return (
<div>
Quicklinks Block
<Link href="/tests">Tests</Link>
<Link href="/diseases">Diseases And Conditions</Link>
</div>
);
}

View File

@ -0,0 +1,5 @@
const LatestNews = () => {
return <div>Latest News Block</div>;
};
export default LatestNews;

View File

View File

@ -1,68 +1,69 @@
import Link from 'next/link';
import FooterMenu from 'components/layout/footer-menu';
import LogoSquare from 'components/logo-square';
import { Suspense } from 'react';
const Footer = () => (
<footer>
<div>
<ul>
<li>
<Link href="/about">About Us</Link>
</li>
<li>
<Link href="/newsroom">Newsroom</Link>
</li>
<li>
<Link href="/careers">Careers</Link>
</li>
<li>
<Link href="/investors">Investors</Link>
</li>
</ul>
<ul>
<li>
<Link href="/lab">Labs</Link>
</li>
<li>
<Link href="/test-results">Test Results</Link>
</li>
<li>
<Link href="/all-patient">All Patient</Link>
</li>
<li>
<Link href="/all-provider">All Provider</Link>
</li>
</ul>
<ul>
<li>
<Link href="/suppliers">Suppliers & Vendors</Link>
</li>
<li>
<Link href="/hsnpa">HSNPA Notice</Link>
</li>
<li>
<Link href="/privacy-practices">Privacy Practices</Link>
</li>
<li>
<Link href="/no-surprises">No Surprises Act</Link>
</li>
</ul>
<ul>
<li>
<Link href="/biopharma">Biopharma</Link>
</li>
<li>
<Link href="/drug-testing">Drug Testing</Link>
</li>
<li>
<Link href="/paternity-testing">Paternity Testing</Link>
</li>
<li>
<Link href="/health-testing">Health Testing</Link>
</li>
</ul>
</div>
<div>
<p>&copy; 2024 Labcorp</p>
</div>
</footer>
);
const { COMPANY_NAME, SITE_NAME } = process.env;
export default async function Footer() {
const currentYear = new Date().getFullYear();
const copyrightDate = 2023 + (currentYear > 2023 ? `-${currentYear}` : '');
const skeleton = 'w-full h-6 animate-pulse rounded bg-neutral-200 dark:bg-neutral-700';
const menu = [];
const copyrightName = COMPANY_NAME || SITE_NAME || '';
return (
<footer className="text-sm text-neutral-500 dark:text-neutral-400">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6 border-t border-neutral-200 px-6 py-12 text-sm md:flex-row md:gap-12 md:px-4 min-[1320px]:px-0 dark:border-neutral-700">
<div>
<Link className="flex items-center gap-2 text-black md:pt-1 dark:text-white" href="/">
<LogoSquare size="sm" />
<span className="uppercase">{SITE_NAME}</span>
</Link>
</div>
<Suspense
fallback={
<div className="flex h-[188px] w-[200px] flex-col gap-2">
<div className={skeleton} />
<div className={skeleton} />
<div className={skeleton} />
<div className={skeleton} />
<div className={skeleton} />
<div className={skeleton} />
</div>
}
>
<FooterMenu menu={menu} />
</Suspense>
<div className="md:ml-auto">
<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"
aria-label="Deploy on Vercel"
href="https://vercel.com/templates/next.js/nextjs-commerce"
>
<span className="px-3"></span>
<hr className="h-full border-r border-neutral-200 dark:border-neutral-700" />
<span className="px-3">Deploy</span>
</a>
</div>
</div>
<div className="border-t border-neutral-200 py-6 text-sm dark:border-neutral-700">
<div className="mx-auto flex w-full max-w-7xl flex-col items-center gap-1 px-4 md:flex-row md:gap-0 md:px-4 min-[1320px]:px-0">
<p>
&copy; {copyrightDate} {copyrightName}
{copyrightName.length && !copyrightName.endsWith('.') ? '.' : ''} All rights reserved.
</p>
<hr className="mx-4 hidden h-4 w-[1px] border-l border-neutral-400 md:inline-block" />
<p>Designed in California</p>
<p className="md:ml-auto">
<a href="https://vercel.com" className="text-black dark:text-white">
Crafted by Vercel
</a>
</p>
</div>
</div>
</footer>
);
}
export default Footer;

View File

@ -0,0 +1,13 @@
const { COMPANY_NAME, SITE_NAME } = process.env;
import Navbar from 'components/layout/navbar';
import Topbar from 'components/layout/topbar';
export default function Header() {
return (
<header className="responsivegrid aem-GridColumn aem-GridColumn--default--12 container">
<Topbar />
<Navbar />
</header>
);
}

View File

@ -0,0 +1,5 @@
const Hero1 = () => {
return <div>Hero 1 Block</div>;
};
export default Hero1;

View File

@ -0,0 +1,5 @@
const Hero2 = () => {
return <div>Hero 2 Block</div>;
};
export default Hero2;

View File

@ -1,57 +1,13 @@
import Cart from 'components/cart';
import OpenCart from 'components/cart/open-cart';
import LogoSquare from 'components/logo-square';
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 = [];
export default function Navbar() {
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>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/news">News</Link>
<Link href="/careers">Careers</Link>
<Link href="/help">Help</Link>
</nav>
);
}

View File

@ -0,0 +1,956 @@
export default function Topbar() {
return (
<div className="responsivegrid container">
<div id="container-1f99be38e4" className="cmp-container">
<div className="responsivegrid contained global-topnav-menu padding-top-bottom-16 container">
<div id="container-2af0118eb1" className="cmp-container">
<div className="aem-Grid aem-Grid--12 aem-Grid--tablet--7 aem-Grid--default--12 ">
<div className="globalnavigation aem-GridColumn--tablet--1 aem-GridColumn--default--none aem-GridColumn aem-GridColumn--offset--default--0 aem-GridColumn--default--1">
<button
id="globalnavigation-hamburger"
className="globalnavigation-toggle"
aria-controls="globalnavigation-menu"
aria-expanded="false"
aria-label="Open global Navigation"
></button>
<div className="globalnavigation-navModal" id="globalnavigation-menu">
<div className="close-btn">
<button
id="globalnavigation-closemodal"
aria-controls="globalnavigation-menu"
aria-label="Close global Navigation"
></button>
</div>
<div className="xfpageext xfpage page basicpage">
<div className="xf-content-height">
<div className="root responsivegrid container">
<div id="container-3afad37a75" className="cmp-container">
<div className="aem-Grid aem-Grid--12 aem-Grid--default--12 ">
<nav className="responsivegrid aem-GridColumn aem-GridColumn--default--12 container">
<div id="container-77eb8fa08d" className="cmp-container">
<div className="aem-Grid aem-Grid--12 aem-Grid--default--12 ">
<div className="responsivegrid aem-GridColumn--default--none aem-GridColumn aem-GridColumn--default--12 aem-GridColumn--offset--default--0 container bg-white">
<div id="container-3c8442d54d" className="cmp-container">
<div className="aem-Grid aem-Grid--12 aem-Grid--default--12 ">
<div className="image aem-GridColumn--default--none aem-GridColumn aem-GridColumn--default--5 aem-GridColumn--offset--default--0">
<div className="cmp-image">
<a
className="cmp-image__link"
tabIndex={-1}
href="https://www.labcorp.com/"
aria-label="Labcorp Logo"
>
<img
src="https://www.labcorp.com/content/dam/labcorp/images/2023/12/labcorp-logo.png"
loading="lazy"
className="cmp-image__image"
tabIndex={0}
itemProp="contentUrl"
width={200}
height={53}
alt="Labcorp"
/>
</a>
</div>
</div>
</div>
</div>
</div>
<div className="responsivegrid bg-gray-7 aem-GridColumn aem-GridColumn--default--12 container">
<div id="container-5d6ca48035" className="cmp-container">
<div className="aem-Grid aem-Grid--12 aem-Grid--default--12 ">
<div className="button globalnav-btn aem-GridColumn aem-GridColumn--default--12">
<div className="user-icon">
<div>
<a
id="button-b3d4bc0d4e"
className="cmp-button"
href="https://www.labcorp.com/logins"
>
<span
className="cmp-button__icon cmp-button__icon--user-icon"
aria-hidden="true"
/>
<span className="cmp-button__text">Logins</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="responsivegrid bg-gray-7 aem-GridColumn--default--none aem-GridColumn aem-GridColumn--default--12 aem-GridColumn--offset--default--0 container">
<div id="container-8fc48ba12b" className="cmp-container">
<div className="aem-Grid aem-Grid--12 aem-Grid--default--12 ">
<div className="experiencefragment aem-GridColumn aem-GridColumn--default--12">
<div
id="experiencefragment-25c77b4a39"
className="cmp-experiencefragment cmp-experiencefragment--top-nav-quick-links"
>
<div
id="container-c53f00e4dd"
className="cmp-container"
>
<div className="aem-Grid aem-Grid--12 aem-Grid--default--12 ">
<div className="responsivegrid aem-GridColumn aem-GridColumn--default--12 container">
<div
id="container-97e567dae1"
className="cmp-container"
>
<div className="aem-Grid aem-Grid--12 aem-Grid--default--12 ">
<div className="topnavquicklinks aem-GridColumn aem-GridColumn--default--12">
<div className="tab-container">
{/* Tab 1 */}
<button
id="tabset1-1-9ea9c975-c40c-44bc-88f4-64061a562a9f"
role="tab"
aria-selected="true"
aria-controls="panel1-9ea9c975-c40c-44bc-88f4-64061a562a9f"
className="button-tab"
>
Individuals &amp; Patients
</button>
<div
className="panel-tab "
id="panel1-9ea9c975-c40c-44bc-88f4-64061a562a9f"
role="tabpanel"
aria-labelledby="tabset1-1-9ea9c975-c40c-44bc-88f4-64061a562a9f"
aria-hidden="false"
>
<div>
<div className="tab_content-button">
<a
href="https://www.labcorp.com/labs-and-appointments-advanced-search"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--location-icon" />
<span className="tab-text">
Find a Lab
</span>
</a>
<a
href="https://www.labcorp.com/patients/results"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--casestudy-icon" />
<span className="tab-text">
View Test Results
</span>
</a>
<a
href="https://patient.labcorp.com/invoices/find"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--patientbill-icon" />
<span className="tab-text">
Pay a Bill{' '}
</span>
</a>
<a
href="https://www.ondemand.labcorp.com/products"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--retail-icon" />
<span className="tab-text">
Shop for Tests{' '}
</span>
</a>
</div>
</div>
<a
href="https://www.labcorp.com/patients"
className="tab-link"
target="_self"
>
<span className="tab-text">
View Individuals &amp; Patients Page{' '}
</span>
</a>
</div>
{/* Tab 2 */}
<button
id="tabset1-2-0fd86cae-38ad-41c7-b0d2-e983b532447f"
role="tab"
aria-controls="panel2-0fd86cae-38ad-41c7-b0d2-e983b532447f"
className="button-tab"
>
Providers
</button>
<div
className="panel-tab"
id="panel2-0fd86cae-38ad-41c7-b0d2-e983b532447f"
role="tabpanel"
aria-labelledby="tabset1-2-0fd86cae-38ad-41c7-b0d2-e983b532447f"
>
<div>
<div className="tab_content-button">
<a
href="https://www.labcorp.com/test-menu/search"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--container-icon" />
<span className="tab-text">
Test Menu
</span>
</a>
<a
href="https://www.labcorplink.com/ui/#/login"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--patients-icon" />
<span className="tab-text">
Provider Login
</span>
</a>
<a
href="https://www.labcorp.com/science"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--patientresources-icon" />
<span className="tab-text">
Education &amp; Experts
</span>
</a>
<a
href="https://www.labcorp.com/contact-labcorp-account-representative"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--communicate-icon" />
<span className="tab-text">
Contact Us
</span>
</a>
</div>
</div>
<a
href="https://www.labcorp.com/providers"
className="tab-link"
target="_self"
>
<span className="tab-text">
View Providers Page
</span>
</a>
</div>
{/* Tab 3 */}
<button
id="tabset1-3-0ae4906c-8cba-48bc-b0f1-65db9ab26912"
role="tab"
aria-controls="panel3-0ae4906c-8cba-48bc-b0f1-65db9ab26912"
className="button-tab"
>
Health Systems &amp; Organizations
</button>
<div
className="panel-tab"
id="panel3-0ae4906c-8cba-48bc-b0f1-65db9ab26912"
role="tabpanel"
aria-labelledby="tabset1-3-0ae4906c-8cba-48bc-b0f1-65db9ab26912"
>
<div>
<div className="tab_content-button">
<a
href="https://www.labcorp.com/organizations/hospitals"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--hospitalhealthsystems-icon" />
<span className="tab-text">
Hospitals &amp; Health Systems
</span>
</a>
<a
href="https://www.labcorp.com/organizations/employers"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--employeewellness-icon" />
<span className="tab-text">
Employee Wellness &amp; Testing
</span>
</a>
<a
href="https://www.labcorp.com/organizations/managed-care"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--managedcare-icon" />
<span className="tab-text">
Managed Care &amp; Payors
</span>
</a>
<a
href="https://www.labcorp.com/organizations/resources"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--casestudy-icon" />
<span className="tab-text">
Resources
</span>
</a>
</div>
</div>
<a
href="https://www.labcorp.com/organizations"
className="tab-link"
target="_self"
>
<span className="tab-text">
View Organizations Page
</span>
</a>
</div>
{/* Tab 4 */}
<button
id="tabset1-4-29750eab-a3ec-4870-87b1-085d39098005"
role="tab"
aria-controls="panel4-29750eab-a3ec-4870-87b1-085d39098005"
className="button-tab"
>
Biopharma
</button>
<div
className="panel-tab"
id="panel4-29750eab-a3ec-4870-87b1-085d39098005"
role="tabpanel"
aria-labelledby="tabset1-4-29750eab-a3ec-4870-87b1-085d39098005"
>
<div>
<div className="tab_content-button">
<a
href="https://www.labcorp.com/biopharma/nonclinical-research-studies"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--datasets-icon" />
<span className="tab-text">
Nonclinical Research
</span>
</a>
<a
href="https://www.labcorp.com/biopharma/clinical-trial-testing-labs"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--development-icon" />
<span className="tab-text">
Central Laboratory Services
</span>
</a>
<a
href="https://biopharma.labcorp.com/clinical-testing/labs-kits/investigators/order-a-kit.html"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--retail-icon" />
<span className="tab-text">
Order a Kit
</span>
</a>
<a
href="https://biopharma.labcorp.com/contact-us.html"
className="tab-link-button"
target="_self"
>
<span className="cmp-button__icon cmp-button__icon--communicate-icon" />
<span className="tab-text">
Contact Us
</span>
</a>
</div>
</div>
<a
href="https://www.labcorp.com/biopharma"
className="tab-link"
target="_self"
>
<span className="tab-text">
View Biopharma Page
</span>
</a>
</div>
{/* Tab 5 */}
{/* Tab 6 */}
</div>
</div>
</div>
</div>
</div>
<nav className="responsivegrid aem-GridColumn aem-GridColumn--default--12 container">
<div
id="container-620ef24c7a"
className="cmp-container"
></div>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="responsivegrid bg-gray-7 aem-GridColumn--default--none aem-GridColumn aem-GridColumn--default--12 aem-GridColumn--offset--default--0 container">
<div id="container-71e63cf21d" className="cmp-container">
<div className="aem-Grid aem-Grid--12 aem-Grid--default--12 ">
<div className="list navStyle lastItemCaret aem-GridColumn aem-GridColumn--default--12">
<h2>
<a
href="https://www.labcorp.com/wellness"
target="_self"
>
Managing Your Health
</a>
</h2>
<ul id="list-ad78bd18cf" className="cmp-list">
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://www.ondemand.labcorp.com/products"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">
Shop for Health Tests
</span>
</a>
</li>
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://womenshealth.labcorp.com/"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">
Explore Women's Health
</span>
</a>
</li>
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://www.labcorp.com/patients/health-screenings"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">
Annual Wellness Guidelines{' '}
</span>
</a>
</li>
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://www.labcorp.com/wellness"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">More</span>
</a>
</li>
</ul>
</div>
<div className="separator aem-GridColumn aem-GridColumn--default--12">
<div id="separator-3b791ec7bb" className="cmp-separator">
<hr
className="cmp-separator__horizontal-rule"
aria-hidden="true"
role="none"
/>
</div>
</div>
<div className="list navStyle lastItemCaret aem-GridColumn aem-GridColumn--default--12">
<h2>
<a
href="https://www.labcorp.com/diseases"
target="_self"
>
Diseases &amp; Specialties
</a>
</h2>
<ul id="list-ee659b5733" className="cmp-list">
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://oncology.labcorp.com"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">
Cancer &amp; Oncology
</span>
</a>
</li>
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://www.labcorp.com/providers/neurology"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">
Neurology
</span>
</a>
</li>
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://www.labcorp.com/providers/rheumatology"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">
Rheumatology
</span>
</a>
</li>
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://www.labcorp.com/diseases"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">More</span>
</a>
</li>
</ul>
</div>
<div className="separator aem-GridColumn aem-GridColumn--default--12">
<div id="separator-fa749afb76" className="cmp-separator">
<hr
className="cmp-separator__horizontal-rule"
aria-hidden="true"
role="none"
/>
</div>
</div>
<div className="list navStyle lastItemCaret aem-GridColumn aem-GridColumn--default--12">
<h2>
<a
href="https://www.labcorp.com/modalities"
target="_self"
>
Treatment Methods &amp; Product Testing
</a>
</h2>
<ul id="list-4d5e01f122" className="cmp-list">
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://biopharma.labcorp.com/clinical-testing/precision-medicine-solutions/cell-and-gene-therapy.html"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">
Cell &amp; Gene Therapies
</span>
</a>
</li>
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://biopharma.labcorp.com/clinical-testing/precision-medicine-solutions.html"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">
Precision Medicine{' '}
</span>
</a>
</li>
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://biopharma.labcorp.com/industry-solutions/by-product/vaccines.html"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">
Vaccines
</span>
</a>
</li>
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://www.labcorp.com/modalities"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">More</span>
</a>
</li>
</ul>
</div>
<div className="separator aem-GridColumn aem-GridColumn--default--12">
<div id="separator-f91e9dd05c" className="cmp-separator">
<hr
className="cmp-separator__horizontal-rule"
aria-hidden="true"
role="none"
/>
</div>
</div>
<div className="list navStyle lastItemCaret aem-GridColumn aem-GridColumn--default--12">
<h2>
<a
href="https://www.labcorp.com/disciplines"
target="_self"
>
Lab Disciplines &amp; Services
</a>
</h2>
<ul id="list-9058237192" className="cmp-list">
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://www.labcorp.com/genetics-genomics"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">
Genetics &amp; Genomics
</span>
</a>
</li>
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://biopharma.labcorp.com/services/pathology.html"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">
Pathology
</span>
</a>
</li>
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://biopharma.labcorp.com/services/safety-assessment.html"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">
Toxicology
</span>
</a>
</li>
<li className="cmp-list__item">
<a
className="cmp-list__item-link"
href="https://www.labcorp.com/disciplines"
target="_self"
data-cmp-clickable="true"
>
<span className="cmp-list__item-title">More</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div className="responsivegrid border-right border-gray aem-GridColumn--default--none aem-GridColumn aem-GridColumn--default--6 aem-GridColumn--offset--default--0 container bg-white">
<div id="container-af7d1e843a" className="cmp-container">
<div className="aem-Grid aem-Grid--6 aem-Grid--default--6 ">
<div className="button globalnav-btn aem-GridColumn aem-GridColumn--default--6">
<div className="cost-estimate-icon">
<div>
<a
id="button-d73f91fd8c"
className="cmp-button"
href="https://www.labcorp.com/providers/specialists"
aria-label="Reference & Specialty Labs"
>
<span
className="cmp-button__icon cmp-button__icon--cost-estimate-icon"
aria-hidden="true"
/>
<span className="cmp-button__text">
Reference &amp; Specialty Labs
</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="responsivegrid aem-GridColumn--default--none aem-GridColumn aem-GridColumn--default--6 aem-GridColumn--offset--default--0 container bg-white">
<div id="container-42f33d8085" className="cmp-container">
<div className="aem-Grid aem-Grid--6 aem-Grid--default--6 ">
<div className="button globalnav-btn aem-GridColumn aem-GridColumn--default--6">
<div className="research-icon">
<div>
<a
id="button-b282156913"
className="cmp-button"
href="https://biopharma.labcorp.com/"
aria-label="Research & Development Labs"
>
<span
className="cmp-button__icon cmp-button__icon--research-icon"
aria-hidden="true"
/>
<span className="cmp-button__text">
Research &amp; Development Labs
</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="responsivegrid bg-navy aem-GridColumn aem-GridColumn--default--12 container">
<div id="container-c3ead77c7b" className="cmp-container">
<div className="aem-Grid aem-Grid--12 aem-Grid--default--12 ">
<div className="responsivegrid aem-GridColumn--default--none aem-GridColumn aem-GridColumn--default--10 aem-GridColumn--offset--default--1 container">
<div id="container-67acfdf659" className="cmp-container">
<div className="aem-Grid aem-Grid--10 aem-Grid--default--10 ">
<div className="text body1 aem-GridColumn--default--none aem-GridColumn aem-GridColumn--default--10 aem-GridColumn--offset--default--0 text-white">
<div id="text-7c83405c7d" className="cmp-text">
<p>
<a
href="https://www.labcorp.com/about"
aria-label="About"
>
About us
</a>
</p>
</div>
</div>
<div className="separator aem-GridColumn--default--none aem-GridColumn aem-GridColumn--default--10 aem-GridColumn--offset--default--0">
<div
id="separator-abc7a2aea6"
className="cmp-separator"
>
<hr className="cmp-separator__horizontal-rule" />
</div>
</div>
<div className="text body1 aem-GridColumn aem-GridColumn--default--10 text-white">
<div id="text-2d0e74e47f" className="cmp-text">
<p>
<a
href="https://www.labcorp.com/newsroom"
aria-label="News"
>
News
</a>
</p>
</div>
</div>
<div className="separator aem-GridColumn aem-GridColumn--default--10">
<div
id="separator-01f3293014"
className="cmp-separator"
>
<hr className="cmp-separator__horizontal-rule" />
</div>
</div>
<div className="text body1 aem-GridColumn aem-GridColumn--default--10 text-white">
<div id="text-cf0d378afb" className="cmp-text">
<p>
<a
href="https://careers.labcorp.com/global/en"
target="_blank"
rel="noopener noreferrer"
aria-label="Careers"
>
Careers
</a>
</p>
</div>
</div>
<div className="separator aem-GridColumn aem-GridColumn--default--10">
<div
id="separator-9ce22eb846"
className="cmp-separator"
>
<hr className="cmp-separator__horizontal-rule" />
</div>
</div>
<div className="text body1 aem-GridColumn aem-GridColumn--default--10 text-white">
<div id="text-0eca6e7765" className="cmp-text">
<p>
<a
href="https://ir.labcorp.com/"
target="_blank"
rel="noopener noreferrer"
aria-label="investors"
>
Investors
</a>
</p>
</div>
</div>
<div className="separator aem-GridColumn aem-GridColumn--default--10">
<div
id="separator-fa8cb396ff"
className="cmp-separator"
>
<hr className="cmp-separator__horizontal-rule" />
</div>
</div>
<div className="text body1 aem-GridColumn aem-GridColumn--default--10 text-white">
<div id="text-84ea2bbe5d" className="cmp-text">
<p>
<a
href="https://www.labcorp.com/contact-us"
aria-label="Help"
>
Help
</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="responsivegrid aem-GridColumn aem-GridColumn--default--12 container">
<div id="container-9faf004657" className="cmp-container">
<div className="aem-Grid aem-Grid--12 aem-Grid--default--12 "></div>
</div>
</div>
</div>
</div>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="image aem-GridColumn--default--none aem-GridColumn aem-GridColumn--tablet--2 aem-GridColumn--offset--default--0 aem-GridColumn--default--2">
<div className="cmp-image" itemType="http://schema.org/ImageObject">
<a className="cmp-image__link" tabIndex={-1} href="/" aria-label="Labcorp Logo">
<img
src="https://www.labcorp.com/content/dam/labcorp/images/2023/12/labcorp-logo@2x.png"
loading="lazy"
className="cmp-image__image"
tabIndex={0}
itemProp="contentUrl"
width={399}
height={106}
alt="Labcorp"
/>
</a>
</div>
</div>
<div className="responsivegrid aem-GridColumn--default--none aem-GridColumn aem-GridColumn--default--8 aem-GridColumn--tablet--7 aem-GridColumn--offset--default--1 container">
<div id="container-09ff0e5b1d" className="cmp-container">
<div className="text text-navy body2">
<div id="text-2d356dd3ac" className="cmp-text">
<p>
<a href="/about">About</a>
</p>
</div>
</div>
<div className="text text-navy body2">
<div id="text-6a5002cdbc" className="cmp-text">
<p>
<a href="/newsroom" target="_self" rel="noopener noreferrer">
News
</a>
</p>
</div>
</div>
<div className="text text-navy body2">
<div id="text-a63751913f" className="cmp-text">
<p>
<a
href="https://careers.labcorp.com/global/en"
target="_self"
rel="noopener noreferrer"
>
Careers
</a>
</p>
</div>
</div>
<div className="separator">
<div id="verticle-sepratore" className="cmp-separator">
<hr className="cmp-separator__horizontal-rule" />
</div>
</div>
<div className="search">
<div className="endpointurl" data-url="https://www.labcorp.com" />
<div className="input-box">
<input
type="text"
id="search-area"
aria-label="search"
placeholder="search"
aria-hidden="true"
/>
<div className="search">
<button
type="button"
className="search-icon"
aria-expanded="false"
aria-label="Search"
aria-controls="search-area"
/>
</div>
<span
className="close-icon"
role="button"
aria-label="search-close-button"
aria-hidden="true"
/>
</div>
</div>
<div className="text text-navy body2">
<div id="text-1cd6cb03e5" className="cmp-text">
<p>
<a href="https://www.labcorp.com/contact-us">Help</a>
</p>
</div>
</div>
<div className="text text-navy body2">
<div id="text-ab96ea9d81" className="cmp-text">
<p>
<a href="https://www.labcorp.com/logins">Login</a>
</p>
</div>
</div>
<div className="iconlisting">
<div className="icon">
<ul>
<li className="icon-accessibility">
<a
aria-label="Level Access website accessibility icon."
href="https://www.essentialaccessibility.com/labcorp/?utm_source=labcorphomepage&utm_medium=iconlarge&utm_term=eachannelpage&utm_content=header&utm_campaign=labcorp"
>
<img alt="Level Access website accessibility icon." loading="lazy" />
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,93 @@
import Image from 'next/image';
const NavBar = () => {
return (
<nav className="navbar">
<div className="navbar__logo">
<a href="/content/labcorp-ondemand/us/en">
<Image
src="https://via.placeholder.com/150x50?text=Labcorp+Logo"
alt="Labcorp Logo"
width={150}
height={50}
/>
</a>
</div>
<div className="navbar__links">
<a href="/content/labcorp-ondemand/us/en/products" className="navbar__link">
Shop Tests
</a>
<a href="/kit/register/code" className="navbar__link">
Register Kit
</a>
<a
href="https://patient.labcorp.com/portal/results/list"
target="_blank"
className="navbar__link"
>
View Results
</a>
<a href="/content/labcorp-ondemand/us/en/about-us" className="navbar__link">
About
</a>
</div>
<div className="navbar__auth">
<span className="navbar__signin">
<a href="https://login-patient.labcorp.com/oauth2/default/v1/authorize?...">Sign In</a>
</span>
<button className="navbar__signout">Sign Out</button>
</div>
<div className="navbar__search">
<button className="search-toggle" title="Search">
<Image
src="https://via.placeholder.com/28?text=Search+Icon"
alt="Search"
width={28}
height={28}
/>
</button>
</div>
<div className="navbar__cart">
<a href="/checkout/cart/">
<Image
src="https://via.placeholder.com/28?text=Cart+Items+Icon"
alt="Cart Items"
width={28}
height={28}
className="cart-icon"
/>
<Image
src="https://via.placeholder.com/28?text=Empty+Cart+Icon"
alt="Empty Cart"
width={28}
height={28}
className="empty-cart-icon"
/>
</a>
</div>
<div className="navbar__burger">
<button>
<Image
src="https://via.placeholder.com/20?text=Menu+Icon"
alt="Menu"
width={20}
height={20}
/>
<Image
src="https://via.placeholder.com/20?text=Close+Icon"
alt="Close"
width={20}
height={20}
/>
</button>
</div>
</nav>
);
};
export default NavBar;

View File

@ -0,0 +1,5 @@
const ScienceInnovation = () => {
return <div>Sience Innovation Block</div>;
};
export default ScienceInnovation;

View File

@ -0,0 +1,3 @@
export default function Alphabet() {
return <nav className="responsivegrid container">Alphabet</nav>;
}

View File

@ -0,0 +1,28 @@
import Link from 'next/link';
export default function TestsTable({ products = [] }) {
return (
<div>
<table>
<thead>
<tr>
<th>Number</th>
<th>Name</th>
<th>Specimen</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr key={product.id}>
<td>{product.id}</td>
<td>
<Link href={`/tests/${product.id}`}>{product.attributes.name}</Link>
</td>
<td>Urine</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

View File

@ -1,33 +0,0 @@
# [Spree Commerce][1] Provider
![Screenshots of Spree Commerce and NextJS Commerce][5]
An integration of [Spree Commerce](https://spreecommerce.org/) within NextJS Commerce. It supports browsing and searching Spree products and adding products to the cart.
**Demo**: [https://spree.vercel.store/][6]
## Installation
1. Setup Spree - [follow the Getting Started guide](https://dev-docs.spreecommerce.org/getting-started/installation).
1. Setup Nextjs Commerce - [instructions for setting up NextJS Commerce][2].
1. Copy the `.env.template` file in this directory (`/framework/spree`) to `.env.local` in the main directory
```bash
cp framework/spree/.env.template .env.local
```
1. Set `NEXT_PUBLIC_SPREE_CATEGORIES_TAXONOMY_PERMALINK` and `NEXT_PUBLIC_SPREE_BRANDS_TAXONOMY_PERMALINK` environment variables:
- They rely on [taxonomies'](https://dev-docs.spreecommerce.org/internals/products#taxons-and-taxonomies) permalinks in Spree.
- Go to the Spree admin panel and create `Categories` and `Brands` taxonomies if they don't exist and copy their permalinks into `.env.local` in NextJS Commerce.
1. Finally, run `npm run dev` :tada:
[1]: https://spreecommerce.org/
[2]: https://github.com/vercel/commerce
[3]: https://github.com/spree/spree_starter
[4]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
[5]: ./README-assets/screenshots.png
[6]: https://spree.vercel.store/

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1,44 +0,0 @@
import type { CheckoutEndpoint } from '.';
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
req: _request,
res: response,
config: _config
}) => {
try {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Checkout</title>
</head>
<body>
<div style='margin: 10rem auto; text-align: center; font-family: SansSerif, "Segoe UI", Helvetica; color: #888;'>
<svg xmlns="http://www.w3.org/2000/svg" style='height: 60px; width: 60px;' fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<h1>Checkout not yet implemented :(</h1>
<p>
See <a href='https://github.com/vercel/commerce/issues/64' target='_blank'>#64</a>
</p>
</div>
</body>
</html>
`;
response.status(200);
response.setHeader('Content-Type', 'text/html');
response.write(html);
response.end();
} catch (error) {
console.error(error);
const message = 'An unexpected error ocurred';
response.status(500).json({ data: null, errors: [{ message }] });
}
};
export default getCheckout;

View File

@ -1,19 +0,0 @@
import { createEndpoint } from '@commerce/api';
import type { GetAPISchema, CommerceAPI } from '@commerce/api';
import checkoutEndpoint from '@commerce/api/endpoints/checkout';
import type { CheckoutSchema } from '@commerce/types/checkout';
import getCheckout from './get-checkout';
import type { SpreeApiProvider } from '../..';
export type CheckoutAPI = GetAPISchema<CommerceAPI<SpreeApiProvider>, CheckoutSchema>;
export type CheckoutEndpoint = CheckoutAPI['endpoint'];
export const handlers: CheckoutEndpoint['handlers'] = { getCheckout };
const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint,
handlers
});
export default checkoutApi;

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1,48 +0,0 @@
import type { CommerceAPI, CommerceAPIConfig } from '@commerce/api';
import { getCommerceApi as commerceApi } from '@commerce/api';
import createApiFetch from './utils/create-api-fetch';
import getAllPages from './operations/get-all-pages';
import getPage from './operations/get-page';
import getSiteInfo from './operations/get-site-info';
import getCustomerWishlist from './operations/get-customer-wishlist';
import getAllProductPaths from './operations/get-all-product-paths';
import getAllProducts from './operations/get-all-products';
import getProduct from './operations/get-product';
import getAllTaxons from './operations/get-all-taxons';
import getProducts from './operations/get-products';
export interface SpreeApiConfig extends CommerceAPIConfig {}
const config: SpreeApiConfig = {
commerceUrl: '',
apiToken: '',
cartCookie: '',
customerCookie: '',
cartCookieMaxAge: 2592000,
fetch: createApiFetch(() => getCommerceApi().getConfig())
};
const operations = {
getAllPages,
getPage,
getSiteInfo,
getCustomerWishlist,
getAllProductPaths,
getAllProducts,
getProduct,
getAllTaxons,
getProducts
};
export const provider = { config, operations };
export type SpreeApiProvider = typeof provider;
export type SpreeApi<P extends SpreeApiProvider = SpreeApiProvider> = CommerceAPI<P>;
export function getCommerceApi<P extends SpreeApiProvider>(
customProvider: P = provider as any
): SpreeApi<P> {
return commerceApi(customProvider);
}

View File

@ -1,72 +0,0 @@
import type { OperationContext, OperationOptions } from '@commerce/api/operations';
import type { GetAllPagesOperation, Page } from '@commerce/types/page';
import { requireConfigValue } from '../../isomorphic-config';
import normalizePage from '../../utils/normalizations/normalize-page';
import type { IPages } from '@spree/storefront-api-v2-sdk/types/interfaces/Page';
import type { SpreeSdkVariables } from '../../types';
import type { SpreeApiConfig, SpreeApiProvider } from '../index';
export default function getAllPagesOperation({ commerce }: OperationContext<SpreeApiProvider>) {
async function getAllPages<T extends GetAllPagesOperation>(options?: {
config?: Partial<SpreeApiConfig>;
preview?: boolean;
}): Promise<T['data']>;
async function getAllPages<T extends GetAllPagesOperation>(
opts: {
config?: Partial<SpreeApiConfig>;
preview?: boolean;
} & OperationOptions
): Promise<T['data']>;
async function getAllPages<T extends GetAllPagesOperation>({
config: userConfig,
preview,
query,
url
}: {
url?: string;
config?: Partial<SpreeApiConfig>;
preview?: boolean;
query?: string;
} = {}): Promise<T['data']> {
console.info(
'getAllPages called. Configuration: ',
'query: ',
query,
'userConfig: ',
userConfig,
'preview: ',
preview,
'url: ',
url
);
const config = commerce.getConfig(userConfig);
const { fetch: apiFetch } = config;
const variables: SpreeSdkVariables = {
methodPath: 'pages.list',
arguments: [
{
per_page: 500,
filter: {
locale_eq: config.locale || (requireConfigValue('defaultLocale') as string)
}
}
]
};
const { data: spreeSuccessResponse } = await apiFetch<IPages, SpreeSdkVariables>('__UNUSED__', {
variables
});
const normalizedPages: Page[] = spreeSuccessResponse.data.map<Page>((spreePage) =>
normalizePage(spreeSuccessResponse, spreePage, config.locales || [])
);
return { pages: normalizedPages };
}
return getAllPages;
}

View File

@ -1,91 +0,0 @@
import type { OperationContext, OperationOptions } from '@commerce/api/operations';
import type { Product } from '@commerce/types/product';
import type { GetAllProductPathsOperation } from '@commerce/types/product';
import { requireConfigValue } from '../../isomorphic-config';
import type { IProductsSlugs, SpreeSdkVariables } from '../../types';
import getProductPath from '../../utils/get-product-path';
import type { SpreeApiConfig, SpreeApiProvider } from '..';
const imagesSize = requireConfigValue('imagesSize') as string;
const imagesQuality = requireConfigValue('imagesQuality') as number;
export default function getAllProductPathsOperation({
commerce
}: OperationContext<SpreeApiProvider>) {
async function getAllProductPaths<T extends GetAllProductPathsOperation>(opts?: {
variables?: T['variables'];
config?: Partial<SpreeApiConfig>;
}): Promise<T['data']>;
async function getAllProductPaths<T extends GetAllProductPathsOperation>(
opts: {
variables?: T['variables'];
config?: Partial<SpreeApiConfig>;
} & OperationOptions
): Promise<T['data']>;
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
query,
variables: getAllProductPathsVariables = {},
config: userConfig
}: {
query?: string;
variables?: T['variables'];
config?: Partial<SpreeApiConfig>;
} = {}): Promise<T['data']> {
console.info(
'getAllProductPaths called. Configuration: ',
'query: ',
query,
'getAllProductPathsVariables: ',
getAllProductPathsVariables,
'config: ',
userConfig
);
const productsCount = requireConfigValue('lastUpdatedProductsPrerenderCount');
if (productsCount === 0) {
return {
products: []
};
}
const variables: SpreeSdkVariables = {
methodPath: 'products.list',
arguments: [
{},
{
fields: {
product: 'slug'
},
per_page: productsCount,
image_transformation: {
quality: imagesQuality,
size: imagesSize
}
}
]
};
const config = commerce.getConfig(userConfig);
const { fetch: apiFetch } = config; // TODO: Send config.locale to Spree.
const { data: spreeSuccessResponse } = await apiFetch<IProductsSlugs, SpreeSdkVariables>(
'__UNUSED__',
{
variables
}
);
const normalizedProductsPaths: Pick<Product, 'path'>[] = spreeSuccessResponse.data.map(
(spreeProduct) => ({
path: getProductPath(spreeProduct)
})
);
return { products: normalizedProductsPaths };
}
return getAllProductPaths;
}

View File

@ -1,84 +0,0 @@
import type { Product } from '@commerce/types/product';
import type { GetAllProductsOperation } from '@commerce/types/product';
import type { OperationContext, OperationOptions } from '@commerce/api/operations';
import type { IProducts } from '@spree/storefront-api-v2-sdk/types/interfaces/Product';
import type { SpreeApiConfig, SpreeApiProvider } from '../index';
import type { SpreeSdkVariables } from '../../types';
import normalizeProduct from '../../utils/normalizations/normalize-product';
import { requireConfigValue } from '../../isomorphic-config';
const imagesSize = requireConfigValue('imagesSize') as string;
const imagesQuality = requireConfigValue('imagesQuality') as number;
export default function getAllProductsOperation({ commerce }: OperationContext<SpreeApiProvider>) {
async function getAllProducts<T extends GetAllProductsOperation>(opts?: {
variables?: T['variables'];
config?: Partial<SpreeApiConfig>;
preview?: boolean;
}): Promise<T['data']>;
async function getAllProducts<T extends GetAllProductsOperation>(
opts: {
variables?: T['variables'];
config?: Partial<SpreeApiConfig>;
preview?: boolean;
} & OperationOptions
): Promise<T['data']>;
async function getAllProducts<T extends GetAllProductsOperation>({
variables: getAllProductsVariables = {},
config: userConfig
}: {
variables?: T['variables'];
config?: Partial<SpreeApiConfig>;
} = {}): Promise<{ products: Product[] }> {
console.info(
'getAllProducts called. Configuration: ',
'getAllProductsVariables: ',
getAllProductsVariables,
'config: ',
userConfig
);
const defaultProductsTaxonomyId = requireConfigValue('allProductsTaxonomyId') as string | false;
const first = getAllProductsVariables.first;
const filter = !defaultProductsTaxonomyId
? {}
: { filter: { taxons: defaultProductsTaxonomyId }, sort: '-updated_at' };
const variables: SpreeSdkVariables = {
methodPath: 'products.list',
arguments: [
{},
{
include: 'primary_variant,variants,images,option_types,variants.option_values',
per_page: first,
...filter,
image_transformation: {
quality: imagesQuality,
size: imagesSize
}
}
]
};
const config = commerce.getConfig(userConfig);
const { fetch: apiFetch } = config; // TODO: Send config.locale to Spree.
const { data: spreeSuccessResponse } = await apiFetch<IProducts, SpreeSdkVariables>(
'__UNUSED__',
{
variables
}
);
const normalizedProducts: Product[] = spreeSuccessResponse.data.map((spreeProduct) =>
normalizeProduct(spreeSuccessResponse, spreeProduct)
);
return { products: normalizedProducts };
}
return getAllProducts;
}

View File

@ -1,63 +0,0 @@
import _ from 'lodash';
import type { ITaxons, TaxonAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Taxon';
import type { SpreeSdkVariables } from '../../types';
const taxonsSort = (spreeTaxon1: TaxonAttr, spreeTaxon2: TaxonAttr): number => {
const { left: left1, right: right1 } = spreeTaxon1.attributes;
const { left: left2, right: right2 } = spreeTaxon2.attributes;
if (right1 < left2) {
return -1;
}
if (right2 < left1) {
return 1;
}
return 0;
};
const buildTaxonsTree = (taxons: TaxonAttr[], parentId: string | number): TaxonAttr[] => {
const children = _.chain(taxons)
.filter((item) => {
const relationships = item.relationships || {};
return parentId === _.get(relationships, 'parent.data.id');
})
.sort(taxonsSort)
.value();
return children.map((child) => ({
id: child.id,
name: child.attributes.name,
type: child.type,
position: child.attributes.position,
children: buildTaxonsTree(taxons, child.id)
}));
};
export default function getAllTaxonsOperation({ commerce, locale }) {
async function getAllTaxons(options = {}) {
const { config: userConfig } = options;
const config = commerce.getConfig(userConfig);
const { fetch: apiFetch } = config;
const variables: SpreeSdkVariables = {
methodPath: 'taxons.list',
arguments: [
{
locale: config.locale
}
]
};
const { data: spreeSuccessResponse } = await apiFetch('__UNUSED__', { variables });
const normalizedTaxons = buildTaxonsTree(spreeSuccessResponse.data, '1');
return { taxons: normalizedTaxons };
}
return getAllTaxons;
}

View File

@ -1,6 +0,0 @@
export default function getCustomerWishlistOperation() {
function getCustomerWishlist(): any {
return { wishlist: {} };
}
return getCustomerWishlist;
}

View File

@ -1,73 +0,0 @@
import type { OperationContext, OperationOptions } from '@commerce/api/operations';
import type { GetPageOperation } from '@commerce/types/page';
import type { SpreeSdkVariables } from '../../types';
import type { SpreeApiConfig, SpreeApiProvider } from '..';
import type { IPage } from '@spree/storefront-api-v2-sdk/types/interfaces/Page';
import normalizePage from '../../utils/normalizations/normalize-page';
export type Page = any;
export type GetPageResult = { page?: Page };
export type PageVariables = {
id: number;
};
export default function getPageOperation({ commerce }: OperationContext<SpreeApiProvider>) {
async function getPage<T extends GetPageOperation>(opts: {
variables: T['variables'];
config?: Partial<SpreeApiConfig>;
preview?: boolean;
}): Promise<T['data']>;
async function getPage<T extends GetPageOperation>(
opts: {
variables: T['variables'];
config?: Partial<SpreeApiConfig>;
preview?: boolean;
} & OperationOptions
): Promise<T['data']>;
async function getPage<T extends GetPageOperation>({
url,
config: userConfig,
preview,
variables: getPageVariables
}: {
url?: string;
variables: T['variables'];
config?: Partial<SpreeApiConfig>;
preview?: boolean;
}): Promise<T['data']> {
console.info(
'getPage called. Configuration: ',
'userConfig: ',
userConfig,
'preview: ',
preview,
'url: ',
url
);
const config = commerce.getConfig(userConfig);
const { fetch: apiFetch } = config;
const variables: SpreeSdkVariables = {
methodPath: 'pages.show',
arguments: [getPageVariables.id]
};
const { data: spreeSuccessResponse } = await apiFetch<IPage, SpreeSdkVariables>('__UNUSED__', {
variables
});
const normalizedPage: Page = normalizePage(
spreeSuccessResponse,
spreeSuccessResponse.data,
config.locales || []
);
return { page: normalizedPage };
}
return getPage;
}

View File

@ -1,81 +0,0 @@
import type { SpreeApiConfig, SpreeApiProvider } from '../index';
import type { GetProductOperation } from '@commerce/types/product';
import type { OperationContext, OperationOptions } from '@commerce/api/operations';
import type { IProduct } from '@spree/storefront-api-v2-sdk/types/interfaces/Product';
import type { SpreeSdkVariables } from '../../types';
import MissingSlugVariableError from '../../errors/MissingSlugVariableError';
import normalizeProduct from '../../utils/normalizations/normalize-product';
import { requireConfigValue } from '../../isomorphic-config';
const imagesSize = requireConfigValue('imagesSize') as string;
const imagesQuality = requireConfigValue('imagesQuality') as number;
export default function getProductOperation({ commerce }: OperationContext<SpreeApiProvider>) {
async function getProduct<T extends GetProductOperation>(opts: {
variables: T['variables'];
config?: Partial<SpreeApiConfig>;
preview?: boolean;
}): Promise<T['data']>;
async function getProduct<T extends GetProductOperation>(
opts: {
variables: T['variables'];
config?: Partial<SpreeApiConfig>;
preview?: boolean;
} & OperationOptions
): Promise<T['data']>;
async function getProduct<T extends GetProductOperation>({
query = '',
variables: getProductVariables,
config: userConfig
}: {
query?: string;
variables?: T['variables'];
config?: Partial<SpreeApiConfig>;
preview?: boolean;
}): Promise<T['data']> {
console.log(
'getProduct called. Configuration: ',
'getProductVariables: ',
getProductVariables,
'config: ',
userConfig
);
if (!getProductVariables?.slug) {
throw new MissingSlugVariableError();
}
const variables: SpreeSdkVariables = {
methodPath: 'products.show',
arguments: [
getProductVariables.slug,
{},
{
include: 'primary_variant,variants,images,option_types,variants.option_values',
image_transformation: {
quality: imagesQuality,
size: imagesSize
}
}
]
};
const config = commerce.getConfig(userConfig);
const { fetch: apiFetch } = config; // TODO: Send config.locale to Spree.
const { data: spreeSuccessResponse } = await apiFetch<IProduct, SpreeSdkVariables>(
'__UNUSED__',
{
variables
}
);
return {
product: normalizeProduct(spreeSuccessResponse, spreeSuccessResponse.data)
};
}
return getProduct;
}

View File

@ -1,37 +0,0 @@
import normalizeProduct from '../../utils/normalizations/normalize-product';
import { requireConfigValue } from '../../isomorphic-config';
const imagesSize = requireConfigValue('imagesSize');
const imagesQuality = requireConfigValue('imagesQuality');
export default function getProductsOperation({ commerce }) {
async function getProducts({ taxons = [], config: userConfig } = {}) {
const filter = { filter: { taxons: taxons.join(',') }, sort: '-updated_at' };
const variables = {
methodPath: 'products.list',
arguments: [
{
include: 'primary_variant,variants,images,option_types,variants.option_values',
per_page: 100,
...filter,
image_transformation: {
quality: imagesQuality,
size: imagesSize
}
}
]
};
const config = commerce.getConfig(userConfig);
const { fetch: apiFetch } = config;
const { data: spreeSuccessResponse } = await apiFetch('__UNUSED__', { variables });
const normalizedProducts = spreeSuccessResponse.data.map((spreeProduct) =>
normalizeProduct(spreeSuccessResponse, spreeProduct)
);
return { products: normalizedProducts };
}
return getProducts;
}

View File

@ -1,120 +0,0 @@
import type { OperationContext, OperationOptions } from '@commerce/api/operations';
import type { Category, GetSiteInfoOperation } from '@commerce/types/site';
import type { ITaxons, TaxonAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Taxon';
import { requireConfigValue } from '../../isomorphic-config';
import type { SpreeSdkVariables } from '../../types';
import type { SpreeApiConfig, SpreeApiProvider } from '..';
const taxonsSort = (spreeTaxon1: TaxonAttr, spreeTaxon2: TaxonAttr): number => {
const { left: left1, right: right1 } = spreeTaxon1.attributes;
const { left: left2, right: right2 } = spreeTaxon2.attributes;
if (right1 < left2) {
return -1;
}
if (right2 < left1) {
return 1;
}
return 0;
};
export type GetSiteInfoResult<
T extends { categories: any[]; brands: any[] } = {
categories: Category[];
brands: any[];
}
> = T;
export default function getSiteInfoOperation({ commerce }: OperationContext<SpreeApiProvider>) {
async function getSiteInfo<T extends GetSiteInfoOperation>(opts?: {
config?: Partial<SpreeApiConfig>;
preview?: boolean;
}): Promise<T['data']>;
async function getSiteInfo<T extends GetSiteInfoOperation>(
opts: {
config?: Partial<SpreeApiConfig>;
preview?: boolean;
} & OperationOptions
): Promise<T['data']>;
async function getSiteInfo<T extends GetSiteInfoOperation>({
query,
variables: getSiteInfoVariables = {},
config: userConfig
}: {
query?: string;
variables?: any;
config?: Partial<SpreeApiConfig>;
preview?: boolean;
} = {}): Promise<GetSiteInfoResult> {
console.info(
'getSiteInfo called. Configuration: ',
'query: ',
query,
'getSiteInfoVariables ',
getSiteInfoVariables,
'config: ',
userConfig
);
const createVariables = (parentPermalink: string): SpreeSdkVariables => ({
methodPath: 'taxons.list',
arguments: [
{
filter: {
parent_permalink: parentPermalink
}
}
]
});
const config = commerce.getConfig(userConfig);
const { fetch: apiFetch } = config; // TODO: Send config.locale to Spree.
const { data: spreeCategoriesSuccessResponse } = await apiFetch<ITaxons, SpreeSdkVariables>(
'__UNUSED__',
{
variables: createVariables(requireConfigValue('categoriesTaxonomyPermalink') as string)
}
);
const { data: spreeBrandsSuccessResponse } = await apiFetch<ITaxons, SpreeSdkVariables>(
'__UNUSED__',
{
variables: createVariables(requireConfigValue('brandsTaxonomyPermalink') as string)
}
);
const normalizedCategories: GetSiteInfoOperation['data']['categories'] =
spreeCategoriesSuccessResponse.data.sort(taxonsSort).map((spreeTaxon: TaxonAttr) => {
return {
id: spreeTaxon.id,
name: spreeTaxon.attributes.name,
slug: spreeTaxon.id,
path: spreeTaxon.id
};
});
const normalizedBrands: GetSiteInfoOperation['data']['brands'] = spreeBrandsSuccessResponse.data
.sort(taxonsSort)
.map((spreeTaxon: TaxonAttr) => {
return {
node: {
entityId: spreeTaxon.id,
path: `brands/${spreeTaxon.id}`,
name: spreeTaxon.attributes.name
}
};
});
return {
categories: normalizedCategories,
brands: normalizedBrands
};
}
return getSiteInfo;
}

View File

@ -1,8 +0,0 @@
export { default as getPage } from './get-page';
export { default as getSiteInfo } from './get-site-info';
export { default as getAllPages } from './get-all-pages';
export { default as getProduct } from './get-product';
export { default as getAllProducts } from './get-all-products';
export { default as getAllProductPaths } from './get-all-product-paths';
export { default as getAllTaxons } from './get-all-taxons';
export { default as getProducts } from './get-products';

View File

@ -1,74 +0,0 @@
import { SpreeApiConfig } from '..';
import { errors, makeClient } from '@spree/storefront-api-v2-sdk';
import { requireConfigValue } from '../../isomorphic-config';
import convertSpreeErrorToGraphQlError from '../../utils/convert-spree-error-to-graph-ql-error';
import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse';
import getSpreeSdkMethodFromEndpointPath from '../../utils/get-spree-sdk-method-from-endpoint-path';
import SpreeSdkMethodFromEndpointPathError from '../../errors/SpreeSdkMethodFromEndpointPathError';
import { GraphQLFetcher, GraphQLFetcherResult } from '@commerce/api';
import createCustomizedFetchFetcher, {
fetchResponseKey
} from '../../utils/create-customized-fetch-fetcher';
import fetch, { Request } from 'node-fetch';
import type { SpreeSdkResponseWithRawResponse } from '../../types';
export type CreateApiFetch = (
getConfig: () => SpreeApiConfig
) => GraphQLFetcher<GraphQLFetcherResult<any>, any>;
// TODO: GraphQLFetcher<GraphQLFetcherResult<any>, any> should be GraphQLFetcher<GraphQLFetcherResult<any>, SpreeSdkVariables>.
// But CommerceAPIConfig['fetch'] cannot be extended from Variables = any to SpreeSdkVariables.
const createApiFetch: CreateApiFetch = (_getConfig) => {
const client = makeClient({
host: requireConfigValue('apiHost') as string,
createFetcher: (fetcherOptions) => {
return createCustomizedFetchFetcher({
fetch,
requestConstructor: Request,
...fetcherOptions
});
}
});
return async (url, queryData = {}, fetchOptions = {}) => {
console.log(
'apiFetch called. query = ',
'url = ',
url,
'queryData = ',
queryData,
'fetchOptions = ',
fetchOptions
);
const { variables } = queryData;
if (!variables) {
throw new SpreeSdkMethodFromEndpointPathError(`Required SpreeSdkVariables not provided.`);
}
const storeResponse: ResultResponse<SpreeSdkResponseWithRawResponse> =
await getSpreeSdkMethodFromEndpointPath(client, variables.methodPath)(...variables.arguments);
if (storeResponse.isSuccess()) {
const data = storeResponse.success();
const rawFetchResponse = data[fetchResponseKey];
return {
data,
res: rawFetchResponse
};
}
const storeResponseError = storeResponse.fail();
if (storeResponseError instanceof errors.SpreeError) {
throw convertSpreeErrorToGraphQlError(storeResponseError);
}
throw storeResponseError;
};
};
export default createApiFetch;

View File

@ -1,3 +0,0 @@
import vercelFetch from '@vercel/fetch';
export default vercelFetch();

View File

@ -1,3 +0,0 @@
export { default as useLogin } from './use-login';
export { default as useLogout } from './use-logout';
export { default as useSignup } from './use-signup';

View File

@ -1,81 +0,0 @@
import { useCallback } from 'react';
import type { MutationHook } from '@commerce/utils/types';
import useLogin, { UseLogin } from '@commerce/auth/use-login';
import type { LoginHook } from '@commerce/types/login';
import type { AuthTokenAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Authentication';
import { FetcherError, ValidationError } from '@commerce/utils/errors';
import useCustomer from '../customer/use-customer';
import useCart from '../cart/use-cart';
import useWishlist from '../wishlist/use-wishlist';
import login from '../utils/login';
export default useLogin as UseLogin<typeof handler>;
export const handler: MutationHook<LoginHook> = {
// Provide fetchOptions for SWR cache key
fetchOptions: {
url: 'authentication',
query: 'getToken'
},
async fetcher({ input, options, fetch }) {
console.info(
'useLogin fetcher called. Configuration: ',
'input: ',
input,
'options: ',
options
);
const { email, password } = input;
if (!email || !password) {
throw new ValidationError({
message: 'Email and password need to be provided.'
});
}
const getTokenParameters: AuthTokenAttr = {
username: email,
password
};
try {
await login(fetch, getTokenParameters, false);
return null;
} catch (getTokenError) {
if (getTokenError instanceof FetcherError && getTokenError.status === 400) {
// Change the error message to be more user friendly.
throw new FetcherError({
status: getTokenError.status,
message: 'The email or password is invalid.',
code: getTokenError.code
});
}
throw getTokenError;
}
},
useHook: ({ fetch }) => {
const useWrappedHook: ReturnType<MutationHook<LoginHook>['useHook']> = () => {
const customer = useCustomer();
const cart = useCart();
const wishlist = useWishlist();
return useCallback(
async function login(input) {
const data = await fetch({ input });
await customer.revalidate();
await cart.revalidate();
await wishlist.revalidate();
return data;
},
[customer, cart, wishlist]
);
};
return useWrappedHook;
}
};

View File

@ -1,79 +0,0 @@
import { MutationHook } from '@commerce/utils/types';
import useLogout, { UseLogout } from '@commerce/auth/use-logout';
import type { LogoutHook } from '@commerce/types/logout';
import { useCallback } from 'react';
import useCustomer from '../customer/use-customer';
import useCart from '../cart/use-cart';
import useWishlist from '../wishlist/use-wishlist';
import {
ensureUserTokenResponse,
removeUserTokenResponse
} from '../utils/tokens/user-token-response';
import revokeUserTokens from '../utils/tokens/revoke-user-tokens';
import TokensNotRejectedError from '../errors/TokensNotRejectedError';
export default useLogout as UseLogout<typeof handler>;
export const handler: MutationHook<LogoutHook> = {
// Provide fetchOptions for SWR cache key
fetchOptions: {
url: 'authentication',
query: 'revokeToken'
},
async fetcher({ input, options, fetch }) {
console.info(
'useLogout fetcher called. Configuration: ',
'input: ',
input,
'options: ',
options
);
const userToken = ensureUserTokenResponse();
if (userToken) {
try {
// Revoke any tokens associated with the logged in user.
await revokeUserTokens(fetch, {
accessToken: userToken.access_token,
refreshToken: userToken.refresh_token
});
} catch (revokeUserTokenError) {
// Squash token revocation errors and rethrow anything else.
if (!(revokeUserTokenError instanceof TokensNotRejectedError)) {
throw revokeUserTokenError;
}
}
// Whether token revocation succeeded or not, remove them from local storage.
removeUserTokenResponse();
}
return null;
},
useHook: ({ fetch }) => {
const useWrappedHook: ReturnType<MutationHook<LogoutHook>['useHook']> = () => {
const customer = useCustomer({
swrOptions: { isPaused: () => true }
});
const cart = useCart({
swrOptions: { isPaused: () => true }
});
const wishlist = useWishlist({
swrOptions: { isPaused: () => true }
});
return useCallback(async () => {
const data = await fetch();
await customer.mutate(null, false);
await cart.mutate(null, false);
await wishlist.mutate(null, false);
return data;
}, [customer, cart, wishlist]);
};
return useWrappedHook;
}
};

View File

@ -1,94 +0,0 @@
import { useCallback } from 'react';
import type { GraphQLFetcherResult } from '@commerce/api';
import type { MutationHook } from '@commerce/utils/types';
import useSignup, { UseSignup } from '@commerce/auth/use-signup';
import type { SignupHook } from '@commerce/types/signup';
import { ValidationError } from '@commerce/utils/errors';
import type { IAccount } from '@spree/storefront-api-v2-sdk/types/interfaces/Account';
import type { AuthTokenAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Authentication';
import useCustomer from '../customer/use-customer';
import useCart from '../cart/use-cart';
import useWishlist from '../wishlist/use-wishlist';
import login from '../utils/login';
import { requireConfigValue } from '../isomorphic-config';
export default useSignup as UseSignup<typeof handler>;
export const handler: MutationHook<SignupHook> = {
// Provide fetchOptions for SWR cache key
fetchOptions: {
url: 'account',
query: 'create'
},
async fetcher({ input, options, fetch }) {
console.info(
'useSignup fetcher called. Configuration: ',
'input: ',
input,
'options: ',
options
);
const { email, password } = input;
if (!email || !password) {
throw new ValidationError({
message: 'Email and password need to be provided.'
});
}
// TODO: Replace any with specific type from Spree SDK
// once it's added to the SDK.
const createAccountParameters: any = {
user: {
email,
password,
// The stock NJC interface doesn't have a
// password confirmation field, so just copy password.
passwordConfirmation: password
}
};
// Create the user account.
await fetch<GraphQLFetcherResult<IAccount>>({
variables: {
methodPath: 'account.create',
arguments: [createAccountParameters]
}
});
const getTokenParameters: AuthTokenAttr = {
username: email,
password
};
// Login immediately after the account is created.
if (requireConfigValue('loginAfterSignup')) {
await login(fetch, getTokenParameters, true);
}
return null;
},
useHook: ({ fetch }) => {
const useWrappedHook: ReturnType<MutationHook<SignupHook>['useHook']> = () => {
const customer = useCustomer();
const cart = useCart();
const wishlist = useWishlist();
return useCallback(
async (input) => {
const data = await fetch({ input });
await customer.revalidate();
await cart.revalidate();
await wishlist.revalidate();
return data;
},
[customer, cart, wishlist]
);
};
return useWrappedHook;
}
};

View File

@ -1,4 +0,0 @@
export { default as useCart } from './use-cart';
export { default as useAddItem } from './use-add-item';
export { default as useRemoveItem } from './use-remove-item';
export { default as useUpdateItem } from './use-update-item';

View File

@ -1,109 +0,0 @@
import useAddItem from '@commerce/cart/use-add-item';
import type { UseAddItem } from '@commerce/cart/use-add-item';
import type { MutationHook } from '@commerce/utils/types';
import { useCallback } from 'react';
import useCart from './use-cart';
import type { AddItemHook } from '@commerce/types/cart';
import normalizeCart from '../utils/normalizations/normalize-cart';
import type { GraphQLFetcherResult } from '@commerce/api';
import type { IOrder } from '@spree/storefront-api-v2-sdk/types/interfaces/Order';
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token';
import type { AddItem } from '@spree/storefront-api-v2-sdk/types/interfaces/endpoints/CartClass';
import { setCartToken } from '../utils/tokens/cart-token';
import ensureIToken from '../utils/tokens/ensure-itoken';
import createEmptyCart from '../utils/create-empty-cart';
import { FetcherError } from '@commerce/utils/errors';
import isLoggedIn from '../utils/tokens/is-logged-in';
export default useAddItem as UseAddItem<typeof handler>;
export const handler: MutationHook<AddItemHook> = {
// Provide fetchOptions for SWR cache key
fetchOptions: {
url: 'cart',
query: 'addItem'
},
async fetcher({ input, options, fetch }) {
console.info(
'useAddItem fetcher called. Configuration: ',
'input: ',
input,
'options: ',
options
);
const { quantity, productId, variantId } = input;
const safeQuantity = quantity ?? 1;
let token: IToken | undefined = ensureIToken();
const addItemParameters: AddItem = {
variant_id: variantId,
quantity: safeQuantity,
include: [
'line_items',
'line_items.variant',
'line_items.variant.product',
'line_items.variant.product.images',
'line_items.variant.images',
'line_items.variant.option_values',
'line_items.variant.product.option_types'
].join(',')
};
if (!token) {
const { data: spreeCartCreateSuccessResponse } = await createEmptyCart(fetch);
setCartToken(spreeCartCreateSuccessResponse.data.attributes.token);
token = ensureIToken();
}
try {
const { data: spreeSuccessResponse } = await fetch<GraphQLFetcherResult<IOrder>>({
variables: {
methodPath: 'cart.addItem',
arguments: [token, addItemParameters]
}
});
return normalizeCart(spreeSuccessResponse, spreeSuccessResponse.data);
} catch (addItemError) {
if (addItemError instanceof FetcherError && addItemError.status === 404) {
const { data: spreeRetroactiveCartCreateSuccessResponse } = await createEmptyCart(fetch);
if (!isLoggedIn()) {
setCartToken(spreeRetroactiveCartCreateSuccessResponse.data.attributes.token);
}
// Return an empty cart. The user has to add the item again.
// This is going to be a rare situation.
return normalizeCart(
spreeRetroactiveCartCreateSuccessResponse,
spreeRetroactiveCartCreateSuccessResponse.data
);
}
throw addItemError;
}
},
useHook: ({ fetch }) => {
const useWrappedHook: ReturnType<MutationHook<AddItemHook>['useHook']> = () => {
const { mutate } = useCart();
return useCallback(
async (input) => {
const data = await fetch({ input });
await mutate(data, false);
return data;
},
[mutate]
);
};
return useWrappedHook;
}
};

View File

@ -1,108 +0,0 @@
import { useMemo } from 'react';
import type { SWRHook } from '@commerce/utils/types';
import useCart from '@commerce/cart/use-cart';
import type { UseCart } from '@commerce/cart/use-cart';
import type { GetCartHook } from '@commerce/types/cart';
import normalizeCart from '../utils/normalizations/normalize-cart';
import type { GraphQLFetcherResult } from '@commerce/api';
import type { IOrder } from '@spree/storefront-api-v2-sdk/types/interfaces/Order';
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token';
import { FetcherError } from '@commerce/utils/errors';
import { setCartToken } from '../utils/tokens/cart-token';
import ensureIToken from '../utils/tokens/ensure-itoken';
import isLoggedIn from '../utils/tokens/is-logged-in';
import createEmptyCart from '../utils/create-empty-cart';
import { requireConfigValue } from '../isomorphic-config';
const imagesSize = requireConfigValue('imagesSize') as string;
const imagesQuality = requireConfigValue('imagesQuality') as number;
export default useCart as UseCart<typeof handler>;
// This handler avoids calling /api/cart.
// There doesn't seem to be a good reason to call it.
// So far, only @framework/bigcommerce uses it.
export const handler: SWRHook<GetCartHook> = {
// Provide fetchOptions for SWR cache key
fetchOptions: {
url: 'cart',
query: 'show'
},
async fetcher({ input, options, fetch }) {
console.info('useCart fetcher called. Configuration: ', 'input: ', input, 'options: ', options);
let spreeCartResponse: IOrder | null;
const token: IToken | undefined = ensureIToken();
if (!token) {
spreeCartResponse = null;
} else {
try {
const { data: spreeCartShowSuccessResponse } = await fetch<GraphQLFetcherResult<IOrder>>({
variables: {
methodPath: 'cart.show',
arguments: [
token,
{
include: [
'line_items',
'line_items.variant',
'line_items.variant.product',
'line_items.variant.product.images',
'line_items.variant.images',
'line_items.variant.option_values',
'line_items.variant.product.option_types'
].join(','),
image_transformation: {
quality: imagesQuality,
size: imagesSize
}
}
]
}
});
spreeCartResponse = spreeCartShowSuccessResponse;
} catch (fetchCartError) {
if (!(fetchCartError instanceof FetcherError) || fetchCartError.status !== 404) {
throw fetchCartError;
}
spreeCartResponse = null;
}
}
if (!spreeCartResponse || spreeCartResponse?.data.attributes.completed_at) {
const { data: spreeCartCreateSuccessResponse } = await createEmptyCart(fetch);
spreeCartResponse = spreeCartCreateSuccessResponse;
if (!isLoggedIn()) {
setCartToken(spreeCartResponse.data.attributes.token);
}
}
return normalizeCart(spreeCartResponse, spreeCartResponse.data);
},
useHook: ({ useData }) => {
const useWrappedHook: ReturnType<SWRHook<GetCartHook>['useHook']> = (input) => {
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions }
});
return useMemo<typeof response & { isEmpty: boolean }>(() => {
return Object.create(response, {
isEmpty: {
get() {
return (response.data?.lineItems.length ?? 0) === 0;
},
enumerable: true
}
});
}, [response]);
};
return useWrappedHook;
}
};

View File

@ -1,107 +0,0 @@
import type { MutationHook } from '@commerce/utils/types';
import useRemoveItem from '@commerce/cart/use-remove-item';
import type { UseRemoveItem } from '@commerce/cart/use-remove-item';
import type { RemoveItemHook } from '@commerce/types/cart';
import useCart from './use-cart';
import { useCallback } from 'react';
import normalizeCart from '../utils/normalizations/normalize-cart';
import type { IOrder } from '@spree/storefront-api-v2-sdk/types/interfaces/Order';
import type { GraphQLFetcherResult } from '@commerce/api';
import type { IQuery } from '@spree/storefront-api-v2-sdk/types/interfaces/Query';
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token';
import ensureIToken from '../utils/tokens/ensure-itoken';
import createEmptyCart from '../utils/create-empty-cart';
import { setCartToken } from '../utils/tokens/cart-token';
import { FetcherError } from '@commerce/utils/errors';
import isLoggedIn from '../utils/tokens/is-logged-in';
export default useRemoveItem as UseRemoveItem<typeof handler>;
export const handler: MutationHook<RemoveItemHook> = {
// Provide fetchOptions for SWR cache key
fetchOptions: {
url: 'cart',
query: 'removeItem'
},
async fetcher({ input, options, fetch }) {
console.info(
'useRemoveItem fetcher called. Configuration: ',
'input: ',
input,
'options: ',
options
);
const { itemId: lineItemId } = input;
let token: IToken | undefined = ensureIToken();
if (!token) {
const { data: spreeCartCreateSuccessResponse } = await createEmptyCart(fetch);
setCartToken(spreeCartCreateSuccessResponse.data.attributes.token);
token = ensureIToken();
}
const removeItemParameters: IQuery = {
include: [
'line_items',
'line_items.variant',
'line_items.variant.product',
'line_items.variant.product.images',
'line_items.variant.images',
'line_items.variant.option_values',
'line_items.variant.product.option_types'
].join(',')
};
try {
const { data: spreeSuccessResponse } = await fetch<GraphQLFetcherResult<IOrder>>({
variables: {
methodPath: 'cart.removeItem',
arguments: [token, lineItemId, removeItemParameters]
}
});
return normalizeCart(spreeSuccessResponse, spreeSuccessResponse.data);
} catch (removeItemError) {
if (removeItemError instanceof FetcherError && removeItemError.status === 404) {
const { data: spreeRetroactiveCartCreateSuccessResponse } = await createEmptyCart(fetch);
if (!isLoggedIn()) {
setCartToken(spreeRetroactiveCartCreateSuccessResponse.data.attributes.token);
}
// Return an empty cart. This is going to be a rare situation.
return normalizeCart(
spreeRetroactiveCartCreateSuccessResponse,
spreeRetroactiveCartCreateSuccessResponse.data
);
}
throw removeItemError;
}
},
useHook: ({ fetch }) => {
const useWrappedHook: ReturnType<MutationHook<RemoveItemHook>['useHook']> = () => {
const { mutate } = useCart();
return useCallback(
async (input) => {
const data = await fetch({ input: { itemId: input.id } });
// Upon calling cart.removeItem, Spree returns the old version of the cart,
// with the already removed line item. Invalidate the useCart mutation
// to fetch the cart again.
await mutate(data, true);
return data;
},
[mutate]
);
};
return useWrappedHook;
}
};

View File

@ -1,134 +0,0 @@
import type { MutationHook } from '@commerce/utils/types';
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item';
import type { UpdateItemHook } from '@commerce/types/cart';
import useCart from './use-cart';
import { useMemo } from 'react';
import { FetcherError, ValidationError } from '@commerce/utils/errors';
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token';
import type { SetQuantity } from '@spree/storefront-api-v2-sdk/types/interfaces/endpoints/CartClass';
import type { GraphQLFetcherResult } from '@commerce/api';
import type { IOrder } from '@spree/storefront-api-v2-sdk/types/interfaces/Order';
import normalizeCart from '../utils/normalizations/normalize-cart';
import debounce from 'lodash.debounce';
import ensureIToken from '../utils/tokens/ensure-itoken';
import createEmptyCart from '../utils/create-empty-cart';
import { setCartToken } from '../utils/tokens/cart-token';
import isLoggedIn from '../utils/tokens/is-logged-in';
export default useUpdateItem as UseUpdateItem<any>;
export const handler: MutationHook<UpdateItemHook> = {
// Provide fetchOptions for SWR cache key
fetchOptions: {
url: 'cart',
query: 'setQuantity'
},
async fetcher({ input, options, fetch }) {
console.info(
'useRemoveItem fetcher called. Configuration: ',
'input: ',
input,
'options: ',
options
);
const { itemId, item } = input;
if (!item.quantity) {
throw new ValidationError({
message: 'Line item quantity needs to be provided.'
});
}
let token: IToken | undefined = ensureIToken();
if (!token) {
const { data: spreeCartCreateSuccessResponse } = await createEmptyCart(fetch);
setCartToken(spreeCartCreateSuccessResponse.data.attributes.token);
token = ensureIToken();
}
try {
const setQuantityParameters: SetQuantity = {
line_item_id: itemId,
quantity: item.quantity,
include: [
'line_items',
'line_items.variant',
'line_items.variant.product',
'line_items.variant.product.images',
'line_items.variant.images',
'line_items.variant.option_values',
'line_items.variant.product.option_types'
].join(',')
};
const { data: spreeSuccessResponse } = await fetch<GraphQLFetcherResult<IOrder>>({
variables: {
methodPath: 'cart.setQuantity',
arguments: [token, setQuantityParameters]
}
});
return normalizeCart(spreeSuccessResponse, spreeSuccessResponse.data);
} catch (updateItemError) {
if (updateItemError instanceof FetcherError && updateItemError.status === 404) {
const { data: spreeRetroactiveCartCreateSuccessResponse } = await createEmptyCart(fetch);
if (!isLoggedIn()) {
setCartToken(spreeRetroactiveCartCreateSuccessResponse.data.attributes.token);
}
// Return an empty cart. The user has to update the item again.
// This is going to be a rare situation.
return normalizeCart(
spreeRetroactiveCartCreateSuccessResponse,
spreeRetroactiveCartCreateSuccessResponse.data
);
}
throw updateItemError;
}
},
useHook: ({ fetch }) => {
const useWrappedHook: ReturnType<MutationHook<UpdateItemHook>['useHook']> = (context) => {
const { mutate } = useCart();
return useMemo(
() =>
debounce(async (input: UpdateItemHook['actionInput']) => {
const itemId = context?.item?.id;
const productId = input.productId ?? context?.item?.productId;
const variantId = input.variantId ?? context?.item?.variantId;
const quantity = input.quantity;
if (!itemId || !productId || !variantId) {
throw new ValidationError({
message: 'Invalid input used for this operation'
});
}
const data = await fetch({
input: {
item: {
productId,
variantId,
quantity
},
itemId
}
});
await mutate(data, false);
return data;
}, context?.wait ?? 500),
[mutate, context]
);
};
return useWrappedHook;
}
};

View File

@ -1,17 +0,0 @@
import { SWRHook } from '@commerce/utils/types';
import useCheckout, { UseCheckout } from '@commerce/checkout/use-checkout';
export default useCheckout as UseCheckout<typeof handler>;
export const handler: SWRHook<any> = {
// Provide fetchOptions for SWR cache key
fetchOptions: {
// TODO: Revise url and query
url: 'checkout',
query: 'show'
},
async fetcher({ input, options, fetch }) {},
useHook:
({ useData }) =>
async (input) => ({})
};

View File

@ -1,10 +0,0 @@
{
"provider": "spree",
"features": {
"wishlist": true,
"cart": true,
"search": true,
"customerAuth": true,
"customCheckout": false
}
}

View File

@ -1,18 +0,0 @@
import useAddItem from '@commerce/customer/address/use-add-item';
import type { UseAddItem } from '@commerce/customer/address/use-add-item';
import type { MutationHook } from '@commerce/utils/types';
export default useAddItem as UseAddItem<typeof handler>;
export const handler: MutationHook<any> = {
// Provide fetchOptions for SWR cache key
fetchOptions: {
url: 'account',
query: 'createAddress'
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({})
};

View File

@ -1,19 +0,0 @@
import useAddItem from '@commerce/customer/address/use-add-item';
import type { UseAddItem } from '@commerce/customer/address/use-add-item';
import type { MutationHook } from '@commerce/utils/types';
export default useAddItem as UseAddItem<typeof handler>;
export const handler: MutationHook<any> = {
// Provide fetchOptions for SWR cache key
fetchOptions: {
// TODO: Revise url and query
url: 'checkout',
query: 'addPayment'
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({})
};

Some files were not shown because too many files have changed in this diff Show More