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"
|
||||
SITE_NAME="Next.js Commerce"
|
||||
SHOPIFY_STOREFRONT_ACCESS_TOKEN=
|
||||
SHOPIFY_STORE_DOMAIN=
|
||||
SITE_NAME="Next.js Commerce by BigCommerce"
|
||||
BIGCOMMERCE_ACCESS_TOKEN=
|
||||
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 Prose from 'components/prose';
|
||||
import { getPage } from 'lib/shopify';
|
||||
import { getPage } from 'lib/bigcommerce';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { addToCart, removeFromCart, updateCart } from 'lib/shopify';
|
||||
import { isShopifyError } from 'lib/type-guards';
|
||||
import { addToCart, removeFromCart, updateCart } from 'lib/bigcommerce';
|
||||
import { isVercelCommerceError } from 'lib/type-guards';
|
||||
|
||||
function formatErrorMessage(err: Error): string {
|
||||
return JSON.stringify(err, Object.getOwnPropertyNames(err));
|
||||
@ -10,16 +10,18 @@ function formatErrorMessage(err: Error): string {
|
||||
|
||||
export async function POST(req: NextRequest): Promise<Response> {
|
||||
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 });
|
||||
} else if (isBigCommerceAPI && !merchandiseId?.length) {
|
||||
return NextResponse.json({ error: 'Missing variantId' }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
await addToCart(cartId, [{ merchandiseId, quantity: 1 }]);
|
||||
await addToCart(cartId || '', [{ merchandiseId, quantity: 1 }]);
|
||||
return NextResponse.json({ status: 204 });
|
||||
} catch (e) {
|
||||
if (isShopifyError(e)) {
|
||||
if (isVercelCommerceError(e)) {
|
||||
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 });
|
||||
} catch (e) {
|
||||
if (isShopifyError(e)) {
|
||||
if (isVercelCommerceError(e)) {
|
||||
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]);
|
||||
return NextResponse.json({ status: 204 });
|
||||
} catch (e) {
|
||||
if (isShopifyError(e)) {
|
||||
if (isVercelCommerceError(e)) {
|
||||
return NextResponse.json({ message: formatErrorMessage(e.message) }, { status: e.status });
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { Suspense } from 'react';
|
||||
export const runtime = 'edge';
|
||||
|
||||
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: {
|
||||
images: [
|
||||
{
|
||||
|
@ -10,8 +10,8 @@ import { Gallery } from 'components/product/gallery';
|
||||
import { VariantSelector } from 'components/product/variant-selector';
|
||||
import Prose from 'components/prose';
|
||||
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
||||
import { getProduct, getProductRecommendations } from 'lib/shopify';
|
||||
import { Image } from 'lib/shopify/types';
|
||||
import { getProduct, getProductRecommendations } from 'lib/bigcommerce';
|
||||
import { Image } from 'lib/bigcommerce/types';
|
||||
|
||||
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 { notFound } from 'next/navigation';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Grid from 'components/grid';
|
||||
import ProductGridItems from 'components/layout/product-grid-items';
|
||||
import { getProducts } from 'lib/bigcommerce';
|
||||
import { defaultSort, sorting } from 'lib/constants';
|
||||
import { getProducts } from 'lib/shopify';
|
||||
|
||||
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';
|
||||
|
||||
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 Link from 'next/link';
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { useCookies } from 'react-cookie';
|
||||
import CartIcon from 'components/icons/cart';
|
||||
import CartModal from './modal';
|
||||
|
||||
import type { Cart } from 'lib/shopify/types';
|
||||
import type { VercelCart as Cart } from 'lib/bigcommerce/types';
|
||||
|
||||
export default function CartButton({
|
||||
cart,
|
||||
|
@ -4,7 +4,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { startTransition, useState } from 'react';
|
||||
|
||||
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 }) {
|
||||
const router = useRouter();
|
||||
|
@ -4,7 +4,7 @@ import { startTransition, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import MinusIcon from 'components/icons/minus';
|
||||
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';
|
||||
|
||||
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 CartButton from './button';
|
||||
|
||||
|
@ -7,7 +7,7 @@ import CloseIcon from 'components/icons/close';
|
||||
import ShoppingBagIcon from 'components/icons/shopping-bag';
|
||||
import Price from 'components/price';
|
||||
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 DeleteItemButton from './delete-item-button';
|
||||
import EditItemQuantityButton from './edit-item-quantity-button';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { GridTileImage } from 'components/grid/tile';
|
||||
import { getCollectionProducts } from 'lib/shopify';
|
||||
import type { Product } from 'lib/shopify/types';
|
||||
import { getCollectionProducts } from 'lib/bigcommerce';
|
||||
import type { VercelProduct as Product } from 'lib/bigcommerce/types';
|
||||
import Link from 'next/link';
|
||||
|
||||
function ThreeItemGridItem({
|
||||
|
@ -3,8 +3,8 @@ import Link from 'next/link';
|
||||
import GitHubIcon from 'components/icons/github';
|
||||
import LogoIcon from 'components/icons/logo';
|
||||
import VercelIcon from 'components/icons/vercel';
|
||||
import { getMenu } from 'lib/shopify';
|
||||
import { Menu } from 'lib/shopify/types';
|
||||
import { getMenu } from 'lib/bigcommerce';
|
||||
import { VercelMenu as Menu } from 'lib/bigcommerce/types';
|
||||
|
||||
const { SITE_NAME } = process.env;
|
||||
|
||||
|
@ -4,18 +4,19 @@ import { Suspense } from 'react';
|
||||
import Cart from 'components/cart';
|
||||
import CartIcon from 'components/icons/cart';
|
||||
import LogoIcon from 'components/icons/logo';
|
||||
import { getMenu } from 'lib/shopify';
|
||||
import { Menu } from 'lib/shopify/types';
|
||||
import { getMenu } from 'lib/bigcommerce';
|
||||
import { VercelMenu as Menu } from 'lib/bigcommerce/types';
|
||||
import MobileMenu from './mobile-menu';
|
||||
import Search from './search';
|
||||
|
||||
export default async function Navbar() {
|
||||
const menu = await getMenu('next-js-frontend-header-menu');
|
||||
const demoMenu = menu.slice(0, 4);
|
||||
|
||||
return (
|
||||
<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">
|
||||
<MobileMenu menu={menu} />
|
||||
<MobileMenu menu={demoMenu} />
|
||||
</div>
|
||||
<div className="flex justify-self-center md:w-1/3 md:justify-self-start">
|
||||
<div className="md:mr-4">
|
||||
@ -23,9 +24,9 @@ export default async function Navbar() {
|
||||
<LogoIcon className="h-8 transition-transform hover:scale-110" />
|
||||
</Link>
|
||||
</div>
|
||||
{menu.length ? (
|
||||
{demoMenu.length ? (
|
||||
<ul className="hidden md:flex">
|
||||
{menu.map((item: Menu) => (
|
||||
{demoMenu.map((item: Menu) => (
|
||||
<li key={item.title}>
|
||||
<Link
|
||||
href={item.path}
|
||||
|
@ -8,7 +8,7 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
import CloseIcon from 'components/icons/close';
|
||||
import MenuIcon from 'components/icons/menu';
|
||||
import { Menu } from 'lib/shopify/types';
|
||||
import { VercelMenu as Menu } from 'lib/bigcommerce/types';
|
||||
import Search from './search';
|
||||
|
||||
export default function MobileMenu({ menu }: { menu: Menu[] }) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Grid from 'components/grid';
|
||||
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';
|
||||
|
||||
export default function ProductGridItems({ products }: { products: Product[] }) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { getCollections } from 'lib/shopify';
|
||||
import { getCollections } from 'lib/bigcommerce';
|
||||
import FilterList from './filter';
|
||||
|
||||
async function CollectionList() {
|
||||
|
@ -5,7 +5,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState, useTransition } from 'react';
|
||||
|
||||
import LoadingDots from 'components/loading-dots';
|
||||
import { ProductVariant } from 'lib/shopify/types';
|
||||
import { VercelProductVariant as ProductVariant } from 'lib/bigcommerce/types';
|
||||
|
||||
export function AddToCart({
|
||||
variants,
|
||||
@ -14,7 +14,8 @@ export function AddToCart({
|
||||
variants: ProductVariant[];
|
||||
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 searchParams = useSearchParams();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
@ -28,7 +29,7 @@ export function AddToCart({
|
||||
);
|
||||
|
||||
if (variant) {
|
||||
setSelectedVariantId(variant.id);
|
||||
setSelectedVariantId(variant.parentId || variant.id);
|
||||
}
|
||||
}, [searchParams, variants, setSelectedVariantId]);
|
||||
|
||||
@ -42,7 +43,8 @@ export function AddToCart({
|
||||
const response = await fetch(`/api/cart`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
merchandiseId: selectedVariantId
|
||||
merchandiseId: selectedVariantId,
|
||||
isBigCommerceAPI: true
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -1,7 +1,10 @@
|
||||
'use client';
|
||||
|
||||
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 Link from 'next/link';
|
||||
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 = {
|
||||
title: string;
|
||||
slug: string | null;
|
||||
sortKey: 'RELEVANCE' | 'BEST_SELLING' | 'CREATED_AT' | 'PRICE';
|
||||
sortKey: keyof typeof VercelSortKeys;
|
||||
reverse: boolean;
|
||||
};
|
||||
|
||||
@ -22,4 +49,3 @@ export const sorting: SortFilterItem[] = [
|
||||
|
||||
export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
|
||||
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;
|
||||
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);
|
||||
};
|
||||
|
||||
export const isShopifyError = (error: unknown): error is ShopifyErrorLike => {
|
||||
export const isVercelCommerceError = (error: unknown): error is VercelCommerceErrorLike => {
|
||||
if (!isObject(error)) return false;
|
||||
|
||||
if (error instanceof Error) return true;
|
||||
|
@ -5,12 +5,9 @@ module.exports = {
|
||||
ignoreDuringBuilds: true
|
||||
},
|
||||
images: {
|
||||
formats: ['image/avif', 'image/webp'],
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'cdn.shopify.com',
|
||||
pathname: '/s/files/**'
|
||||
hostname: process.env.BIGCOMMERCE_CDN_HOSTNAME ?? '*.bigcommerce.com'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
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