mirror of
https://github.com/vercel/commerce.git
synced 2025-06-15 20:01:21 +00:00
refactoring
This commit is contained in:
parent
34845604e5
commit
e496235adc
@ -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>
|
||||
);
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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} />;
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
© {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>
|
||||
);
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
import Cart from 'components/cart';
|
||||
import OpenCart from 'components/cart/open-cart';
|
||||
import LogoSquare from 'components/logo-square';
|
||||
import { getMenu } from 'lib/shopify';
|
||||
import { Menu } from 'lib/shopify/types';
|
||||
import Link from 'next/link';
|
||||
import { Suspense } from 'react';
|
||||
import MobileMenu from './mobile-menu';
|
||||
import Search, { SearchSkeleton } from './search';
|
||||
const { SITE_NAME } = process.env;
|
||||
|
||||
export default async function Navbar() {
|
||||
const menu = await getMenu('next-js-frontend-header-menu');
|
||||
|
||||
return (
|
||||
<nav className="relative flex items-center justify-between p-4 lg:px-6">
|
||||
<div className="block flex-none md:hidden">
|
||||
<Suspense fallback={null}>
|
||||
<MobileMenu menu={menu} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="flex w-full md:w-1/3">
|
||||
<Link href="/" className="mr-2 flex w-full items-center justify-center md:w-auto lg:mr-6">
|
||||
<LogoSquare />
|
||||
<div className="ml-2 flex-none text-sm font-medium uppercase md:hidden lg:block">
|
||||
{SITE_NAME}
|
||||
</div>
|
||||
</Link>
|
||||
{menu.length ? (
|
||||
<ul className="hidden gap-6 text-sm md:flex md:items-center">
|
||||
{menu.map((item: Menu) => (
|
||||
<li key={item.title}>
|
||||
<Link
|
||||
href={item.path}
|
||||
className="text-neutral-500 underline-offset-4 hover:text-black hover:underline dark:text-neutral-400 dark:hover:text-neutral-300"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="hidden justify-center md:flex md:w-1/3">
|
||||
<Suspense fallback={<SearchSkeleton />}>
|
||||
<Search />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="flex justify-end md:w-1/3">
|
||||
<Suspense fallback={<OpenCart />}>
|
||||
<Cart />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { Fragment, Suspense, useEffect, useState } from 'react';
|
||||
|
||||
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { Menu } from 'lib/shopify/types';
|
||||
import Search, { SearchSkeleton } from './search';
|
||||
|
||||
export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const openMobileMenu = () => setIsOpen(true);
|
||||
const closeMobileMenu = () => setIsOpen(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth > 768) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(false);
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={openMobileMenu}
|
||||
aria-label="Open mobile menu"
|
||||
className="flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors md:hidden dark:border-neutral-700 dark:text-white"
|
||||
>
|
||||
<Bars3Icon className="h-4" />
|
||||
</button>
|
||||
<Transition show={isOpen}>
|
||||
<Dialog onClose={closeMobileMenu} className="relative z-50">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in-out duration-300"
|
||||
enterFrom="opacity-0 backdrop-blur-none"
|
||||
enterTo="opacity-100 backdrop-blur-[.5px]"
|
||||
leave="transition-all ease-in-out duration-200"
|
||||
leaveFrom="opacity-100 backdrop-blur-[.5px]"
|
||||
leaveTo="opacity-0 backdrop-blur-none"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
|
||||
</Transition.Child>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition-all ease-in-out duration-300"
|
||||
enterFrom="translate-x-[-100%]"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition-all ease-in-out duration-200"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-[-100%]"
|
||||
>
|
||||
<Dialog.Panel className="fixed bottom-0 left-0 right-0 top-0 flex h-full w-full flex-col bg-white pb-6 dark:bg-black">
|
||||
<div className="p-4">
|
||||
<button
|
||||
className="mb-4 flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white"
|
||||
onClick={closeMobileMenu}
|
||||
aria-label="Close mobile menu"
|
||||
>
|
||||
<XMarkIcon className="h-6" />
|
||||
</button>
|
||||
|
||||
<div className="mb-4 w-full">
|
||||
<Suspense fallback={<SearchSkeleton />}>
|
||||
<Search />
|
||||
</Suspense>
|
||||
</div>
|
||||
{menu.length ? (
|
||||
<ul className="flex w-full flex-col">
|
||||
{menu.map((item: Menu) => (
|
||||
<li
|
||||
className="py-2 text-xl text-black transition-colors hover:text-neutral-500 dark:text-white"
|
||||
key={item.title}
|
||||
>
|
||||
<Link href={item.path} onClick={closeMobileMenu}>
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function Search() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
const val = e.target as HTMLFormElement;
|
||||
const search = val.search as HTMLInputElement;
|
||||
const newParams = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (search.value) {
|
||||
newParams.set('q', search.value);
|
||||
} else {
|
||||
newParams.delete('q');
|
||||
}
|
||||
|
||||
router.push(createUrl('/search', newParams));
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
|
||||
<input
|
||||
key={searchParams?.get('q')}
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="Search for products..."
|
||||
autoComplete="off"
|
||||
defaultValue={searchParams?.get('q') || ''}
|
||||
className="w-full rounded-lg border bg-white px-4 py-2 text-sm text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
|
||||
<MagnifyingGlassIcon className="h-4" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchSkeleton() {
|
||||
return (
|
||||
<form className="w-max-[550px] relative w-full lg:w-80 xl:w-full">
|
||||
<input
|
||||
placeholder="Search for products..."
|
||||
className="w-full rounded-lg border bg-white px-4 py-2 text-sm text-black placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<div className="absolute right-0 top-0 mr-3 flex h-full items-center">
|
||||
<MagnifyingGlassIcon className="h-4" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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} />;
|
||||
}
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
}
|
@ -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;
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
));
|
||||
}
|
@ -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
23
app/diseases/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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
11
app/ondemand/layout.tsx
Normal 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
3
app/ondemand/page.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default async function OndemandPage() {
|
||||
return <h3>FUCK page</h3>;
|
||||
}
|
0
app/ondemand/products/[id]/page.tsx
Normal file
0
app/ondemand/products/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
14
app/page.tsx
14
app/page.tsx
@ -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
10
app/tests/[id]/page.tsx
Normal 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
24
app/tests/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
5
components/contact-us.tsx
Normal file
5
components/contact-us.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
const ContactUs = () => {
|
||||
return <div>Contact Us Block</div>;
|
||||
};
|
||||
|
||||
export default ContactUs;
|
13
components/diseases/diseases-and-conditions.tsx
Normal file
13
components/diseases/diseases-and-conditions.tsx
Normal 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;
|
15
components/diseases/specialities.tsx
Normal file
15
components/diseases/specialities.tsx
Normal 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;
|
11
components/home/quicklinks.tsx
Normal file
11
components/home/quicklinks.tsx
Normal 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>
|
||||
);
|
||||
}
|
5
components/latest-news.tsx
Normal file
5
components/latest-news.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
const LatestNews = () => {
|
||||
return <div>Latest News Block</div>;
|
||||
};
|
||||
|
||||
export default LatestNews;
|
0
components/layout/base-layout.tsx
Normal file
0
components/layout/base-layout.tsx
Normal 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>© 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>
|
||||
© {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;
|
||||
|
13
components/layout/header.tsx
Normal file
13
components/layout/header.tsx
Normal 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>
|
||||
);
|
||||
}
|
5
components/layout/hero/hero-1.tsx
Normal file
5
components/layout/hero/hero-1.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
const Hero1 = () => {
|
||||
return <div>Hero 1 Block</div>;
|
||||
};
|
||||
|
||||
export default Hero1;
|
5
components/layout/hero/hero-2.tsx
Normal file
5
components/layout/hero/hero-2.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
const Hero2 = () => {
|
||||
return <div>Hero 2 Block</div>;
|
||||
};
|
||||
|
||||
export default Hero2;
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
956
components/layout/topbar.tsx
Normal file
956
components/layout/topbar.tsx
Normal 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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>
|
||||
);
|
||||
}
|
93
components/ondemand/navbar/index.tsx
Normal file
93
components/ondemand/navbar/index.tsx
Normal 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;
|
5
components/science-innovation.tsx
Normal file
5
components/science-innovation.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
const ScienceInnovation = () => {
|
||||
return <div>Sience Innovation Block</div>;
|
||||
};
|
||||
|
||||
export default ScienceInnovation;
|
3
components/tests/alphabet.tsx
Normal file
3
components/tests/alphabet.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function Alphabet() {
|
||||
return <nav className="responsivegrid container">Alphabet</nav>;
|
||||
}
|
28
components/tests/tests-table.tsx
Normal file
28
components/tests/tests-table.tsx
Normal 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 |
@ -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/
|
@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@ -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;
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@ -1 +0,0 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export default function getCustomerWishlistOperation() {
|
||||
function getCustomerWishlist(): any {
|
||||
return { wishlist: {} };
|
||||
}
|
||||
return getCustomerWishlist;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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';
|
@ -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;
|
@ -1,3 +0,0 @@
|
||||
import vercelFetch from '@vercel/fetch';
|
||||
|
||||
export default vercelFetch();
|
@ -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';
|
@ -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;
|
||||
}
|
||||
};
|
@ -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;
|
||||
}
|
||||
};
|
@ -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;
|
||||
}
|
||||
};
|
@ -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';
|
@ -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;
|
||||
}
|
||||
};
|
@ -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;
|
||||
}
|
||||
};
|
@ -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;
|
||||
}
|
||||
};
|
@ -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;
|
||||
}
|
||||
};
|
@ -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) => ({})
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"provider": "spree",
|
||||
"features": {
|
||||
"wishlist": true,
|
||||
"cart": true,
|
||||
"search": true,
|
||||
"customerAuth": true,
|
||||
"customCheckout": false
|
||||
}
|
||||
}
|
@ -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 () => ({})
|
||||
};
|
@ -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
Loading…
x
Reference in New Issue
Block a user