mirror of
https://github.com/vercel/commerce.git
synced 2025-05-22 09:26:59 +00:00
feat: project initial setup
This commit is contained in:
parent
f5dade74fb
commit
b17652b26b
13
.env.example
13
.env.example
@ -1,5 +1,10 @@
|
|||||||
TWITTER_CREATOR="@vercel"
|
TWITTER_CREATOR="@BigCommerce"
|
||||||
TWITTER_SITE="https://nextjs.org/commerce"
|
TWITTER_SITE="https://nextjs.org/commerce"
|
||||||
SITE_NAME="Next.js Commerce"
|
SITE_NAME="Next.js Commerce by BigCommerce"
|
||||||
SHOPIFY_STOREFRONT_ACCESS_TOKEN=
|
BIGCOMMERCE_ACCESS_TOKEN=
|
||||||
SHOPIFY_STORE_DOMAIN=
|
BIGCOMMERCE_CHANNEL_ID=
|
||||||
|
BIGCOMMERCE_STORE_HASH=
|
||||||
|
# Optional
|
||||||
|
BIGCOMMERCE_CANONICAL_STORE_DOMAIN="mybigcommerce.com"
|
||||||
|
BIGCOMMERCE_API_URL="https://api.bigcommerce.com"
|
||||||
|
BIGCOMMERCE_CDN_HOSTNAME="*.bigcommerce.com"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
import Prose from 'components/prose';
|
import Prose from 'components/prose';
|
||||||
import { getPage } from 'lib/shopify';
|
import { getPage } from 'lib/bigcommerce';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { addToCart, removeFromCart, updateCart } from 'lib/shopify';
|
import { addToCart, removeFromCart, updateCart } from 'lib/bigcommerce';
|
||||||
import { isShopifyError } from 'lib/type-guards';
|
import { isVercelCommerceError } from 'lib/type-guards';
|
||||||
|
|
||||||
function formatErrorMessage(err: Error): string {
|
function formatErrorMessage(err: Error): string {
|
||||||
return JSON.stringify(err, Object.getOwnPropertyNames(err));
|
return JSON.stringify(err, Object.getOwnPropertyNames(err));
|
||||||
@ -10,16 +10,18 @@ function formatErrorMessage(err: Error): string {
|
|||||||
|
|
||||||
export async function POST(req: NextRequest): Promise<Response> {
|
export async function POST(req: NextRequest): Promise<Response> {
|
||||||
const cartId = cookies().get('cartId')?.value;
|
const cartId = cookies().get('cartId')?.value;
|
||||||
const { merchandiseId } = await req.json();
|
const { merchandiseId, isBigCommerceAPI } = await req.json();
|
||||||
|
|
||||||
if (!cartId?.length || !merchandiseId?.length) {
|
if ((!isBigCommerceAPI && !cartId?.length) || !merchandiseId?.length) {
|
||||||
return NextResponse.json({ error: 'Missing cartId or variantId' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing cartId or variantId' }, { status: 400 });
|
||||||
|
} else if (isBigCommerceAPI && !merchandiseId?.length) {
|
||||||
|
return NextResponse.json({ error: 'Missing variantId' }, { status: 400 });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await addToCart(cartId, [{ merchandiseId, quantity: 1 }]);
|
await addToCart(cartId || '', [{ merchandiseId, quantity: 1 }]);
|
||||||
return NextResponse.json({ status: 204 });
|
return NextResponse.json({ status: 204 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isShopifyError(e)) {
|
if (isVercelCommerceError(e)) {
|
||||||
return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status });
|
return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +49,7 @@ export async function PUT(req: NextRequest): Promise<Response> {
|
|||||||
]);
|
]);
|
||||||
return NextResponse.json({ status: 204 });
|
return NextResponse.json({ status: 204 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isShopifyError(e)) {
|
if (isVercelCommerceError(e)) {
|
||||||
return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status });
|
return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +68,7 @@ export async function DELETE(req: NextRequest): Promise<Response> {
|
|||||||
await removeFromCart(cartId, [lineId]);
|
await removeFromCart(cartId, [lineId]);
|
||||||
return NextResponse.json({ status: 204 });
|
return NextResponse.json({ status: 204 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isShopifyError(e)) {
|
if (isVercelCommerceError(e)) {
|
||||||
return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status });
|
return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { Suspense } from 'react';
|
|||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
description: 'High-performance ecommerce store built with Next.js, Vercel, and Shopify.',
|
description: 'High-performance ecommerce store built with Next.js, Vercel, and BigCommerce.',
|
||||||
openGraph: {
|
openGraph: {
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
|
@ -10,8 +10,8 @@ import { Gallery } from 'components/product/gallery';
|
|||||||
import { VariantSelector } from 'components/product/variant-selector';
|
import { VariantSelector } from 'components/product/variant-selector';
|
||||||
import Prose from 'components/prose';
|
import Prose from 'components/prose';
|
||||||
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
||||||
import { getProduct, getProductRecommendations } from 'lib/shopify';
|
import { getProduct, getProductRecommendations } from 'lib/bigcommerce';
|
||||||
import { Image } from 'lib/shopify/types';
|
import { Image } from 'lib/bigcommerce/types';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getCollection, getCollectionProducts } from 'lib/shopify';
|
import { getCollection, getCollectionProducts } from 'lib/bigcommerce';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Grid from 'components/grid';
|
import Grid from 'components/grid';
|
||||||
import ProductGridItems from 'components/layout/product-grid-items';
|
import ProductGridItems from 'components/layout/product-grid-items';
|
||||||
|
import { getProducts } from 'lib/bigcommerce';
|
||||||
import { defaultSort, sorting } from 'lib/constants';
|
import { defaultSort, sorting } from 'lib/constants';
|
||||||
import { getProducts } from 'lib/shopify';
|
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getCollections, getPages, getProducts } from 'lib/shopify';
|
import { getCollections, getPages, getProducts } from 'lib/bigcommerce';
|
||||||
import { MetadataRoute } from 'next';
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
|
const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getCollectionProducts } from 'lib/shopify';
|
import { getCollectionProducts } from 'lib/bigcommerce';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { useCookies } from 'react-cookie';
|
|||||||
import CartIcon from 'components/icons/cart';
|
import CartIcon from 'components/icons/cart';
|
||||||
import CartModal from './modal';
|
import CartModal from './modal';
|
||||||
|
|
||||||
import type { Cart } from 'lib/shopify/types';
|
import type { VercelCart as Cart } from 'lib/bigcommerce/types';
|
||||||
|
|
||||||
export default function CartButton({
|
export default function CartButton({
|
||||||
cart,
|
cart,
|
||||||
|
@ -4,7 +4,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { startTransition, useState } from 'react';
|
import { startTransition, useState } from 'react';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { CartItem } from 'lib/shopify/types';
|
import type { VercelCartItem as CartItem } from 'lib/bigcommerce/types';
|
||||||
|
|
||||||
export default function DeleteItemButton({ item }: { item: CartItem }) {
|
export default function DeleteItemButton({ item }: { item: CartItem }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -4,7 +4,7 @@ import { startTransition, useState } from 'react';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import MinusIcon from 'components/icons/minus';
|
import MinusIcon from 'components/icons/minus';
|
||||||
import PlusIcon from 'components/icons/plus';
|
import PlusIcon from 'components/icons/plus';
|
||||||
import type { CartItem } from 'lib/shopify/types';
|
import type { VercelCartItem as CartItem } from 'lib/bigcommerce/types';
|
||||||
import LoadingDots from '../loading-dots';
|
import LoadingDots from '../loading-dots';
|
||||||
|
|
||||||
export default function EditItemQuantityButton({
|
export default function EditItemQuantityButton({
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createCart, getCart } from 'lib/shopify';
|
import { createCart, getCart } from 'lib/bigcommerce';
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import CartButton from './button';
|
import CartButton from './button';
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import CloseIcon from 'components/icons/close';
|
|||||||
import ShoppingBagIcon from 'components/icons/shopping-bag';
|
import ShoppingBagIcon from 'components/icons/shopping-bag';
|
||||||
import Price from 'components/price';
|
import Price from 'components/price';
|
||||||
import { DEFAULT_OPTION } from 'lib/constants';
|
import { DEFAULT_OPTION } from 'lib/constants';
|
||||||
import type { Cart } from 'lib/shopify/types';
|
import type { VercelCart as Cart } from 'lib/bigcommerce/types';
|
||||||
import { createUrl } from 'lib/utils';
|
import { createUrl } from 'lib/utils';
|
||||||
import DeleteItemButton from './delete-item-button';
|
import DeleteItemButton from './delete-item-button';
|
||||||
import EditItemQuantityButton from './edit-item-quantity-button';
|
import EditItemQuantityButton from './edit-item-quantity-button';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { GridTileImage } from 'components/grid/tile';
|
import { GridTileImage } from 'components/grid/tile';
|
||||||
import { getCollectionProducts } from 'lib/shopify';
|
import { getCollectionProducts } from 'lib/bigcommerce';
|
||||||
import type { Product } from 'lib/shopify/types';
|
import type { VercelProduct as Product } from 'lib/bigcommerce/types';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
function ThreeItemGridItem({
|
function ThreeItemGridItem({
|
||||||
|
@ -3,8 +3,8 @@ import Link from 'next/link';
|
|||||||
import GitHubIcon from 'components/icons/github';
|
import GitHubIcon from 'components/icons/github';
|
||||||
import LogoIcon from 'components/icons/logo';
|
import LogoIcon from 'components/icons/logo';
|
||||||
import VercelIcon from 'components/icons/vercel';
|
import VercelIcon from 'components/icons/vercel';
|
||||||
import { getMenu } from 'lib/shopify';
|
import { getMenu } from 'lib/bigcommerce';
|
||||||
import { Menu } from 'lib/shopify/types';
|
import { VercelMenu as Menu } from 'lib/bigcommerce/types';
|
||||||
|
|
||||||
const { SITE_NAME } = process.env;
|
const { SITE_NAME } = process.env;
|
||||||
|
|
||||||
|
@ -4,18 +4,19 @@ import { Suspense } from 'react';
|
|||||||
import Cart from 'components/cart';
|
import Cart from 'components/cart';
|
||||||
import CartIcon from 'components/icons/cart';
|
import CartIcon from 'components/icons/cart';
|
||||||
import LogoIcon from 'components/icons/logo';
|
import LogoIcon from 'components/icons/logo';
|
||||||
import { getMenu } from 'lib/shopify';
|
import { getMenu } from 'lib/bigcommerce';
|
||||||
import { Menu } from 'lib/shopify/types';
|
import { VercelMenu as Menu } from 'lib/bigcommerce/types';
|
||||||
import MobileMenu from './mobile-menu';
|
import MobileMenu from './mobile-menu';
|
||||||
import Search from './search';
|
import Search from './search';
|
||||||
|
|
||||||
export default async function Navbar() {
|
export default async function Navbar() {
|
||||||
const menu = await getMenu('next-js-frontend-header-menu');
|
const menu = await getMenu('next-js-frontend-header-menu');
|
||||||
|
const demoMenu = menu.slice(0, 4);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="relative flex items-center justify-between bg-white p-4 dark:bg-black lg:px-6">
|
<nav className="relative flex items-center justify-between bg-white p-4 dark:bg-black lg:px-6">
|
||||||
<div className="block w-1/3 md:hidden">
|
<div className="block w-1/3 md:hidden">
|
||||||
<MobileMenu menu={menu} />
|
<MobileMenu menu={demoMenu} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-self-center md:w-1/3 md:justify-self-start">
|
<div className="flex justify-self-center md:w-1/3 md:justify-self-start">
|
||||||
<div className="md:mr-4">
|
<div className="md:mr-4">
|
||||||
@ -23,9 +24,9 @@ export default async function Navbar() {
|
|||||||
<LogoIcon className="h-8 transition-transform hover:scale-110" />
|
<LogoIcon className="h-8 transition-transform hover:scale-110" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{menu.length ? (
|
{demoMenu.length ? (
|
||||||
<ul className="hidden md:flex">
|
<ul className="hidden md:flex">
|
||||||
{menu.map((item: Menu) => (
|
{demoMenu.map((item: Menu) => (
|
||||||
<li key={item.title}>
|
<li key={item.title}>
|
||||||
<Link
|
<Link
|
||||||
href={item.path}
|
href={item.path}
|
||||||
|
@ -8,7 +8,7 @@ import { useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import CloseIcon from 'components/icons/close';
|
import CloseIcon from 'components/icons/close';
|
||||||
import MenuIcon from 'components/icons/menu';
|
import MenuIcon from 'components/icons/menu';
|
||||||
import { Menu } from 'lib/shopify/types';
|
import { VercelMenu as Menu } from 'lib/bigcommerce/types';
|
||||||
import Search from './search';
|
import Search from './search';
|
||||||
|
|
||||||
export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Grid from 'components/grid';
|
import Grid from 'components/grid';
|
||||||
import { GridTileImage } from 'components/grid/tile';
|
import { GridTileImage } from 'components/grid/tile';
|
||||||
import { Product } from 'lib/shopify/types';
|
import { VercelProduct as Product } from 'lib/bigcommerce/types';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export default function ProductGridItems({ products }: { products: Product[] }) {
|
export default function ProductGridItems({ products }: { products: Product[] }) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
import { getCollections } from 'lib/shopify';
|
import { getCollections } from 'lib/bigcommerce';
|
||||||
import FilterList from './filter';
|
import FilterList from './filter';
|
||||||
|
|
||||||
async function CollectionList() {
|
async function CollectionList() {
|
||||||
|
@ -5,7 +5,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
|||||||
import { useEffect, useState, useTransition } from 'react';
|
import { useEffect, useState, useTransition } from 'react';
|
||||||
|
|
||||||
import LoadingDots from 'components/loading-dots';
|
import LoadingDots from 'components/loading-dots';
|
||||||
import { ProductVariant } from 'lib/shopify/types';
|
import { VercelProductVariant as ProductVariant } from 'lib/bigcommerce/types';
|
||||||
|
|
||||||
export function AddToCart({
|
export function AddToCart({
|
||||||
variants,
|
variants,
|
||||||
@ -14,7 +14,8 @@ export function AddToCart({
|
|||||||
variants: ProductVariant[];
|
variants: ProductVariant[];
|
||||||
availableForSale: boolean;
|
availableForSale: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [selectedVariantId, setSelectedVariantId] = useState(variants[0]?.id);
|
const productEntityId = variants[0]?.parentId || variants[0]?.id;
|
||||||
|
const [selectedVariantId, setSelectedVariantId] = useState(productEntityId);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
@ -28,7 +29,7 @@ export function AddToCart({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (variant) {
|
if (variant) {
|
||||||
setSelectedVariantId(variant.id);
|
setSelectedVariantId(variant.parentId || variant.id);
|
||||||
}
|
}
|
||||||
}, [searchParams, variants, setSelectedVariantId]);
|
}, [searchParams, variants, setSelectedVariantId]);
|
||||||
|
|
||||||
@ -42,7 +43,8 @@ export function AddToCart({
|
|||||||
const response = await fetch(`/api/cart`, {
|
const response = await fetch(`/api/cart`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
merchandiseId: selectedVariantId
|
merchandiseId: selectedVariantId,
|
||||||
|
isBigCommerceAPI: true
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { ProductOption, ProductVariant } from 'lib/shopify/types';
|
import {
|
||||||
|
VercelProductOption as ProductOption,
|
||||||
|
VercelProductVariant as ProductVariant
|
||||||
|
} from 'lib/bigcommerce/types';
|
||||||
import { createUrl } from 'lib/utils';
|
import { createUrl } from 'lib/utils';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
4
lib/bigcommerce/constants.ts
Normal file
4
lib/bigcommerce/constants.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const BIGCOMMERCE_API_URL = process.env.BIGCOMMERCE_API_URL ?? 'https://api.bigcommerce.com';
|
||||||
|
export const BIGCOMMERCE_CANONICAL_STORE_DOMAIN =
|
||||||
|
process.env.BIGCOMMERCE_CANONICAL_STORE_DOMAIN ?? 'mybigcommerce.com';
|
||||||
|
export const BIGCOMMERCE_GRAPHQL_API_ENDPOINT = `${BIGCOMMERCE_CANONICAL_STORE_DOMAIN}/graphql`;
|
187
lib/bigcommerce/fragments/cart.ts
Normal file
187
lib/bigcommerce/fragments/cart.ts
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
const physicalItemFragment = /* GraphQL */ `
|
||||||
|
fragment physicalItem on CartPhysicalItem {
|
||||||
|
entityId
|
||||||
|
parentEntityId
|
||||||
|
variantEntityId
|
||||||
|
productEntityId
|
||||||
|
sku
|
||||||
|
name
|
||||||
|
url
|
||||||
|
imageUrl
|
||||||
|
brand
|
||||||
|
quantity
|
||||||
|
isTaxable
|
||||||
|
discounts {
|
||||||
|
entityId
|
||||||
|
discountedAmount {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
discountedAmount {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
couponAmount {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
listPrice {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
originalPrice {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
salePrice {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
extendedListPrice {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
extendedSalePrice {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
isShippingRequired
|
||||||
|
selectedOptions {
|
||||||
|
entityId
|
||||||
|
name
|
||||||
|
... on CartSelectedCheckboxOption {
|
||||||
|
value
|
||||||
|
valueEntityId
|
||||||
|
}
|
||||||
|
... on CartSelectedDateFieldOption {
|
||||||
|
date {
|
||||||
|
utc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on CartSelectedFileUploadOption {
|
||||||
|
fileName
|
||||||
|
}
|
||||||
|
... on CartSelectedMultiLineTextFieldOption {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
... on CartSelectedMultipleChoiceOption {
|
||||||
|
value
|
||||||
|
valueEntityId
|
||||||
|
}
|
||||||
|
... on CartSelectedNumberFieldOption {
|
||||||
|
number
|
||||||
|
}
|
||||||
|
... on CartSelectedTextFieldOption {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
giftWrapping {
|
||||||
|
name
|
||||||
|
amount {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const digitalItemFragment = /* GraphQL */ `
|
||||||
|
fragment digitalItem on CartDigitalItem {
|
||||||
|
entityId
|
||||||
|
parentEntityId
|
||||||
|
variantEntityId
|
||||||
|
productEntityId
|
||||||
|
sku
|
||||||
|
name
|
||||||
|
url
|
||||||
|
imageUrl
|
||||||
|
brand
|
||||||
|
quantity
|
||||||
|
isTaxable
|
||||||
|
discounts {
|
||||||
|
entityId
|
||||||
|
discountedAmount {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
discountedAmount {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
couponAmount {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
listPrice {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
originalPrice {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
salePrice {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
extendedListPrice {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
extendedSalePrice {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
selectedOptions {
|
||||||
|
entityId
|
||||||
|
name
|
||||||
|
... on CartSelectedCheckboxOption {
|
||||||
|
value
|
||||||
|
valueEntityId
|
||||||
|
}
|
||||||
|
... on CartSelectedDateFieldOption {
|
||||||
|
date {
|
||||||
|
utc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on CartSelectedFileUploadOption {
|
||||||
|
fileName
|
||||||
|
}
|
||||||
|
... on CartSelectedMultiLineTextFieldOption {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
... on CartSelectedMultipleChoiceOption {
|
||||||
|
value
|
||||||
|
valueEntityId
|
||||||
|
}
|
||||||
|
... on CartSelectedNumberFieldOption {
|
||||||
|
number
|
||||||
|
}
|
||||||
|
... on CartSelectedTextFieldOption {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const customItemFragment = /* GraphQL */ `
|
||||||
|
fragment customItem on CartCustomItem {
|
||||||
|
entityId
|
||||||
|
sku
|
||||||
|
name
|
||||||
|
quantity
|
||||||
|
extendedListPrice {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
listPrice {
|
||||||
|
value
|
||||||
|
currencyCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export { customItemFragment, digitalItemFragment, physicalItemFragment };
|
12
lib/bigcommerce/fragments/page.ts
Normal file
12
lib/bigcommerce/fragments/page.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export const pageContentFragment = /* GraphQL */ `
|
||||||
|
fragment pageContent on WebPage {
|
||||||
|
__typename
|
||||||
|
entityId
|
||||||
|
name
|
||||||
|
seo {
|
||||||
|
metaKeywords
|
||||||
|
metaDescription
|
||||||
|
pageTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
177
lib/bigcommerce/fragments/product.ts
Normal file
177
lib/bigcommerce/fragments/product.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
const productOptionFragment = /* GraphQL */ `
|
||||||
|
fragment productOption on CatalogProductOption {
|
||||||
|
__typename
|
||||||
|
entityId
|
||||||
|
displayName
|
||||||
|
isRequired
|
||||||
|
... on MultipleChoiceOption {
|
||||||
|
displayStyle
|
||||||
|
values(first: 5) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
entityId
|
||||||
|
isDefault
|
||||||
|
... on SwatchOptionValue {
|
||||||
|
hexColors
|
||||||
|
imageUrl(width: 200)
|
||||||
|
label
|
||||||
|
isSelected
|
||||||
|
}
|
||||||
|
... on MultipleChoiceOptionValue {
|
||||||
|
entityId
|
||||||
|
label
|
||||||
|
isSelected
|
||||||
|
}
|
||||||
|
... on ProductPickListOptionValue {
|
||||||
|
entityId
|
||||||
|
label
|
||||||
|
isSelected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on NumberFieldOption {
|
||||||
|
entityId
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
... on TextFieldOption {
|
||||||
|
entityId
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
... on MultiLineTextFieldOption {
|
||||||
|
entityId
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
... on FileUploadFieldOption {
|
||||||
|
entityId
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
... on DateFieldOption {
|
||||||
|
entityId
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
... on CheckboxOption {
|
||||||
|
entityId
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const productVariantFragment = /* GraphQL */ `
|
||||||
|
fragment productVariant on Variant {
|
||||||
|
id
|
||||||
|
entityId
|
||||||
|
sku
|
||||||
|
upc
|
||||||
|
isPurchasable
|
||||||
|
prices {
|
||||||
|
price {
|
||||||
|
value
|
||||||
|
currencyCode
|
||||||
|
}
|
||||||
|
priceRange {
|
||||||
|
min {
|
||||||
|
value
|
||||||
|
currencyCode
|
||||||
|
}
|
||||||
|
max {
|
||||||
|
value
|
||||||
|
currencyCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options(first: 5) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
entityId
|
||||||
|
displayName
|
||||||
|
values(first: 5) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
entityId
|
||||||
|
label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const productFragment = /* GraphQL */ `
|
||||||
|
fragment product on Product {
|
||||||
|
id
|
||||||
|
entityId
|
||||||
|
sku
|
||||||
|
upc
|
||||||
|
name
|
||||||
|
brand {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
plainTextDescription
|
||||||
|
description
|
||||||
|
availabilityV2 {
|
||||||
|
status
|
||||||
|
description
|
||||||
|
}
|
||||||
|
defaultImage {
|
||||||
|
...ImageFields
|
||||||
|
}
|
||||||
|
images {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...ImageFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seo {
|
||||||
|
pageTitle
|
||||||
|
metaDescription
|
||||||
|
metaKeywords
|
||||||
|
}
|
||||||
|
prices {
|
||||||
|
price {
|
||||||
|
...MoneyFields
|
||||||
|
}
|
||||||
|
priceRange {
|
||||||
|
min {
|
||||||
|
...MoneyFields
|
||||||
|
}
|
||||||
|
max {
|
||||||
|
...MoneyFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createdAt {
|
||||||
|
utc
|
||||||
|
}
|
||||||
|
variants(first: 5) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...productVariant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
productOptions(first: 3) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...productOption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment ImageFields on Image {
|
||||||
|
url: url(width: 1080)
|
||||||
|
altText
|
||||||
|
}
|
||||||
|
fragment MoneyFields on Money {
|
||||||
|
value
|
||||||
|
currencyCode
|
||||||
|
}
|
||||||
|
${productOptionFragment}
|
||||||
|
${productVariantFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export { productOptionFragment, productVariantFragment, productFragment };
|
580
lib/bigcommerce/index.ts
Normal file
580
lib/bigcommerce/index.ts
Normal file
@ -0,0 +1,580 @@
|
|||||||
|
import { isVercelCommerceError } from 'lib/type-guards';
|
||||||
|
import { BIGCOMMERCE_GRAPHQL_API_ENDPOINT } from './constants';
|
||||||
|
import {
|
||||||
|
bigCommerceToVercelCollection,
|
||||||
|
bigCommerceToVercelPageContent,
|
||||||
|
bigcommerceToVercelCart,
|
||||||
|
bigcommerceToVercelProduct,
|
||||||
|
bigcommerceToVercelProducts,
|
||||||
|
vercelFromBigCommerceLineItems,
|
||||||
|
vercelToBigCommerceSorting
|
||||||
|
} from './mappers';
|
||||||
|
import {
|
||||||
|
addCartLineItemMutation,
|
||||||
|
createCartMutation,
|
||||||
|
deleteCartLineItemMutation,
|
||||||
|
updateCartLineItemMutation
|
||||||
|
} from './mutations/cart';
|
||||||
|
import { getCartQuery } from './queries/cart';
|
||||||
|
import { getCategoryQuery, getStoreCategoriesQuery } from './queries/category';
|
||||||
|
import { getCheckoutQuery } from './queries/checkout';
|
||||||
|
import { getMenuQuery } from './queries/menu';
|
||||||
|
import { getPageQuery, getPagesQuery } from './queries/page';
|
||||||
|
import {
|
||||||
|
getNewestProductsQuery,
|
||||||
|
getPopularProductsQuery,
|
||||||
|
getProductQuery,
|
||||||
|
getProductsCollectionQuery,
|
||||||
|
getProductsRecommedationsQuery,
|
||||||
|
searchProductsQuery
|
||||||
|
} from './queries/product';
|
||||||
|
import { getEntityIdByRouteQuery } from './queries/route';
|
||||||
|
import { fetchStorefrontToken } from './storefront-config';
|
||||||
|
import {
|
||||||
|
BigCommerceAddToCartOperation,
|
||||||
|
BigCommerceCart,
|
||||||
|
BigCommerceCartOperation,
|
||||||
|
BigCommerceCategoryTreeItem,
|
||||||
|
BigCommerceCheckoutOperation,
|
||||||
|
BigCommerceCollectionOperation,
|
||||||
|
BigCommerceCollectionsOperation,
|
||||||
|
BigCommerceCreateCartOperation,
|
||||||
|
BigCommerceDeleteCartItemOperation,
|
||||||
|
BigCommerceEntityIdOperation,
|
||||||
|
BigCommerceMenuOperation,
|
||||||
|
BigCommerceNewestProductsOperation,
|
||||||
|
BigCommercePageOperation,
|
||||||
|
BigCommercePagesOperation,
|
||||||
|
BigCommercePopularProductsOperation,
|
||||||
|
BigCommerceProductOperation,
|
||||||
|
BigCommerceProductsCollectionOperation,
|
||||||
|
BigCommerceRecommendationsOperation,
|
||||||
|
BigCommerceSearchProductsOperation,
|
||||||
|
BigCommerceUpdateCartItemOperation,
|
||||||
|
VercelCart,
|
||||||
|
VercelCollection,
|
||||||
|
VercelMenu,
|
||||||
|
VercelPage,
|
||||||
|
VercelProduct
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const channelIdSegment =
|
||||||
|
parseInt(process.env.BIGCOMMERCE_CHANNEL_ID!) !== 1
|
||||||
|
? `-${process.env.BIGCOMMERCE_CHANNEL_ID}`
|
||||||
|
: '';
|
||||||
|
const domain = `https://store-${process.env.BIGCOMMERCE_STORE_HASH!}${channelIdSegment}`;
|
||||||
|
const endpoint = `${domain}.${BIGCOMMERCE_GRAPHQL_API_ENDPOINT}`;
|
||||||
|
|
||||||
|
type ExtractVariables<T> = T extends { variables: object } ? T['variables'] : never;
|
||||||
|
|
||||||
|
const getEntityIdByHandle = async (entityHandle: string) => {
|
||||||
|
const res = await bigcommerceFetch<BigCommerceEntityIdOperation>({
|
||||||
|
query: getEntityIdByRouteQuery,
|
||||||
|
variables: {
|
||||||
|
path: `/${entityHandle}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.body.data.site.route.node.entityId;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function bigcommerceFetch<T>({
|
||||||
|
query,
|
||||||
|
variables,
|
||||||
|
headers,
|
||||||
|
cache = 'force-cache'
|
||||||
|
}: {
|
||||||
|
query: string;
|
||||||
|
variables?: ExtractVariables<T>;
|
||||||
|
headers?: HeadersInit;
|
||||||
|
cache?: RequestCache;
|
||||||
|
}): Promise<{ status: number; body: T } | never> {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { token }
|
||||||
|
} = await fetchStorefrontToken();
|
||||||
|
|
||||||
|
const result = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...headers
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...(query && { query }),
|
||||||
|
...(variables && { variables })
|
||||||
|
}),
|
||||||
|
cache,
|
||||||
|
next: { revalidate: 900 } // 15 minutes
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await result.json();
|
||||||
|
|
||||||
|
if (body.errors) {
|
||||||
|
throw body.errors[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: result.status,
|
||||||
|
body
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (isVercelCommerceError(e)) {
|
||||||
|
throw {
|
||||||
|
status: e.status || 500,
|
||||||
|
message: e.message,
|
||||||
|
query
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
error: e,
|
||||||
|
query
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryEntityIdbyHandle = async (handle: string) => {
|
||||||
|
const resp = await bigcommerceFetch<BigCommerceMenuOperation>({
|
||||||
|
query: getMenuQuery
|
||||||
|
});
|
||||||
|
const recursiveFindCollectionId = (list: BigCommerceCategoryTreeItem[], slug: string): number => {
|
||||||
|
const collectionId = list
|
||||||
|
.flatMap((item): number | null => {
|
||||||
|
if (item.path.includes(slug!)) {
|
||||||
|
return item.entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.children && item.children.length) {
|
||||||
|
return recursiveFindCollectionId(item.children!, slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((id) => typeof id === 'number')[0];
|
||||||
|
|
||||||
|
return collectionId!;
|
||||||
|
};
|
||||||
|
|
||||||
|
return recursiveFindCollectionId(resp.body.data.site.categoryTree, handle);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBigCommerceProductsWithCheckout = async (
|
||||||
|
cartId: string,
|
||||||
|
lines: { merchandiseId: string; quantity: number }[]
|
||||||
|
) => {
|
||||||
|
const bigCommerceProducts = await Promise.all(
|
||||||
|
lines.map(async ({ merchandiseId }) => {
|
||||||
|
const productId = Number(merchandiseId);
|
||||||
|
|
||||||
|
const resp = await bigcommerceFetch<BigCommerceProductOperation>({
|
||||||
|
query: getProductQuery,
|
||||||
|
variables: {
|
||||||
|
productId
|
||||||
|
},
|
||||||
|
cache: 'no-store'
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
productId,
|
||||||
|
productData: resp.body.data.site.product
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const resCheckout = await bigcommerceFetch<BigCommerceCheckoutOperation>({
|
||||||
|
query: getCheckoutQuery,
|
||||||
|
variables: {
|
||||||
|
entityId: cartId
|
||||||
|
},
|
||||||
|
cache: 'no-store'
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
productsByIdList: bigCommerceProducts,
|
||||||
|
checkout: resCheckout.body.data.site.checkout
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createCart(): Promise<VercelCart> {
|
||||||
|
// NOTE: on BigCommerce side we can't create cart
|
||||||
|
// w/t item params as quantity, productEntityId
|
||||||
|
return {
|
||||||
|
id: '',
|
||||||
|
checkoutUrl: '',
|
||||||
|
cost: {
|
||||||
|
subtotalAmount: {
|
||||||
|
amount: '',
|
||||||
|
currencyCode: ''
|
||||||
|
},
|
||||||
|
totalAmount: {
|
||||||
|
amount: '',
|
||||||
|
currencyCode: ''
|
||||||
|
},
|
||||||
|
totalTaxAmount: {
|
||||||
|
amount: '',
|
||||||
|
currencyCode: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lines: [],
|
||||||
|
totalQuantity: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addToCart(
|
||||||
|
cartId: string,
|
||||||
|
lines: { merchandiseId: string; quantity: number }[]
|
||||||
|
): Promise<VercelCart> {
|
||||||
|
let bigCommerceCart: BigCommerceCart;
|
||||||
|
|
||||||
|
if (cartId) {
|
||||||
|
const res = await bigcommerceFetch<BigCommerceAddToCartOperation>({
|
||||||
|
query: addCartLineItemMutation,
|
||||||
|
variables: {
|
||||||
|
addCartLineItemsInput: {
|
||||||
|
cartEntityId: cartId,
|
||||||
|
data: {
|
||||||
|
lineItems: lines.map(({ merchandiseId, quantity }) => ({
|
||||||
|
productEntityId: parseInt(merchandiseId, 10),
|
||||||
|
quantity
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cache: 'no-store'
|
||||||
|
});
|
||||||
|
|
||||||
|
bigCommerceCart = res.body.data.cart.addCartLineItems.cart;
|
||||||
|
} else {
|
||||||
|
const res = await bigcommerceFetch<BigCommerceCreateCartOperation>({
|
||||||
|
query: createCartMutation,
|
||||||
|
variables: {
|
||||||
|
createCartInput: {
|
||||||
|
lineItems: lines.map(({ merchandiseId, quantity }) => ({
|
||||||
|
productEntityId: parseInt(merchandiseId, 10),
|
||||||
|
quantity
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cache: 'no-store'
|
||||||
|
});
|
||||||
|
|
||||||
|
bigCommerceCart = res.body.data.cart.createCart.cart;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { productsByIdList, checkout } = await getBigCommerceProductsWithCheckout(
|
||||||
|
bigCommerceCart.entityId,
|
||||||
|
lines
|
||||||
|
);
|
||||||
|
|
||||||
|
return bigcommerceToVercelCart(bigCommerceCart, productsByIdList, checkout);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeFromCart(cartId: string, lineIds: string[]): Promise<VercelCart> {
|
||||||
|
let cartState: { status: number; body: BigCommerceDeleteCartItemOperation };
|
||||||
|
|
||||||
|
for (let removals = lineIds.length; removals > 0; removals--) {
|
||||||
|
const lineId = lineIds[removals - 1]!;
|
||||||
|
|
||||||
|
const res = await bigcommerceFetch<BigCommerceDeleteCartItemOperation>({
|
||||||
|
query: deleteCartLineItemMutation,
|
||||||
|
variables: {
|
||||||
|
deleteCartLineItemInput: {
|
||||||
|
cartEntityId: cartId,
|
||||||
|
lineItemEntityId: lineId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cache: 'no-store'
|
||||||
|
});
|
||||||
|
|
||||||
|
cartState = res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cart = cartState!.body.data.cart.deleteCartLineItem.cart;
|
||||||
|
const lines = vercelFromBigCommerceLineItems(cart.lineItems);
|
||||||
|
const { productsByIdList, checkout } = await getBigCommerceProductsWithCheckout(cartId, lines);
|
||||||
|
|
||||||
|
return bigcommerceToVercelCart(cart, productsByIdList, checkout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: looks like we can update only product-level update.
|
||||||
|
// Update on selected options requires variantEntityId, optionEntityId
|
||||||
|
export async function updateCart(
|
||||||
|
cartId: string,
|
||||||
|
lines: { id: string; merchandiseId: string; quantity: number }[]
|
||||||
|
): Promise<VercelCart> {
|
||||||
|
let cartState: { status: number; body: BigCommerceUpdateCartItemOperation } | undefined;
|
||||||
|
|
||||||
|
for (let updates = lines.length; updates > 0; updates--) {
|
||||||
|
const { id, merchandiseId, quantity } = lines[updates - 1]!;
|
||||||
|
const res = await bigcommerceFetch<BigCommerceUpdateCartItemOperation>({
|
||||||
|
query: updateCartLineItemMutation,
|
||||||
|
variables: {
|
||||||
|
updateCartLineItemInput: {
|
||||||
|
cartEntityId: cartId,
|
||||||
|
lineItemEntityId: id,
|
||||||
|
data: {
|
||||||
|
lineItem: {
|
||||||
|
quantity,
|
||||||
|
productEntityId: Number(merchandiseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cache: 'no-store'
|
||||||
|
});
|
||||||
|
|
||||||
|
cartState = res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedCart = cartState!.body.data.cart.updateCartLineItem.cart;
|
||||||
|
const { productsByIdList, checkout } = await getBigCommerceProductsWithCheckout(cartId, lines);
|
||||||
|
|
||||||
|
return bigcommerceToVercelCart(updatedCart, productsByIdList, checkout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: DONE & review if it works
|
||||||
|
export async function getCart(cartId: string): Promise<VercelCart | null> {
|
||||||
|
const res = await bigcommerceFetch<BigCommerceCartOperation>({
|
||||||
|
query: getCartQuery,
|
||||||
|
variables: { entityId: cartId },
|
||||||
|
cache: 'no-store'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.body.data.site.cart) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cart = res.body.data.site.cart;
|
||||||
|
const lines = vercelFromBigCommerceLineItems(cart.lineItems);
|
||||||
|
const { productsByIdList, checkout } = await getBigCommerceProductsWithCheckout(cartId, lines);
|
||||||
|
|
||||||
|
return bigcommerceToVercelCart(cart, productsByIdList, checkout);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCollection(handle: string): Promise<VercelCollection> {
|
||||||
|
const entityId = await getCategoryEntityIdbyHandle(handle); // NOTE: check if this approach suits us
|
||||||
|
const res = await bigcommerceFetch<BigCommerceCollectionOperation>({
|
||||||
|
query: getCategoryQuery,
|
||||||
|
variables: {
|
||||||
|
entityId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return bigCommerceToVercelCollection(res.body.data.site.category);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCollectionProducts({
|
||||||
|
collection,
|
||||||
|
reverse,
|
||||||
|
sortKey
|
||||||
|
}: {
|
||||||
|
collection: string;
|
||||||
|
reverse?: boolean;
|
||||||
|
sortKey?: string;
|
||||||
|
}): Promise<VercelProduct[]> {
|
||||||
|
const expectedCollectionBreakpoints: Record<string, string> = {
|
||||||
|
'hidden-homepage-carousel': 'carousel_collection',
|
||||||
|
'hidden-homepage-featured-items': 'featured_collection'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (expectedCollectionBreakpoints[collection] === 'carousel_collection') {
|
||||||
|
const res = await bigcommerceFetch<BigCommerceNewestProductsOperation>({
|
||||||
|
query: getNewestProductsQuery,
|
||||||
|
variables: {
|
||||||
|
first: 10
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.body.data.site.newestProducts) {
|
||||||
|
console.log(`No collection found for \`${collection}\``);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const productList = res.body.data.site.newestProducts.edges.map((item) => item.node);
|
||||||
|
|
||||||
|
return bigcommerceToVercelProducts(productList);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedCollectionBreakpoints[collection] === 'featured_collection') {
|
||||||
|
const res = await bigcommerceFetch<BigCommercePopularProductsOperation>({
|
||||||
|
query: getPopularProductsQuery,
|
||||||
|
variables: {
|
||||||
|
first: 10
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.body.data.site.bestSellingProducts) {
|
||||||
|
console.log(`No collection found for \`${collection}\``);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const productList = res.body.data.site.bestSellingProducts.edges.map((item) => item.node);
|
||||||
|
|
||||||
|
return bigcommerceToVercelProducts(productList);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityId = await getCategoryEntityIdbyHandle(collection);
|
||||||
|
const sortBy = vercelToBigCommerceSorting(reverse ?? false, sortKey);
|
||||||
|
const res = await bigcommerceFetch<BigCommerceProductsCollectionOperation>({
|
||||||
|
query: getProductsCollectionQuery,
|
||||||
|
variables: {
|
||||||
|
entityId,
|
||||||
|
first: 10,
|
||||||
|
hideOutOfStock: false,
|
||||||
|
sortBy: sortBy === 'RELEVANCE' ? 'DEFAULT' : sortBy
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.body.data.site.category) {
|
||||||
|
console.log(`No collection found for \`${collection}\``);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const productList = res.body.data.site.category.products.edges.map((item) => item.node);
|
||||||
|
|
||||||
|
return bigcommerceToVercelProducts(productList);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCollections(): Promise<VercelCollection[]> {
|
||||||
|
const res = await bigcommerceFetch<BigCommerceCollectionsOperation>({
|
||||||
|
query: getStoreCategoriesQuery
|
||||||
|
});
|
||||||
|
const collectionIdList = res.body.data.site.categoryTree.map(({ entityId }) => entityId);
|
||||||
|
|
||||||
|
const collections = await Promise.all(
|
||||||
|
collectionIdList.map(async (entityId) => {
|
||||||
|
const res = await bigcommerceFetch<BigCommerceCollectionOperation>({
|
||||||
|
query: getCategoryQuery,
|
||||||
|
variables: {
|
||||||
|
entityId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return bigCommerceToVercelCollection(res.body.data.site.category);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return collections;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMenu(handle: string): Promise<VercelMenu[]> {
|
||||||
|
const expectedMenyType = 'footerOrHeader';
|
||||||
|
const handleToSlug: Record<string, string> = {
|
||||||
|
'next-js-frontend-footer-menu': expectedMenyType,
|
||||||
|
'next-js-frontend-header-menu': expectedMenyType
|
||||||
|
};
|
||||||
|
const configureMenuPath = (path: string) =>
|
||||||
|
path
|
||||||
|
.split('/')
|
||||||
|
.filter((item) => item.length)
|
||||||
|
.pop();
|
||||||
|
const createVercelCollectionPath = (title: string) => `/search/${title}`;
|
||||||
|
const configureVercelMenu = (
|
||||||
|
menuData: BigCommerceCategoryTreeItem[],
|
||||||
|
isMenuData: boolean
|
||||||
|
): VercelMenu[] => {
|
||||||
|
if (isMenuData) {
|
||||||
|
return menuData.flatMap((item) => {
|
||||||
|
const { name, path, hasChildren, children } = item;
|
||||||
|
const verceLTitle = configureMenuPath(path);
|
||||||
|
|
||||||
|
const vercelMenuItem = {
|
||||||
|
title: name,
|
||||||
|
path: createVercelCollectionPath(verceLTitle!)
|
||||||
|
};
|
||||||
|
// NOTE: for NavBar we probably should keep it only high level categories
|
||||||
|
// if (hasChildren && children) {
|
||||||
|
// return configureVercelMenu(children, hasChildren);
|
||||||
|
// }
|
||||||
|
|
||||||
|
return [vercelMenuItem];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (handleToSlug[handle] === expectedMenyType) {
|
||||||
|
const res = await bigcommerceFetch<BigCommerceMenuOperation>({
|
||||||
|
query: getMenuQuery
|
||||||
|
});
|
||||||
|
|
||||||
|
return configureVercelMenu(res.body.data.site.categoryTree, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace with BC API next Page(s) Methods
|
||||||
|
export async function getPage(handle: string): Promise<VercelPage> {
|
||||||
|
const entityId = await getEntityIdByHandle(handle);
|
||||||
|
const res = await bigcommerceFetch<BigCommercePageOperation>({
|
||||||
|
query: getPageQuery,
|
||||||
|
variables: {
|
||||||
|
entityId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return bigCommerceToVercelPageContent(res.body.data.site.content.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPages(): Promise<VercelPage[]> {
|
||||||
|
const res = await bigcommerceFetch<BigCommercePagesOperation>({
|
||||||
|
query: getPagesQuery
|
||||||
|
});
|
||||||
|
|
||||||
|
const pagesList = res.body.data.site.content.pages.edges.map((item) => item.node);
|
||||||
|
|
||||||
|
return pagesList.map((page) => bigCommerceToVercelPageContent(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProduct(handle: string): Promise<VercelProduct | undefined> {
|
||||||
|
// const productId = await getEntityIdByHandle(handle); // NOTE: check of this approach work
|
||||||
|
const res = await bigcommerceFetch<BigCommerceProductOperation>({
|
||||||
|
query: getProductQuery,
|
||||||
|
variables: {
|
||||||
|
productId: parseInt(handle, 10)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return bigcommerceToVercelProduct(res.body.data.site.product);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProductRecommendations(productId: string): Promise<VercelProduct[]> {
|
||||||
|
const res = await bigcommerceFetch<BigCommerceRecommendationsOperation>({
|
||||||
|
query: getProductsRecommedationsQuery,
|
||||||
|
variables: {
|
||||||
|
productId: productId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const productList = res.body.data.site.product.relatedProducts.edges.map((item) => item.node);
|
||||||
|
|
||||||
|
return bigcommerceToVercelProducts(productList);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProducts({
|
||||||
|
query,
|
||||||
|
reverse,
|
||||||
|
sortKey
|
||||||
|
}: {
|
||||||
|
query?: string;
|
||||||
|
reverse?: boolean;
|
||||||
|
sortKey?: string;
|
||||||
|
}): Promise<VercelProduct[]> {
|
||||||
|
const sort = vercelToBigCommerceSorting(reverse ?? false, sortKey);
|
||||||
|
const res = await bigcommerceFetch<BigCommerceSearchProductsOperation>({
|
||||||
|
query: searchProductsQuery,
|
||||||
|
variables: {
|
||||||
|
filters: {
|
||||||
|
searchTerm: query || ''
|
||||||
|
},
|
||||||
|
sort
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const productList = res.body.data.site.search.searchProducts.products.edges.map(
|
||||||
|
(item) => item.node
|
||||||
|
);
|
||||||
|
|
||||||
|
return bigcommerceToVercelProducts(productList);
|
||||||
|
}
|
336
lib/bigcommerce/mappers.ts
Normal file
336
lib/bigcommerce/mappers.ts
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
import { BigCommerceSortKeys, VercelSortKeys, vercelToBigCommerceSortKeys } from 'lib/constants';
|
||||||
|
import {
|
||||||
|
BigCommerceCart,
|
||||||
|
BigCommerceCheckout,
|
||||||
|
BigCommerceCollection,
|
||||||
|
BigCommercePage,
|
||||||
|
BigCommerceProduct,
|
||||||
|
BigCommerceProductOption,
|
||||||
|
BigCommerceProductVariant,
|
||||||
|
CartCustomItem,
|
||||||
|
DigitalOrPhysicalItem,
|
||||||
|
VercelCart,
|
||||||
|
VercelCartItem,
|
||||||
|
VercelCollection,
|
||||||
|
VercelPage,
|
||||||
|
VercelProduct,
|
||||||
|
VercelProductOption,
|
||||||
|
VercelProductVariant
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
type ProductsList = { productId: number; productData: BigCommerceProduct }[];
|
||||||
|
|
||||||
|
const vercelFromBigCommerceLineItems = (lineItems: BigCommerceCart['lineItems']) => {
|
||||||
|
const { physicalItems, digitalItems, customItems } = lineItems;
|
||||||
|
const cartItemMapper = ({ entityId, quantity }: DigitalOrPhysicalItem | CartCustomItem) => ({
|
||||||
|
merchandiseId: entityId.toString(),
|
||||||
|
quantity
|
||||||
|
});
|
||||||
|
|
||||||
|
return [physicalItems, digitalItems, customItems].flatMap((list) => list.map(cartItemMapper));
|
||||||
|
};
|
||||||
|
|
||||||
|
const bigcommerceToVercelOptions = (options: BigCommerceProductOption[]): VercelProductOption[] => {
|
||||||
|
return options.map((option) => {
|
||||||
|
return {
|
||||||
|
id: option.entityId.toString(),
|
||||||
|
name: option.displayName.toString(),
|
||||||
|
values: option.values ? option.values.edges.map(({ node: value }) => value.label) : []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const bigcommerceToVercelVariants = (
|
||||||
|
variants: BigCommerceProductVariant[],
|
||||||
|
productId: number
|
||||||
|
): VercelProductVariant[] => {
|
||||||
|
return variants.map((variant) => {
|
||||||
|
return {
|
||||||
|
parentId: productId.toString(),
|
||||||
|
id: variant.entityId.toString(),
|
||||||
|
title: '',
|
||||||
|
availableForSale: variant.isPurchasable,
|
||||||
|
selectedOptions: variant.options?.edges.map(({ node: option }) => ({
|
||||||
|
name: option.displayName ?? '',
|
||||||
|
value: option.values.edges.map(({ node }) => node.label)[0] ?? ''
|
||||||
|
})) || [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
],
|
||||||
|
price: {
|
||||||
|
amount:
|
||||||
|
variant.prices?.price.value.toString() ||
|
||||||
|
variant.prices?.priceRange.max.value.toString() ||
|
||||||
|
'0',
|
||||||
|
currencyCode:
|
||||||
|
variant.prices?.price.currencyCode || variant.prices?.priceRange.max.currencyCode || ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const bigcommerceToVercelProduct = (product: BigCommerceProduct): VercelProduct => {
|
||||||
|
const createVercelProductImage = (img: { url: string; altText: string }) => {
|
||||||
|
return {
|
||||||
|
url: img.url,
|
||||||
|
altText: img.altText,
|
||||||
|
width: 2048,
|
||||||
|
height: 2048
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const options = product.productOptions.edges.length
|
||||||
|
? bigcommerceToVercelOptions(product.productOptions.edges.map((item) => item.node))
|
||||||
|
: [];
|
||||||
|
const variants = product.variants.edges.length
|
||||||
|
? bigcommerceToVercelVariants(
|
||||||
|
product.variants.edges.map((item) => item.node),
|
||||||
|
product.entityId
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: product.id.toString(),
|
||||||
|
handle: product.entityId.toString(),
|
||||||
|
availableForSale: product.availabilityV2.status === 'Available' ? true : false,
|
||||||
|
title: product.name,
|
||||||
|
description: product.plainTextDescription || '',
|
||||||
|
descriptionHtml: product.description ?? '',
|
||||||
|
options,
|
||||||
|
priceRange: {
|
||||||
|
maxVariantPrice: {
|
||||||
|
amount:
|
||||||
|
product.prices.priceRange.max.value.toString() ||
|
||||||
|
product.prices.price.value.toString() ||
|
||||||
|
'0',
|
||||||
|
currencyCode:
|
||||||
|
product.prices.priceRange.max.currencyCode || product.prices.price.currencyCode || ''
|
||||||
|
},
|
||||||
|
minVariantPrice: {
|
||||||
|
amount:
|
||||||
|
product.prices.priceRange.min.value.toString() ||
|
||||||
|
product.prices.price.value.toString() ||
|
||||||
|
'0',
|
||||||
|
currencyCode:
|
||||||
|
product.prices.priceRange.min.currencyCode || product.prices.price.currencyCode || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variants,
|
||||||
|
images: product.images
|
||||||
|
? product.images.edges.map(({ node: img }) => createVercelProductImage(img))
|
||||||
|
: [],
|
||||||
|
featuredImage: createVercelProductImage(product.defaultImage),
|
||||||
|
seo: {
|
||||||
|
title: product.seo.pageTitle || product.name,
|
||||||
|
description: product.seo.metaDescription || ''
|
||||||
|
},
|
||||||
|
tags: [product.seo.metaKeywords] || [],
|
||||||
|
updatedAt: product.createdAt.utc.toString()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const bigcommerceToVercelProducts = (products: BigCommerceProduct[]) => {
|
||||||
|
const reshapedProducts = [];
|
||||||
|
|
||||||
|
for (const product of products) {
|
||||||
|
if (product) {
|
||||||
|
const reshapedProduct = bigcommerceToVercelProduct(product);
|
||||||
|
|
||||||
|
if (reshapedProduct) {
|
||||||
|
reshapedProducts.push(reshapedProduct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reshapedProducts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bigcommerceToVercelCartItems = (
|
||||||
|
lineItems: BigCommerceCart['lineItems'],
|
||||||
|
products: ProductsList
|
||||||
|
) => {
|
||||||
|
const getItemMapper = (products: ProductsList, isCustomItem: boolean = false) => {
|
||||||
|
return (item: CartCustomItem | DigitalOrPhysicalItem): VercelCartItem => {
|
||||||
|
const vercelProductFallback = {
|
||||||
|
id: '',
|
||||||
|
handle: '',
|
||||||
|
availableForSale: false,
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
descriptionHtml: '',
|
||||||
|
options: [],
|
||||||
|
priceRange: {
|
||||||
|
maxVariantPrice: { amount: '', currencyCode: '' },
|
||||||
|
minVariantPrice: { amount: '', currencyCode: '' }
|
||||||
|
},
|
||||||
|
variants: [],
|
||||||
|
featuredImage: {
|
||||||
|
url: '',
|
||||||
|
altText: '',
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
},
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: '',
|
||||||
|
altText: '',
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
seo: { title: '', description: '' },
|
||||||
|
tags: [],
|
||||||
|
updatedAt: ''
|
||||||
|
};
|
||||||
|
let product;
|
||||||
|
let selectedOptions;
|
||||||
|
|
||||||
|
if (isCustomItem) {
|
||||||
|
product = vercelProductFallback;
|
||||||
|
selectedOptions = [{ name: '', value: '' }];
|
||||||
|
} else {
|
||||||
|
const productData = products.filter(
|
||||||
|
({ productId }) => productId === (item as DigitalOrPhysicalItem).productEntityId
|
||||||
|
)[0]?.productData;
|
||||||
|
|
||||||
|
product = productData ? bigcommerceToVercelProduct(productData) : vercelProductFallback;
|
||||||
|
selectedOptions = (item as DigitalOrPhysicalItem).selectedOptions.map((option) => ({
|
||||||
|
name: option.name,
|
||||||
|
value: option.value || option.text || option.number?.toString() || option.fileName || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.entityId.toString(),
|
||||||
|
quantity: item.quantity,
|
||||||
|
cost: {
|
||||||
|
totalAmount: {
|
||||||
|
amount:
|
||||||
|
item.extendedListPrice.value.toString() || item.listPrice.value.toString() || '0',
|
||||||
|
currencyCode: item.extendedListPrice.currencyCode || item.listPrice.currencyCode || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
merchandise: {
|
||||||
|
id: item.entityId.toString(),
|
||||||
|
title: `${item.name}`,
|
||||||
|
selectedOptions,
|
||||||
|
product
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const { physicalItems, digitalItems, customItems } = lineItems;
|
||||||
|
const areCustomItemsInCart = customItems.length > 0;
|
||||||
|
const line1 = physicalItems.map((item) => getItemMapper(products)(item));
|
||||||
|
const line2 = digitalItems.map((item) => getItemMapper(products)(item));
|
||||||
|
const line3 = areCustomItemsInCart
|
||||||
|
? customItems.map((item) => getItemMapper(products, areCustomItemsInCart)(item))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return [...line1, ...line2, ...line3];
|
||||||
|
};
|
||||||
|
|
||||||
|
const bigcommerceToVercelCart = (
|
||||||
|
cart: BigCommerceCart,
|
||||||
|
products: ProductsList,
|
||||||
|
checkout: BigCommerceCheckout
|
||||||
|
): VercelCart => {
|
||||||
|
return {
|
||||||
|
id: cart.entityId,
|
||||||
|
checkoutUrl: '', // NOTE: where to get checkoutUrl??
|
||||||
|
cost: {
|
||||||
|
// NOTE: these props lay down in checkout not cart
|
||||||
|
subtotalAmount: {
|
||||||
|
amount: checkout.subtotal.value.toString(),
|
||||||
|
currencyCode: checkout.subtotal.currencyCode
|
||||||
|
},
|
||||||
|
totalAmount: {
|
||||||
|
amount: checkout.grandTotal.value.toString(),
|
||||||
|
currencyCode: checkout.grandTotal.currencyCode
|
||||||
|
},
|
||||||
|
totalTaxAmount: {
|
||||||
|
amount: checkout.taxTotal.value.toString(),
|
||||||
|
currencyCode: checkout.taxTotal.currencyCode
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lines: bigcommerceToVercelCartItems(cart.lineItems, products),
|
||||||
|
totalQuantity: cart.lineItems.totalQuantity
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const bigCommerceToVercelCollection = (collection: BigCommerceCollection): VercelCollection => {
|
||||||
|
if (!collection) {
|
||||||
|
return {
|
||||||
|
handle: '',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
seo: {
|
||||||
|
title: '',
|
||||||
|
description: ''
|
||||||
|
},
|
||||||
|
updatedAt: '',
|
||||||
|
path: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handle: collection.entityId.toString() || collection.name,
|
||||||
|
title: collection.name,
|
||||||
|
description: collection.description,
|
||||||
|
seo: {
|
||||||
|
title: collection.seo.pageTitle,
|
||||||
|
description: collection.seo.metaDescription
|
||||||
|
},
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
path: `/search${collection.path}`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
bigcommerceToVercelCart,
|
||||||
|
bigcommerceToVercelProduct,
|
||||||
|
bigcommerceToVercelProducts,
|
||||||
|
bigCommerceToVercelCollection,
|
||||||
|
vercelFromBigCommerceLineItems
|
||||||
|
};
|
||||||
|
|
||||||
|
export const vercelToBigCommerceSorting = (
|
||||||
|
isReversed: boolean,
|
||||||
|
sortKey?: string
|
||||||
|
): keyof typeof BigCommerceSortKeys | null => {
|
||||||
|
const VercelSorting: Record<string, string> = {
|
||||||
|
RELEVANCE: 'RELEVANCE',
|
||||||
|
BEST_SELLING: 'BEST_SELLING',
|
||||||
|
CREATED_AT: 'CREATED_AT',
|
||||||
|
PRICE: 'PRICE'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!sortKey || VercelSorting[sortKey] === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortKey === VercelSortKeys.PRICE) {
|
||||||
|
return isReversed
|
||||||
|
? vercelToBigCommerceSortKeys.PRICE_ON_REVERSE
|
||||||
|
: vercelToBigCommerceSortKeys.PRICE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return vercelToBigCommerceSortKeys[sortKey as keyof typeof VercelSortKeys];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bigCommerceToVercelPageContent = (page: BigCommercePage): VercelPage => {
|
||||||
|
return {
|
||||||
|
id: page.entityId.toString(),
|
||||||
|
title: page.name,
|
||||||
|
handle: page.path,
|
||||||
|
body: page.htmlBody ?? '',
|
||||||
|
bodySummary: page.plainTextSummary ?? '',
|
||||||
|
seo: {
|
||||||
|
title: page.seo.pageTitle,
|
||||||
|
description: page.seo.metaDescription
|
||||||
|
},
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
};
|
152
lib/bigcommerce/mutations/cart.ts
Normal file
152
lib/bigcommerce/mutations/cart.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { customItemFragment, digitalItemFragment, physicalItemFragment } from '../fragments/cart';
|
||||||
|
|
||||||
|
const addCartLineItemMutation = /* GraphQL */ `
|
||||||
|
mutation addCartLineItems($addCartLineItemsInput: AddCartLineItemsInput!) {
|
||||||
|
cart {
|
||||||
|
addCartLineItems(input: $addCartLineItemsInput) {
|
||||||
|
cart {
|
||||||
|
entityId
|
||||||
|
amount {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
lineItems {
|
||||||
|
totalQuantity
|
||||||
|
physicalItems {
|
||||||
|
...physicalItem
|
||||||
|
}
|
||||||
|
digitalItems {
|
||||||
|
...digitalItem
|
||||||
|
}
|
||||||
|
customItems {
|
||||||
|
...customItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${physicalItemFragment}
|
||||||
|
${digitalItemFragment}
|
||||||
|
${customItemFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const createCartMutation = /* GraphQL */ `
|
||||||
|
mutation createCart($createCartInput: CreateCartInput!) {
|
||||||
|
cart {
|
||||||
|
createCart(input: $createCartInput) {
|
||||||
|
cart {
|
||||||
|
entityId
|
||||||
|
amount {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
lineItems {
|
||||||
|
totalQuantity
|
||||||
|
physicalItems {
|
||||||
|
...physicalItem
|
||||||
|
}
|
||||||
|
digitalItems {
|
||||||
|
...digitalItem
|
||||||
|
}
|
||||||
|
customItems {
|
||||||
|
...customItem
|
||||||
|
}
|
||||||
|
giftCertificates {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${physicalItemFragment}
|
||||||
|
${digitalItemFragment}
|
||||||
|
${customItemFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const deleteCartLineItemMutation = /* GraphQL */ `
|
||||||
|
mutation deleteCartLineItem($deleteCartLineItemInput: DeleteCartLineItemInput!) {
|
||||||
|
cart {
|
||||||
|
deleteCartLineItem(input: $deleteCartLineItemInput) {
|
||||||
|
deletedLineItemEntityId
|
||||||
|
deletedCartEntityId
|
||||||
|
cart {
|
||||||
|
entityId
|
||||||
|
amount {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
lineItems {
|
||||||
|
physicalItems {
|
||||||
|
...physicalItem
|
||||||
|
}
|
||||||
|
digitalItems {
|
||||||
|
...digitalItem
|
||||||
|
}
|
||||||
|
customItems {
|
||||||
|
...customItem
|
||||||
|
}
|
||||||
|
totalQuantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${physicalItemFragment}
|
||||||
|
${digitalItemFragment}
|
||||||
|
${customItemFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const updateCartLineItemMutation = /* GraphQL */ `
|
||||||
|
mutation updateCartLineItem($updateCartLineItemInput: UpdateCartLineItemInput!) {
|
||||||
|
cart {
|
||||||
|
updateCartLineItem(input: $updateCartLineItemInput) {
|
||||||
|
cart {
|
||||||
|
entityId
|
||||||
|
amount {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
updatedAt {
|
||||||
|
utc
|
||||||
|
}
|
||||||
|
totalQuantity
|
||||||
|
lineItems {
|
||||||
|
totalQuantity
|
||||||
|
physicalItems {
|
||||||
|
...physicalItem
|
||||||
|
}
|
||||||
|
digitalItems {
|
||||||
|
...digitalItem
|
||||||
|
}
|
||||||
|
customItems {
|
||||||
|
...customItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${physicalItemFragment}
|
||||||
|
${digitalItemFragment}
|
||||||
|
${customItemFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const deleteCartMutation = /* GraphQL */ `
|
||||||
|
mutation deleteCart($deleteCartInput: DeleteCartInput!) {
|
||||||
|
cart {
|
||||||
|
deleteCart(input: $deleteCartInput) {
|
||||||
|
deletedCartEntityId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export {
|
||||||
|
createCartMutation,
|
||||||
|
addCartLineItemMutation,
|
||||||
|
updateCartLineItemMutation,
|
||||||
|
deleteCartLineItemMutation,
|
||||||
|
deleteCartMutation
|
||||||
|
};
|
94
lib/bigcommerce/queries/cart.ts
Normal file
94
lib/bigcommerce/queries/cart.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { customItemFragment, digitalItemFragment, physicalItemFragment } from '../fragments/cart';
|
||||||
|
|
||||||
|
export const getCartQuery = /* GraphQL */ `
|
||||||
|
query getCart($entityId: String!) {
|
||||||
|
site {
|
||||||
|
cart(entityId: $entityId) {
|
||||||
|
entityId
|
||||||
|
currencyCode
|
||||||
|
isTaxIncluded
|
||||||
|
amount {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
lineItems {
|
||||||
|
physicalItems {
|
||||||
|
...physicalItem
|
||||||
|
}
|
||||||
|
digitalItems {
|
||||||
|
...digitalItem
|
||||||
|
}
|
||||||
|
customItems {
|
||||||
|
...customItem
|
||||||
|
}
|
||||||
|
giftCertificates {
|
||||||
|
entityId
|
||||||
|
name
|
||||||
|
theme
|
||||||
|
amount {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
isTaxable
|
||||||
|
sender {
|
||||||
|
name
|
||||||
|
email
|
||||||
|
}
|
||||||
|
recipient {
|
||||||
|
name
|
||||||
|
email
|
||||||
|
}
|
||||||
|
message
|
||||||
|
}
|
||||||
|
customItems {
|
||||||
|
entityId
|
||||||
|
sku
|
||||||
|
name
|
||||||
|
quantity
|
||||||
|
listPrice {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
extendedListPrice {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalQuantity
|
||||||
|
}
|
||||||
|
createdAt {
|
||||||
|
utc
|
||||||
|
}
|
||||||
|
updatedAt {
|
||||||
|
utc
|
||||||
|
}
|
||||||
|
locale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${physicalItemFragment}
|
||||||
|
${digitalItemFragment}
|
||||||
|
${customItemFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getCheckoutNodeQuery = /* GraphQL */ `
|
||||||
|
query getCheckoutNode($nodeId: ID!) {
|
||||||
|
node(id: $nodeId) {
|
||||||
|
... on Checkout {
|
||||||
|
entityId
|
||||||
|
subtotal {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
taxTotal {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
grandTotal {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
28
lib/bigcommerce/queries/category.ts
Normal file
28
lib/bigcommerce/queries/category.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export const getCategoryQuery = /* GraphQL */ `
|
||||||
|
query getCategory($entityId: Int!) {
|
||||||
|
site {
|
||||||
|
category(entityId: $entityId) {
|
||||||
|
entityId
|
||||||
|
name
|
||||||
|
path
|
||||||
|
description
|
||||||
|
seo {
|
||||||
|
metaDescription
|
||||||
|
metaKeywords
|
||||||
|
pageTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getStoreCategoriesQuery = /* GraphQL */ `
|
||||||
|
query getStoreCategories {
|
||||||
|
site {
|
||||||
|
categoryTree {
|
||||||
|
entityId
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
20
lib/bigcommerce/queries/checkout.ts
Normal file
20
lib/bigcommerce/queries/checkout.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export const getCheckoutQuery = /* GraphQL */ `
|
||||||
|
query getCheckout($entityId: String) {
|
||||||
|
site {
|
||||||
|
checkout(entityId: $entityId) {
|
||||||
|
subtotal {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
taxTotal {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
grandTotal {
|
||||||
|
currencyCode
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
21
lib/bigcommerce/queries/menu.ts
Normal file
21
lib/bigcommerce/queries/menu.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export const getMenuQuery = /* GraphQL */ `
|
||||||
|
query getMenu {
|
||||||
|
site {
|
||||||
|
categoryTree {
|
||||||
|
...CategoryFields
|
||||||
|
children {
|
||||||
|
...CategoryFields
|
||||||
|
children {
|
||||||
|
...CategoryFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment CategoryFields on CategoryTreeItem {
|
||||||
|
hasChildren
|
||||||
|
entityId
|
||||||
|
name
|
||||||
|
path
|
||||||
|
}
|
||||||
|
`;
|
67
lib/bigcommerce/queries/page.ts
Normal file
67
lib/bigcommerce/queries/page.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { pageContentFragment } from '../fragments/page';
|
||||||
|
|
||||||
|
export const getPageQuery = /* GraphQL */ `
|
||||||
|
query getPage($entityId: Int!) {
|
||||||
|
site {
|
||||||
|
content {
|
||||||
|
page(entityId: $entityId) {
|
||||||
|
...pageContent
|
||||||
|
... on NormalPage {
|
||||||
|
plainTextSummary(characterLimit: 100)
|
||||||
|
htmlBody
|
||||||
|
path
|
||||||
|
}
|
||||||
|
... on ContactPage {
|
||||||
|
plainTextSummary(characterLimit: 100)
|
||||||
|
htmlBody
|
||||||
|
path
|
||||||
|
}
|
||||||
|
... on BlogIndexPage {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
... on RawHtmlPage {
|
||||||
|
plainTextSummary(characterLimit: 100)
|
||||||
|
htmlBody
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${pageContentFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getPagesQuery = /* GraphQL */ `
|
||||||
|
query getPages {
|
||||||
|
site {
|
||||||
|
content {
|
||||||
|
pages {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...pageContent
|
||||||
|
... on NormalPage {
|
||||||
|
plainTextSummary(characterLimit: 100)
|
||||||
|
htmlBody
|
||||||
|
path
|
||||||
|
}
|
||||||
|
... on ContactPage {
|
||||||
|
plainTextSummary(characterLimit: 100)
|
||||||
|
htmlBody
|
||||||
|
path
|
||||||
|
}
|
||||||
|
... on BlogIndexPage {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
... on RawHtmlPage {
|
||||||
|
plainTextSummary(characterLimit: 100)
|
||||||
|
htmlBody
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${pageContentFragment}
|
||||||
|
`;
|
130
lib/bigcommerce/queries/product.ts
Normal file
130
lib/bigcommerce/queries/product.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { productFragment } from '../fragments/product';
|
||||||
|
|
||||||
|
export const getProductQuery = /* GraphQL */ `
|
||||||
|
query productById($productId: Int!) {
|
||||||
|
site {
|
||||||
|
product(entityId: $productId) {
|
||||||
|
...product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${productFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getProductsCollectionQuery = /* GraphQL */ `
|
||||||
|
query getProductsCollection(
|
||||||
|
$entityId: Int!
|
||||||
|
$sortBy: CategoryProductSort
|
||||||
|
$hideOutOfStock: Boolean
|
||||||
|
$first: Int
|
||||||
|
) {
|
||||||
|
site {
|
||||||
|
category(entityId: $entityId) {
|
||||||
|
products(sortBy: $sortBy, hideOutOfStock: $hideOutOfStock, first: $first) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${productFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getStoreProductsQuery = /* GraphQL */ `
|
||||||
|
query getStoreProducts($first: Int, $entityIds: [number!]) {
|
||||||
|
site {
|
||||||
|
products(first: $first, entityIds: $entityIds) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${productFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const searchProductsQuery = /* GraphQL */ `
|
||||||
|
query searchProducts($filters: SearchProductsFiltersInput!, $sort: SearchProductsSortInput) {
|
||||||
|
site {
|
||||||
|
search {
|
||||||
|
searchProducts(filters: $filters, sort: $sort) {
|
||||||
|
products {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${productFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getProductsRecommedationsQuery = /* GraphQL */ `
|
||||||
|
query getProductsRecommedations($productId: ID) {
|
||||||
|
site {
|
||||||
|
product(id: $productId) {
|
||||||
|
relatedProducts {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${productFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getNewestProductsQuery = /* GraphQL */ `
|
||||||
|
query getNewestProducts($first: Int) {
|
||||||
|
site {
|
||||||
|
newestProducts(first: $first) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${productFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getFeaturedProductsQuery = /* GraphQL */ `
|
||||||
|
query getFeaturedProducts($first: Int) {
|
||||||
|
site {
|
||||||
|
featuredProducts(first: $first) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${productFragment}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getPopularProductsQuery = /* GraphQL */ `
|
||||||
|
query bestSellingProducts($first: Int) {
|
||||||
|
site {
|
||||||
|
bestSellingProducts(first: $first) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${productFragment}
|
||||||
|
`;
|
30
lib/bigcommerce/queries/route.ts
Normal file
30
lib/bigcommerce/queries/route.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export const getEntityIdByRouteQuery = /* GraphQL */ `
|
||||||
|
query getEntityIdByRoute($path: String!) {
|
||||||
|
site {
|
||||||
|
route(path: $path) {
|
||||||
|
node {
|
||||||
|
__typename
|
||||||
|
... on Product {
|
||||||
|
entityId
|
||||||
|
}
|
||||||
|
... on Category {
|
||||||
|
entityId
|
||||||
|
}
|
||||||
|
... on Brand {
|
||||||
|
entityId
|
||||||
|
}
|
||||||
|
# NOTE: this API is still not public
|
||||||
|
# ... on NormalPage {
|
||||||
|
# entityId
|
||||||
|
# }
|
||||||
|
# ... on ContactPage {
|
||||||
|
# entityId
|
||||||
|
# }
|
||||||
|
# ... on RawHtmlPage {
|
||||||
|
# entityId
|
||||||
|
# }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
29
lib/bigcommerce/storefront-config.ts
Normal file
29
lib/bigcommerce/storefront-config.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { BIGCOMMERCE_API_URL } from './constants';
|
||||||
|
|
||||||
|
interface StorefrontTokenResponse {
|
||||||
|
data: {
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
meta: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchStorefrontToken = async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${BIGCOMMERCE_API_URL}/stores/${process.env.BIGCOMMERCE_STORE_HASH}/v3/storefront/api-token-customer-impersonation`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
accept: 'application/json',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-auth-token': process.env.BIGCOMMERCE_ACCESS_TOKEN!,
|
||||||
|
'x-bc-customer-id': ''
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
channel_id: parseInt(process.env.BIGCOMMERCE_CHANNEL_ID!),
|
||||||
|
expires_at: Math.floor(new Date().getTime() / 1000) + 1 * 24 * 60 * 60 // 1 day
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (await response.json()) as StorefrontTokenResponse;
|
||||||
|
};
|
602
lib/bigcommerce/types.ts
Normal file
602
lib/bigcommerce/types.ts
Normal file
@ -0,0 +1,602 @@
|
|||||||
|
export type Maybe<T> = T | null;
|
||||||
|
|
||||||
|
export type Connection<T> = {
|
||||||
|
edges: Array<Edge<T>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Edge<T> = {
|
||||||
|
node: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VercelPage = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
handle: string;
|
||||||
|
body: string;
|
||||||
|
bodySummary: string;
|
||||||
|
seo?: VercelSEO;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VercelMenu = {
|
||||||
|
title: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VercelCollection = {
|
||||||
|
handle: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
seo: VercelSEO;
|
||||||
|
updatedAt: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VercelMoney = {
|
||||||
|
amount: string;
|
||||||
|
currencyCode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Image = {
|
||||||
|
url: string;
|
||||||
|
altText: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VercelProduct = {
|
||||||
|
id: string;
|
||||||
|
handle: string;
|
||||||
|
availableForSale: boolean;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
descriptionHtml: string;
|
||||||
|
options: VercelProductOption[];
|
||||||
|
priceRange: {
|
||||||
|
maxVariantPrice: VercelMoney;
|
||||||
|
minVariantPrice: VercelMoney;
|
||||||
|
};
|
||||||
|
variants: VercelProductVariant[];
|
||||||
|
featuredImage: Image;
|
||||||
|
images: Image[];
|
||||||
|
seo: VercelSEO;
|
||||||
|
tags: string[];
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VercelProductOption = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
values: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VercelProductVariant = {
|
||||||
|
parentId?: string;
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
availableForSale: boolean;
|
||||||
|
selectedOptions: {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
price: VercelMoney;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VercelSEO = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VercelCartItem = {
|
||||||
|
id: string;
|
||||||
|
quantity: number;
|
||||||
|
cost: {
|
||||||
|
totalAmount: VercelMoney;
|
||||||
|
};
|
||||||
|
merchandise: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
selectedOptions: {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
product: VercelProduct;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VercelCart = {
|
||||||
|
id: string;
|
||||||
|
checkoutUrl: string;
|
||||||
|
cost: {
|
||||||
|
subtotalAmount: VercelMoney;
|
||||||
|
totalAmount: VercelMoney;
|
||||||
|
totalTaxAmount: VercelMoney;
|
||||||
|
};
|
||||||
|
lines: VercelCartItem[];
|
||||||
|
totalQuantity: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceCartOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
cart: BigCommerceCart;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
entityId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceCreateCartOperation = {
|
||||||
|
data: {
|
||||||
|
cart: {
|
||||||
|
createCart: {
|
||||||
|
cart: BigCommerceCart;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
createCartInput: {
|
||||||
|
lineItems: CartItem[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceAddToCartOperation = {
|
||||||
|
data: {
|
||||||
|
cart: {
|
||||||
|
addCartLineItems: {
|
||||||
|
cart: BigCommerceCart;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
addCartLineItemsInput: {
|
||||||
|
cartEntityId: string;
|
||||||
|
data: {
|
||||||
|
lineItems: CartItem[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceDeleteCartItemOperation = {
|
||||||
|
data: {
|
||||||
|
cart: {
|
||||||
|
deleteCartLineItem: {
|
||||||
|
cart: BigCommerceCart;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
deleteCartLineItemInput: {
|
||||||
|
cartEntityId: string;
|
||||||
|
lineItemEntityId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceUpdateCartItemOperation = {
|
||||||
|
data: {
|
||||||
|
cart: {
|
||||||
|
updateCartLineItem: {
|
||||||
|
cart: BigCommerceCart;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
updateCartLineItemInput: {
|
||||||
|
cartEntityId: string;
|
||||||
|
lineItemEntityId: string;
|
||||||
|
data: {
|
||||||
|
lineItem: CartItem;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceCheckoutOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
checkout: BigCommerceCheckout;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
entityId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceProductOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
product: BigCommerceProduct;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
productId: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceProductsOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
products: Connection<BigCommerceProduct>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
entityIds: number[] | [];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceEntityIdOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
route: {
|
||||||
|
node: {
|
||||||
|
__typename:
|
||||||
|
| 'Product'
|
||||||
|
| 'Category'
|
||||||
|
| 'Brand'
|
||||||
|
| 'NormalPage'
|
||||||
|
| 'ContactPage'
|
||||||
|
| 'RawHtmlPage'
|
||||||
|
| 'BlogIndexPage';
|
||||||
|
entityId: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceRecommendationsOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
product: {
|
||||||
|
relatedProducts: Connection<BigCommerceProduct>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
productId: number | string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceSearchProductsOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
search: {
|
||||||
|
searchProducts: {
|
||||||
|
products: Connection<BigCommerceProduct>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
filters: {
|
||||||
|
searchTerm: string;
|
||||||
|
};
|
||||||
|
sort: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceMenuOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
categoryTree: BigCommerceCategoryTreeItem[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceCollectionOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
category: BigCommerceCollection;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
entityId: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceProductsCollectionOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
category: {
|
||||||
|
products: Connection<BigCommerceProduct>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
entityId: number;
|
||||||
|
sortBy: string | null;
|
||||||
|
hideOutOfStock: boolean;
|
||||||
|
first: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceNewestProductsOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
newestProducts: Connection<BigCommerceProduct>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
first: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceFeaturedProductsOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
featuredProducts: Connection<BigCommerceProduct>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
first: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommercePopularProductsOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
bestSellingProducts: Connection<BigCommerceProduct>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
first: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceCollectionsOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
categoryTree: BigCommerceCategoryWithId[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommercePageOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
content: {
|
||||||
|
page: BigCommercePage;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: { entityId: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommercePagesOperation = {
|
||||||
|
data: {
|
||||||
|
site: {
|
||||||
|
content: {
|
||||||
|
pages: Connection<BigCommercePage>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceCheckout = {
|
||||||
|
subtotal: BigCommerceMoney;
|
||||||
|
grandTotal: BigCommerceMoney;
|
||||||
|
taxTotal: BigCommerceMoney;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceCategoryWithId = Omit<BigCommerceCollection, 'description' | 'seo' | 'path'>;
|
||||||
|
|
||||||
|
export type BigCommerceSEO = {
|
||||||
|
pageTitle: string;
|
||||||
|
metaDescription: string;
|
||||||
|
metaKeywords: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceCollection = {
|
||||||
|
entityId: number;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
description: string;
|
||||||
|
seo: BigCommerceSEO;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceCart = {
|
||||||
|
entityId: string;
|
||||||
|
currencyCode: string;
|
||||||
|
isTaxIncluded: boolean;
|
||||||
|
baseAmount: BigCommerceMoney;
|
||||||
|
discountedAmount: BigCommerceMoney;
|
||||||
|
amount: BigCommerceMoney;
|
||||||
|
discounts: CartDiscount[];
|
||||||
|
lineItems: CartLineItems;
|
||||||
|
createdAt: { utc: Date };
|
||||||
|
updatedAt: { utc: Date };
|
||||||
|
locale: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CartLineItems = {
|
||||||
|
physicalItems: DigitalOrPhysicalItem[];
|
||||||
|
digitalItems: DigitalOrPhysicalItem[];
|
||||||
|
customItems: CartCustomItem[];
|
||||||
|
giftCertificates: CartGiftCertificate[];
|
||||||
|
totalQuantity: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CartItem = {
|
||||||
|
quantity: number;
|
||||||
|
productEntityId: number;
|
||||||
|
variantEntityId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceCategoryTreeItem = {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
hasChildren: boolean;
|
||||||
|
entityId: number;
|
||||||
|
children?: BigCommerceCategoryTreeItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommercePage = {
|
||||||
|
__typename: 'NormalPage' | 'ContactPage' | 'RawHtmlPage' | 'BlogIndexPage';
|
||||||
|
entityId: number;
|
||||||
|
name: string;
|
||||||
|
seo: BigCommerceSEO;
|
||||||
|
path: string;
|
||||||
|
plainTextSummary?: string;
|
||||||
|
htmlBody?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceMoney = {
|
||||||
|
value: number;
|
||||||
|
currencyCode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CartDiscount = {
|
||||||
|
entityId: string;
|
||||||
|
discountedAmount: BigCommerceMoney;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CartGiftCertificatePersonDetails = {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DigitalOrPhysicalItem = {
|
||||||
|
entityId: number;
|
||||||
|
parentEntityId: number | null;
|
||||||
|
productEntityId: number;
|
||||||
|
variantEntityId: number | null;
|
||||||
|
sku: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
imageUrl: string | null;
|
||||||
|
brand: string | null;
|
||||||
|
quantity: number;
|
||||||
|
isTaxable: boolean;
|
||||||
|
listPrice: BigCommerceMoney;
|
||||||
|
extendedListPrice: BigCommerceMoney;
|
||||||
|
selectedOptions: {
|
||||||
|
entityId: number;
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
date?: { utc: Date };
|
||||||
|
text?: string;
|
||||||
|
number?: string;
|
||||||
|
fileName?: ScrollSetting;
|
||||||
|
}[];
|
||||||
|
isShippingRequired: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CartCustomItem = {
|
||||||
|
entityId: string;
|
||||||
|
sku: string;
|
||||||
|
name: string;
|
||||||
|
quantity: number;
|
||||||
|
listPrice: BigCommerceMoney;
|
||||||
|
extendedListPrice: BigCommerceMoney;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CartGiftCertificate = {
|
||||||
|
entityId: number;
|
||||||
|
name: string;
|
||||||
|
amount: BigCommerceMoney;
|
||||||
|
isTaxable: boolean;
|
||||||
|
message: string;
|
||||||
|
sender: CartGiftCertificatePersonDetails;
|
||||||
|
recipient: CartGiftCertificatePersonDetails;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceProductVariant = {
|
||||||
|
id: number;
|
||||||
|
entityId: number;
|
||||||
|
sku: string;
|
||||||
|
upc: string | null;
|
||||||
|
isPurchasable: boolean;
|
||||||
|
prices: {
|
||||||
|
price: BigCommerceMoney;
|
||||||
|
priceRange: {
|
||||||
|
min: BigCommerceMoney;
|
||||||
|
max: BigCommerceMoney;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
edges: Array<{
|
||||||
|
node: {
|
||||||
|
entityId: number;
|
||||||
|
displayName: string;
|
||||||
|
values: {
|
||||||
|
edges: Array<{
|
||||||
|
node: {
|
||||||
|
entityId: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceProductOption = {
|
||||||
|
__typename: string;
|
||||||
|
entityId: number;
|
||||||
|
displayName: string;
|
||||||
|
isRequired: boolean;
|
||||||
|
displayStyle: string;
|
||||||
|
values: {
|
||||||
|
edges: Array<{
|
||||||
|
node: {
|
||||||
|
entityId: number;
|
||||||
|
label: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
hexColors: string[];
|
||||||
|
imageUrl: string | null;
|
||||||
|
isSelected: boolean;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BigCommerceProduct = {
|
||||||
|
id: number;
|
||||||
|
entityId: number;
|
||||||
|
sku: string;
|
||||||
|
upc: string | null;
|
||||||
|
name: string;
|
||||||
|
brand: {
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
plainTextDescription: string;
|
||||||
|
description: string;
|
||||||
|
availabilityV2: {
|
||||||
|
status: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
defaultImage: {
|
||||||
|
url: string;
|
||||||
|
altText: string;
|
||||||
|
};
|
||||||
|
images: {
|
||||||
|
edges: Array<{
|
||||||
|
node: {
|
||||||
|
url: string;
|
||||||
|
altText: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
seo: BigCommerceSEO;
|
||||||
|
prices: {
|
||||||
|
price: BigCommerceMoney;
|
||||||
|
priceRange: {
|
||||||
|
min: BigCommerceMoney;
|
||||||
|
max: BigCommerceMoney;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
createdAt: {
|
||||||
|
utc: Date;
|
||||||
|
};
|
||||||
|
variants: Connection<BigCommerceProductVariant>;
|
||||||
|
productOptions: Connection<BigCommerceProductOption>;
|
||||||
|
};
|
@ -1,7 +1,34 @@
|
|||||||
|
export enum BigCommerceSortKeys {
|
||||||
|
A_TO_Z = 'A_TO_Z',
|
||||||
|
BEST_REVIEWED = 'BEST_REVIEWED',
|
||||||
|
BEST_SELLING = 'BEST_SELLING',
|
||||||
|
RELEVANCE = 'RELEVANCE',
|
||||||
|
FEATURED = 'FEATURED',
|
||||||
|
HIGHEST_PRICE = 'HIGHEST_PRICE',
|
||||||
|
LOWEST_PRICE = 'LOWEST_PRICE',
|
||||||
|
NEWEST = 'NEWEST',
|
||||||
|
Z_TO_A = 'Z_TO_A'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VercelSortKeys {
|
||||||
|
RELEVANCE = 'RELEVANCE',
|
||||||
|
BEST_SELLING = 'BEST_SELLING',
|
||||||
|
CREATED_AT = 'CREATED_AT',
|
||||||
|
PRICE = 'PRICE'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum vercelToBigCommerceSortKeys {
|
||||||
|
RELEVANCE = 'RELEVANCE',
|
||||||
|
BEST_SELLING = 'BEST_SELLING',
|
||||||
|
CREATED_AT = 'NEWEST',
|
||||||
|
PRICE = 'LOWEST_PRICE',
|
||||||
|
PRICE_ON_REVERSE = 'HIGHEST_PRICE'
|
||||||
|
}
|
||||||
|
|
||||||
export type SortFilterItem = {
|
export type SortFilterItem = {
|
||||||
title: string;
|
title: string;
|
||||||
slug: string | null;
|
slug: string | null;
|
||||||
sortKey: 'RELEVANCE' | 'BEST_SELLING' | 'CREATED_AT' | 'PRICE';
|
sortKey: keyof typeof VercelSortKeys;
|
||||||
reverse: boolean;
|
reverse: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -22,4 +49,3 @@ export const sorting: SortFilterItem[] = [
|
|||||||
|
|
||||||
export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
|
export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
|
||||||
export const DEFAULT_OPTION = 'Default Title';
|
export const DEFAULT_OPTION = 'Default Title';
|
||||||
export const SHOPIFY_GRAPHQL_API_ENDPOINT = '/api/2023-01/graphql.json';
|
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
import productFragment from './product';
|
|
||||||
|
|
||||||
const cartFragment = /* GraphQL */ `
|
|
||||||
fragment cart on Cart {
|
|
||||||
id
|
|
||||||
checkoutUrl
|
|
||||||
cost {
|
|
||||||
subtotalAmount {
|
|
||||||
amount
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
totalAmount {
|
|
||||||
amount
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
totalTaxAmount {
|
|
||||||
amount
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lines(first: 100) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
quantity
|
|
||||||
cost {
|
|
||||||
totalAmount {
|
|
||||||
amount
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
merchandise {
|
|
||||||
... on ProductVariant {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
selectedOptions {
|
|
||||||
name
|
|
||||||
value
|
|
||||||
}
|
|
||||||
product {
|
|
||||||
...product
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
totalQuantity
|
|
||||||
}
|
|
||||||
${productFragment}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default cartFragment;
|
|
@ -1,10 +0,0 @@
|
|||||||
const imageFragment = /* GraphQL */ `
|
|
||||||
fragment image on Image {
|
|
||||||
url
|
|
||||||
altText
|
|
||||||
width
|
|
||||||
height
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default imageFragment;
|
|
@ -1,64 +0,0 @@
|
|||||||
import imageFragment from './image';
|
|
||||||
import seoFragment from './seo';
|
|
||||||
|
|
||||||
const productFragment = /* GraphQL */ `
|
|
||||||
fragment product on Product {
|
|
||||||
id
|
|
||||||
handle
|
|
||||||
availableForSale
|
|
||||||
title
|
|
||||||
description
|
|
||||||
descriptionHtml
|
|
||||||
options {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
values
|
|
||||||
}
|
|
||||||
priceRange {
|
|
||||||
maxVariantPrice {
|
|
||||||
amount
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
minVariantPrice {
|
|
||||||
amount
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variants(first: 250) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
availableForSale
|
|
||||||
selectedOptions {
|
|
||||||
name
|
|
||||||
value
|
|
||||||
}
|
|
||||||
price {
|
|
||||||
amount
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
featuredImage {
|
|
||||||
...image
|
|
||||||
}
|
|
||||||
images(first: 20) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
...image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
seo {
|
|
||||||
...seo
|
|
||||||
}
|
|
||||||
tags
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
${imageFragment}
|
|
||||||
${seoFragment}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default productFragment;
|
|
@ -1,8 +0,0 @@
|
|||||||
const seoFragment = /* GraphQL */ `
|
|
||||||
fragment seo on SEO {
|
|
||||||
description
|
|
||||||
title
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default seoFragment;
|
|
@ -1,385 +0,0 @@
|
|||||||
import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT } from 'lib/constants';
|
|
||||||
import { isShopifyError } from 'lib/type-guards';
|
|
||||||
import {
|
|
||||||
addToCartMutation,
|
|
||||||
createCartMutation,
|
|
||||||
editCartItemsMutation,
|
|
||||||
removeFromCartMutation
|
|
||||||
} from './mutations/cart';
|
|
||||||
import { getCartQuery } from './queries/cart';
|
|
||||||
import {
|
|
||||||
getCollectionProductsQuery,
|
|
||||||
getCollectionQuery,
|
|
||||||
getCollectionsQuery
|
|
||||||
} from './queries/collection';
|
|
||||||
import { getMenuQuery } from './queries/menu';
|
|
||||||
import { getPageQuery, getPagesQuery } from './queries/page';
|
|
||||||
import {
|
|
||||||
getProductQuery,
|
|
||||||
getProductRecommendationsQuery,
|
|
||||||
getProductsQuery
|
|
||||||
} from './queries/product';
|
|
||||||
import {
|
|
||||||
Cart,
|
|
||||||
Collection,
|
|
||||||
Connection,
|
|
||||||
Menu,
|
|
||||||
Page,
|
|
||||||
Product,
|
|
||||||
ShopifyAddToCartOperation,
|
|
||||||
ShopifyCart,
|
|
||||||
ShopifyCartOperation,
|
|
||||||
ShopifyCollection,
|
|
||||||
ShopifyCollectionOperation,
|
|
||||||
ShopifyCollectionProductsOperation,
|
|
||||||
ShopifyCollectionsOperation,
|
|
||||||
ShopifyCreateCartOperation,
|
|
||||||
ShopifyMenuOperation,
|
|
||||||
ShopifyPageOperation,
|
|
||||||
ShopifyPagesOperation,
|
|
||||||
ShopifyProduct,
|
|
||||||
ShopifyProductOperation,
|
|
||||||
ShopifyProductRecommendationsOperation,
|
|
||||||
ShopifyProductsOperation,
|
|
||||||
ShopifyRemoveFromCartOperation,
|
|
||||||
ShopifyUpdateCartOperation
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
const domain = `https://${process.env.SHOPIFY_STORE_DOMAIN!}`;
|
|
||||||
const endpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`;
|
|
||||||
const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
|
|
||||||
|
|
||||||
type ExtractVariables<T> = T extends { variables: object } ? T['variables'] : never;
|
|
||||||
|
|
||||||
export async function shopifyFetch<T>({
|
|
||||||
query,
|
|
||||||
variables,
|
|
||||||
headers,
|
|
||||||
cache = 'force-cache'
|
|
||||||
}: {
|
|
||||||
query: string;
|
|
||||||
variables?: ExtractVariables<T>;
|
|
||||||
headers?: HeadersInit;
|
|
||||||
cache?: RequestCache;
|
|
||||||
}): Promise<{ status: number; body: T } | never> {
|
|
||||||
try {
|
|
||||||
const result = await fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Shopify-Storefront-Access-Token': key,
|
|
||||||
...headers
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
...(query && { query }),
|
|
||||||
...(variables && { variables })
|
|
||||||
}),
|
|
||||||
cache,
|
|
||||||
next: { revalidate: 900 } // 15 minutes
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = await result.json();
|
|
||||||
|
|
||||||
if (body.errors) {
|
|
||||||
throw body.errors[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: result.status,
|
|
||||||
body
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
if (isShopifyError(e)) {
|
|
||||||
throw {
|
|
||||||
status: e.status || 500,
|
|
||||||
message: e.message,
|
|
||||||
query
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw {
|
|
||||||
error: e,
|
|
||||||
query
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeEdgesAndNodes = (array: Connection<any>) => {
|
|
||||||
return array.edges.map((edge) => edge?.node);
|
|
||||||
};
|
|
||||||
|
|
||||||
const reshapeCart = (cart: ShopifyCart): Cart => {
|
|
||||||
if (!cart.cost?.totalTaxAmount) {
|
|
||||||
cart.cost.totalTaxAmount = {
|
|
||||||
amount: '0.0',
|
|
||||||
currencyCode: 'USD'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...cart,
|
|
||||||
lines: removeEdgesAndNodes(cart.lines)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const reshapeCollection = (collection: ShopifyCollection): Collection | undefined => {
|
|
||||||
if (!collection) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...collection,
|
|
||||||
path: `/search/${collection.handle}`
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const reshapeCollections = (collections: ShopifyCollection[]) => {
|
|
||||||
const reshapedCollections = [];
|
|
||||||
|
|
||||||
for (const collection of collections) {
|
|
||||||
if (collection) {
|
|
||||||
const reshapedCollection = reshapeCollection(collection);
|
|
||||||
|
|
||||||
if (reshapedCollection) {
|
|
||||||
reshapedCollections.push(reshapedCollection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return reshapedCollections;
|
|
||||||
};
|
|
||||||
|
|
||||||
const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean = true) => {
|
|
||||||
if (!product || (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { images, variants, ...rest } = product;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...rest,
|
|
||||||
images: removeEdgesAndNodes(images),
|
|
||||||
variants: removeEdgesAndNodes(variants)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const reshapeProducts = (products: ShopifyProduct[]) => {
|
|
||||||
const reshapedProducts = [];
|
|
||||||
|
|
||||||
for (const product of products) {
|
|
||||||
if (product) {
|
|
||||||
const reshapedProduct = reshapeProduct(product);
|
|
||||||
|
|
||||||
if (reshapedProduct) {
|
|
||||||
reshapedProducts.push(reshapedProduct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return reshapedProducts;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function createCart(): Promise<Cart> {
|
|
||||||
const res = await shopifyFetch<ShopifyCreateCartOperation>({
|
|
||||||
query: createCartMutation,
|
|
||||||
cache: 'no-store'
|
|
||||||
});
|
|
||||||
|
|
||||||
return reshapeCart(res.body.data.cartCreate.cart);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addToCart(
|
|
||||||
cartId: string,
|
|
||||||
lines: { merchandiseId: string; quantity: number }[]
|
|
||||||
): Promise<Cart> {
|
|
||||||
const res = await shopifyFetch<ShopifyAddToCartOperation>({
|
|
||||||
query: addToCartMutation,
|
|
||||||
variables: {
|
|
||||||
cartId,
|
|
||||||
lines
|
|
||||||
},
|
|
||||||
cache: 'no-store'
|
|
||||||
});
|
|
||||||
return reshapeCart(res.body.data.cartLinesAdd.cart);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeFromCart(cartId: string, lineIds: string[]): Promise<Cart> {
|
|
||||||
const res = await shopifyFetch<ShopifyRemoveFromCartOperation>({
|
|
||||||
query: removeFromCartMutation,
|
|
||||||
variables: {
|
|
||||||
cartId,
|
|
||||||
lineIds
|
|
||||||
},
|
|
||||||
cache: 'no-store'
|
|
||||||
});
|
|
||||||
|
|
||||||
return reshapeCart(res.body.data.cartLinesRemove.cart);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateCart(
|
|
||||||
cartId: string,
|
|
||||||
lines: { id: string; merchandiseId: string; quantity: number }[]
|
|
||||||
): Promise<Cart> {
|
|
||||||
const res = await shopifyFetch<ShopifyUpdateCartOperation>({
|
|
||||||
query: editCartItemsMutation,
|
|
||||||
variables: {
|
|
||||||
cartId,
|
|
||||||
lines
|
|
||||||
},
|
|
||||||
cache: 'no-store'
|
|
||||||
});
|
|
||||||
|
|
||||||
return reshapeCart(res.body.data.cartLinesUpdate.cart);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCart(cartId: string): Promise<Cart | null> {
|
|
||||||
const res = await shopifyFetch<ShopifyCartOperation>({
|
|
||||||
query: getCartQuery,
|
|
||||||
variables: { cartId },
|
|
||||||
cache: 'no-store'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.body.data.cart) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return reshapeCart(res.body.data.cart);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCollection(handle: string): Promise<Collection | undefined> {
|
|
||||||
const res = await shopifyFetch<ShopifyCollectionOperation>({
|
|
||||||
query: getCollectionQuery,
|
|
||||||
variables: {
|
|
||||||
handle
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return reshapeCollection(res.body.data.collection);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCollectionProducts({
|
|
||||||
collection,
|
|
||||||
reverse,
|
|
||||||
sortKey
|
|
||||||
}: {
|
|
||||||
collection: string;
|
|
||||||
reverse?: boolean;
|
|
||||||
sortKey?: string;
|
|
||||||
}): Promise<Product[]> {
|
|
||||||
const res = await shopifyFetch<ShopifyCollectionProductsOperation>({
|
|
||||||
query: getCollectionProductsQuery,
|
|
||||||
variables: {
|
|
||||||
handle: collection,
|
|
||||||
reverse,
|
|
||||||
sortKey
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.body.data.collection) {
|
|
||||||
console.log(`No collection found for \`${collection}\``);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return reshapeProducts(removeEdgesAndNodes(res.body.data.collection.products));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCollections(): Promise<Collection[]> {
|
|
||||||
const res = await shopifyFetch<ShopifyCollectionsOperation>({ query: getCollectionsQuery });
|
|
||||||
const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);
|
|
||||||
const collections = [
|
|
||||||
{
|
|
||||||
handle: '',
|
|
||||||
title: 'All',
|
|
||||||
description: 'All products',
|
|
||||||
seo: {
|
|
||||||
title: 'All',
|
|
||||||
description: 'All products'
|
|
||||||
},
|
|
||||||
path: '/search',
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
},
|
|
||||||
// Filter out the `hidden` collections.
|
|
||||||
// Collections that start with `hidden-*` need to be hidden on the search page.
|
|
||||||
...reshapeCollections(shopifyCollections).filter(
|
|
||||||
(collection) => !collection.handle.startsWith('hidden')
|
|
||||||
)
|
|
||||||
];
|
|
||||||
|
|
||||||
return collections;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getMenu(handle: string): Promise<Menu[]> {
|
|
||||||
const res = await shopifyFetch<ShopifyMenuOperation>({
|
|
||||||
query: getMenuQuery,
|
|
||||||
variables: {
|
|
||||||
handle
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
res.body?.data?.menu?.items.map((item: { title: string; url: string }) => ({
|
|
||||||
title: item.title,
|
|
||||||
path: item.url.replace(domain, '').replace('/collections', '/search').replace('/pages', '')
|
|
||||||
})) || []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPage(handle: string): Promise<Page> {
|
|
||||||
const res = await shopifyFetch<ShopifyPageOperation>({
|
|
||||||
query: getPageQuery,
|
|
||||||
variables: { handle }
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.body.data.pageByHandle;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPages(): Promise<Page[]> {
|
|
||||||
const res = await shopifyFetch<ShopifyPagesOperation>({
|
|
||||||
query: getPagesQuery
|
|
||||||
});
|
|
||||||
|
|
||||||
return removeEdgesAndNodes(res.body.data.pages);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getProduct(handle: string): Promise<Product | undefined> {
|
|
||||||
const res = await shopifyFetch<ShopifyProductOperation>({
|
|
||||||
query: getProductQuery,
|
|
||||||
variables: {
|
|
||||||
handle
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return reshapeProduct(res.body.data.product, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getProductRecommendations(productId: string): Promise<Product[]> {
|
|
||||||
const res = await shopifyFetch<ShopifyProductRecommendationsOperation>({
|
|
||||||
query: getProductRecommendationsQuery,
|
|
||||||
variables: {
|
|
||||||
productId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return reshapeProducts(res.body.data.productRecommendations);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getProducts({
|
|
||||||
query,
|
|
||||||
reverse,
|
|
||||||
sortKey
|
|
||||||
}: {
|
|
||||||
query?: string;
|
|
||||||
reverse?: boolean;
|
|
||||||
sortKey?: string;
|
|
||||||
}): Promise<Product[]> {
|
|
||||||
const res = await shopifyFetch<ShopifyProductsOperation>({
|
|
||||||
query: getProductsQuery,
|
|
||||||
variables: {
|
|
||||||
query,
|
|
||||||
reverse,
|
|
||||||
sortKey
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return reshapeProducts(removeEdgesAndNodes(res.body.data.products));
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import cartFragment from '../fragments/cart';
|
|
||||||
|
|
||||||
export const addToCartMutation = /* GraphQL */ `
|
|
||||||
mutation addToCart($cartId: ID!, $lines: [CartLineInput!]!) {
|
|
||||||
cartLinesAdd(cartId: $cartId, lines: $lines) {
|
|
||||||
cart {
|
|
||||||
...cart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${cartFragment}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const createCartMutation = /* GraphQL */ `
|
|
||||||
mutation createCart($lineItems: [CartLineInput!]) {
|
|
||||||
cartCreate(input: { lines: $lineItems }) {
|
|
||||||
cart {
|
|
||||||
...cart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${cartFragment}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const editCartItemsMutation = /* GraphQL */ `
|
|
||||||
mutation editCartItems($cartId: ID!, $lines: [CartLineUpdateInput!]!) {
|
|
||||||
cartLinesUpdate(cartId: $cartId, lines: $lines) {
|
|
||||||
cart {
|
|
||||||
...cart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${cartFragment}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const removeFromCartMutation = /* GraphQL */ `
|
|
||||||
mutation removeFromCart($cartId: ID!, $lineIds: [ID!]!) {
|
|
||||||
cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {
|
|
||||||
cart {
|
|
||||||
...cart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${cartFragment}
|
|
||||||
`;
|
|
@ -1,10 +0,0 @@
|
|||||||
import cartFragment from '../fragments/cart';
|
|
||||||
|
|
||||||
export const getCartQuery = /* GraphQL */ `
|
|
||||||
query getCart($cartId: ID!) {
|
|
||||||
cart(id: $cartId) {
|
|
||||||
...cart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${cartFragment}
|
|
||||||
`;
|
|
@ -1,56 +0,0 @@
|
|||||||
import productFragment from '../fragments/product';
|
|
||||||
import seoFragment from '../fragments/seo';
|
|
||||||
|
|
||||||
const collectionFragment = /* GraphQL */ `
|
|
||||||
fragment collection on Collection {
|
|
||||||
handle
|
|
||||||
title
|
|
||||||
description
|
|
||||||
seo {
|
|
||||||
...seo
|
|
||||||
}
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
${seoFragment}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const getCollectionQuery = /* GraphQL */ `
|
|
||||||
query getCollection($handle: String!) {
|
|
||||||
collection(handle: $handle) {
|
|
||||||
...collection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${collectionFragment}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const getCollectionsQuery = /* GraphQL */ `
|
|
||||||
query getCollections {
|
|
||||||
collections(first: 100, sortKey: TITLE) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
...collection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${collectionFragment}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const getCollectionProductsQuery = /* GraphQL */ `
|
|
||||||
query getCollectionProducts(
|
|
||||||
$handle: String!
|
|
||||||
$sortKey: ProductCollectionSortKeys
|
|
||||||
$reverse: Boolean
|
|
||||||
) {
|
|
||||||
collection(handle: $handle) {
|
|
||||||
products(sortKey: $sortKey, reverse: $reverse, first: 100) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
...product
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${productFragment}
|
|
||||||
`;
|
|
@ -1,10 +0,0 @@
|
|||||||
export const getMenuQuery = /* GraphQL */ `
|
|
||||||
query getMenu($handle: String!) {
|
|
||||||
menu(handle: $handle) {
|
|
||||||
items {
|
|
||||||
title
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
@ -1,41 +0,0 @@
|
|||||||
import seoFragment from '../fragments/seo';
|
|
||||||
|
|
||||||
const pageFragment = /* GraphQL */ `
|
|
||||||
fragment page on Page {
|
|
||||||
... on Page {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
handle
|
|
||||||
body
|
|
||||||
bodySummary
|
|
||||||
seo {
|
|
||||||
...seo
|
|
||||||
}
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${seoFragment}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const getPageQuery = /* GraphQL */ `
|
|
||||||
query getPage($handle: String!) {
|
|
||||||
pageByHandle(handle: $handle) {
|
|
||||||
...page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${pageFragment}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const getPagesQuery = /* GraphQL */ `
|
|
||||||
query getPages {
|
|
||||||
pages(first: 100) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
...page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${pageFragment}
|
|
||||||
`;
|
|
@ -1,32 +0,0 @@
|
|||||||
import productFragment from '../fragments/product';
|
|
||||||
|
|
||||||
export const getProductQuery = /* GraphQL */ `
|
|
||||||
query getProduct($handle: String!) {
|
|
||||||
product(handle: $handle) {
|
|
||||||
...product
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${productFragment}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const getProductsQuery = /* GraphQL */ `
|
|
||||||
query getProducts($sortKey: ProductSortKeys, $reverse: Boolean, $query: String) {
|
|
||||||
products(sortKey: $sortKey, reverse: $reverse, query: $query, first: 100) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
...product
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${productFragment}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const getProductRecommendationsQuery = /* GraphQL */ `
|
|
||||||
query getProductRecommendations($productId: ID!) {
|
|
||||||
productRecommendations(productId: $productId) {
|
|
||||||
...product
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${productFragment}
|
|
||||||
`;
|
|
@ -1,265 +0,0 @@
|
|||||||
export type Maybe<T> = T | null;
|
|
||||||
|
|
||||||
export type Connection<T> = {
|
|
||||||
edges: Array<Edge<T>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Edge<T> = {
|
|
||||||
node: T;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Cart = Omit<ShopifyCart, 'lines'> & {
|
|
||||||
lines: CartItem[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CartItem = {
|
|
||||||
id: string;
|
|
||||||
quantity: number;
|
|
||||||
cost: {
|
|
||||||
totalAmount: Money;
|
|
||||||
};
|
|
||||||
merchandise: {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
selectedOptions: {
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
}[];
|
|
||||||
product: Product;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Collection = ShopifyCollection & {
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Image = {
|
|
||||||
url: string;
|
|
||||||
altText: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Menu = {
|
|
||||||
title: string;
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Money = {
|
|
||||||
amount: string;
|
|
||||||
currencyCode: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Page = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
handle: string;
|
|
||||||
body: string;
|
|
||||||
bodySummary: string;
|
|
||||||
seo?: SEO;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Product = Omit<ShopifyProduct, 'variants' | 'images'> & {
|
|
||||||
variants: ProductVariant[];
|
|
||||||
images: Image[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ProductOption = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
values: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ProductVariant = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
availableForSale: boolean;
|
|
||||||
selectedOptions: {
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
}[];
|
|
||||||
price: Money;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SEO = {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyCart = {
|
|
||||||
id: string;
|
|
||||||
checkoutUrl: string;
|
|
||||||
cost: {
|
|
||||||
subtotalAmount: Money;
|
|
||||||
totalAmount: Money;
|
|
||||||
totalTaxAmount: Money;
|
|
||||||
};
|
|
||||||
lines: Connection<CartItem>;
|
|
||||||
totalQuantity: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyCollection = {
|
|
||||||
handle: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
seo: SEO;
|
|
||||||
updatedAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyProduct = {
|
|
||||||
id: string;
|
|
||||||
handle: string;
|
|
||||||
availableForSale: boolean;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
descriptionHtml: string;
|
|
||||||
options: ProductOption[];
|
|
||||||
priceRange: {
|
|
||||||
maxVariantPrice: Money;
|
|
||||||
minVariantPrice: Money;
|
|
||||||
};
|
|
||||||
variants: Connection<ProductVariant>;
|
|
||||||
featuredImage: Image;
|
|
||||||
images: Connection<Image>;
|
|
||||||
seo: SEO;
|
|
||||||
tags: string[];
|
|
||||||
updatedAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyCartOperation = {
|
|
||||||
data: {
|
|
||||||
cart: ShopifyCart;
|
|
||||||
};
|
|
||||||
variables: {
|
|
||||||
cartId: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyCreateCartOperation = {
|
|
||||||
data: { cartCreate: { cart: ShopifyCart } };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyAddToCartOperation = {
|
|
||||||
data: {
|
|
||||||
cartLinesAdd: {
|
|
||||||
cart: ShopifyCart;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
variables: {
|
|
||||||
cartId: string;
|
|
||||||
lines: {
|
|
||||||
merchandiseId: string;
|
|
||||||
quantity: number;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyRemoveFromCartOperation = {
|
|
||||||
data: {
|
|
||||||
cartLinesRemove: {
|
|
||||||
cart: ShopifyCart;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
variables: {
|
|
||||||
cartId: string;
|
|
||||||
lineIds: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyUpdateCartOperation = {
|
|
||||||
data: {
|
|
||||||
cartLinesUpdate: {
|
|
||||||
cart: ShopifyCart;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
variables: {
|
|
||||||
cartId: string;
|
|
||||||
lines: {
|
|
||||||
id: string;
|
|
||||||
merchandiseId: string;
|
|
||||||
quantity: number;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyCollectionOperation = {
|
|
||||||
data: {
|
|
||||||
collection: ShopifyCollection;
|
|
||||||
};
|
|
||||||
variables: {
|
|
||||||
handle: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyCollectionProductsOperation = {
|
|
||||||
data: {
|
|
||||||
collection: {
|
|
||||||
products: Connection<ShopifyProduct>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
variables: {
|
|
||||||
handle: string;
|
|
||||||
reverse?: boolean;
|
|
||||||
sortKey?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyCollectionsOperation = {
|
|
||||||
data: {
|
|
||||||
collections: Connection<ShopifyCollection>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyMenuOperation = {
|
|
||||||
data: {
|
|
||||||
menu?: {
|
|
||||||
items: {
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
variables: {
|
|
||||||
handle: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyPageOperation = {
|
|
||||||
data: { pageByHandle: Page };
|
|
||||||
variables: { handle: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyPagesOperation = {
|
|
||||||
data: {
|
|
||||||
pages: Connection<Page>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyProductOperation = {
|
|
||||||
data: { product: ShopifyProduct };
|
|
||||||
variables: {
|
|
||||||
handle: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyProductRecommendationsOperation = {
|
|
||||||
data: {
|
|
||||||
productRecommendations: ShopifyProduct[];
|
|
||||||
};
|
|
||||||
variables: {
|
|
||||||
productId: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyProductsOperation = {
|
|
||||||
data: {
|
|
||||||
products: Connection<ShopifyProduct>;
|
|
||||||
};
|
|
||||||
variables: {
|
|
||||||
query?: string;
|
|
||||||
reverse?: boolean;
|
|
||||||
sortKey?: string;
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,4 +1,4 @@
|
|||||||
export interface ShopifyErrorLike {
|
export interface VercelCommerceErrorLike {
|
||||||
status: number;
|
status: number;
|
||||||
message: Error;
|
message: Error;
|
||||||
}
|
}
|
||||||
@ -7,7 +7,7 @@ export const isObject = (object: unknown): object is Record<string, unknown> =>
|
|||||||
return typeof object === 'object' && object !== null && !Array.isArray(object);
|
return typeof object === 'object' && object !== null && !Array.isArray(object);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isShopifyError = (error: unknown): error is ShopifyErrorLike => {
|
export const isVercelCommerceError = (error: unknown): error is VercelCommerceErrorLike => {
|
||||||
if (!isObject(error)) return false;
|
if (!isObject(error)) return false;
|
||||||
|
|
||||||
if (error instanceof Error) return true;
|
if (error instanceof Error) return true;
|
||||||
|
@ -5,12 +5,9 @@ module.exports = {
|
|||||||
ignoreDuringBuilds: true
|
ignoreDuringBuilds: true
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
formats: ['image/avif', 'image/webp'],
|
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: 'https',
|
hostname: process.env.BIGCOMMERCE_CDN_HOSTNAME ?? '*.bigcommerce.com'
|
||||||
hostname: 'cdn.shopify.com',
|
|
||||||
pathname: '/s/files/**'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
1146
pnpm-lock.yaml
generated
1146
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user