mirror of
https://github.com/vercel/commerce.git
synced 2025-07-04 20:21:21 +00:00
Refractor
This commit is contained in:
parent
0e7e7b7d5f
commit
0f82dfdcba
@ -151,5 +151,5 @@ Next, you're free to customize the starter. More updates coming soon. Stay tuned
|
|||||||
After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard.
|
After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard.
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
BigCommerce team has been notified and they plan to add more details about this subject.
|
BigCommerce team has been notified and they plan to add more detailed about this subject.
|
||||||
</details>
|
</details>
|
||||||
|
@ -77,6 +77,7 @@ html {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
|
font-feature-settings: 'case' 1, 'rlig' 1, 'calt' 0;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
@ -38,7 +38,6 @@ const LoginView: FC<Props> = () => {
|
|||||||
} catch ({ errors }) {
|
} catch ({ errors }) {
|
||||||
setMessage(errors[0].message)
|
setMessage(errors[0].message)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setDisabled(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,9 +70,6 @@ const CartItem = ({
|
|||||||
if (item.quantity !== Number(quantity)) {
|
if (item.quantity !== Number(quantity)) {
|
||||||
setQuantity(item.quantity)
|
setQuantity(item.quantity)
|
||||||
}
|
}
|
||||||
// TODO: currently not including quantity in deps is intended, but we should
|
|
||||||
// do this differently as it could break easily
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [item.quantity])
|
}, [item.quantity])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -73,7 +73,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
<div className="flex items-center text-primary text-sm">
|
<div className="flex items-center text-primary text-sm">
|
||||||
<span className="text-primary">Created by</span>
|
<span className="text-primary">Created by</span>
|
||||||
<a
|
<a
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
href="https://vercel.com"
|
href="https://vercel.com"
|
||||||
aria-label="Vercel.com Link"
|
aria-label="Vercel.com Link"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -1,23 +1,7 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply py-12 flex flex-col w-full px-6;
|
@apply flex flex-col w-full;
|
||||||
|
|
||||||
@screen md {
|
@screen md {
|
||||||
@apply flex-row;
|
@apply flex-row;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .asideWrapper {
|
|
||||||
@apply pr-3 w-full relative;
|
|
||||||
|
|
||||||
@screen md {
|
|
||||||
@apply w-48;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .aside {
|
|
||||||
@apply flex flex-row w-full justify-around mb-12;
|
|
||||||
|
|
||||||
@screen md {
|
|
||||||
@apply mb-0 block sticky top-32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,66 +5,40 @@ import { Grid } from '@components/ui'
|
|||||||
import { ProductCard } from '@components/product'
|
import { ProductCard } from '@components/product'
|
||||||
import s from './HomeAllProductsGrid.module.css'
|
import s from './HomeAllProductsGrid.module.css'
|
||||||
import { getCategoryPath, getDesignerPath } from '@lib/search'
|
import { getCategoryPath, getDesignerPath } from '@lib/search'
|
||||||
|
import { Category } from '@commerce/types/site'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
categories?: any
|
categories?: Category[]
|
||||||
brands?: any
|
brands?: any
|
||||||
products?: Product[]
|
products?: Product[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const HomeAllProductsGrid: FC<Props> = ({
|
const HomeAllProductsGrid: FC<Props> = ({
|
||||||
categories,
|
categories = [],
|
||||||
brands,
|
brands,
|
||||||
products = [],
|
products = [],
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={s.root}>
|
<div className={s.root}>
|
||||||
<div className={s.asideWrapper}>
|
|
||||||
<div className={s.aside}>
|
|
||||||
<ul className="mb-10">
|
|
||||||
<li className="py-1 text-base font-bold tracking-wide">
|
|
||||||
<Link href={getCategoryPath('')}>
|
|
||||||
<a>All Categories</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{categories.map((cat: any) => (
|
|
||||||
<li key={cat.path} className="py-1 text-accent-8 text-base">
|
|
||||||
<Link href={getCategoryPath(cat.path)}>
|
|
||||||
<a>{cat.name}</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<ul className="">
|
|
||||||
<li className="py-1 text-base font-bold tracking-wide">
|
|
||||||
<Link href={getDesignerPath('')}>
|
|
||||||
<a>All Designers</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{brands.flatMap(({ node }: any) => (
|
|
||||||
<li key={node.path} className="py-1 text-accent-8 text-base">
|
|
||||||
<Link href={getDesignerPath(node.path)}>
|
|
||||||
<a>{node.name}</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Grid layout="normal">
|
{categories.map((category) => (
|
||||||
{products.map((product) => (
|
<div>
|
||||||
|
<div className="text-primary font-bold">{category.name}</div>
|
||||||
|
<div className="flex">
|
||||||
|
{products.slice(0, 4).map((product) => (
|
||||||
<ProductCard
|
<ProductCard
|
||||||
key={product.path}
|
key={product.path}
|
||||||
product={product}
|
product={product}
|
||||||
variant="simple"
|
variant="simple"
|
||||||
imgProps={{
|
imgProps={{
|
||||||
width: 480,
|
width: 300,
|
||||||
height: 480,
|
height: 300,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -24,7 +24,7 @@ const Loading = () => (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const dynamicProps = {
|
const dynamicProps = {
|
||||||
loading: Loading,
|
loading: () => <Loading />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignUpView = dynamic(
|
const SignUpView = dynamic(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, memo, useEffect } from 'react'
|
import { FC, InputHTMLAttributes, useEffect, useMemo } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import s from './Searchbar.module.css'
|
import s from './Searchbar.module.css'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
@ -13,7 +13,7 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
router.prefetch('/search')
|
router.prefetch('/search')
|
||||||
}, [router])
|
}, [])
|
||||||
|
|
||||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -32,7 +32,8 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return useMemo(
|
||||||
|
() => (
|
||||||
<div className={cn(s.root, className)}>
|
<div className={cn(s.root, className)}>
|
||||||
<label className="hidden" htmlFor={id}>
|
<label className="hidden" htmlFor={id}>
|
||||||
Search
|
Search
|
||||||
@ -54,7 +55,9 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
),
|
||||||
|
[]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(Searchbar)
|
export default Searchbar
|
||||||
|
@ -7,7 +7,6 @@ import Image, { ImageProps } from 'next/image'
|
|||||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||||
import usePrice from '@framework/product/use-price'
|
import usePrice from '@framework/product/use-price'
|
||||||
import ProductTag from '../ProductTag'
|
import ProductTag from '../ProductTag'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
product: Product
|
product: Product
|
||||||
@ -24,6 +23,7 @@ const ProductCard: FC<Props> = ({
|
|||||||
className,
|
className,
|
||||||
noNameTag = false,
|
noNameTag = false,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { price } = usePrice({
|
const { price } = usePrice({
|
||||||
amount: product.price.value,
|
amount: product.price.value,
|
||||||
@ -38,7 +38,7 @@ const ProductCard: FC<Props> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/product/${product.slug}`}>
|
<Link href={`/product/${product.slug}`} {...props}>
|
||||||
<a className={rootClassName}>
|
<a className={rootClassName}>
|
||||||
{variant === 'slim' && (
|
{variant === 'slim' && (
|
||||||
<>
|
<>
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
import { memo } from 'react'
|
|
||||||
import { Swatch } from '@components/product'
|
import { Swatch } from '@components/product'
|
||||||
import type { ProductOption } from '@commerce/types/product'
|
import type { ProductOption } from '@commerce/types/product'
|
||||||
import { SelectedOptions } from '../helpers'
|
import { SelectedOptions } from '../helpers'
|
||||||
|
import React from 'react'
|
||||||
interface ProductOptionsProps {
|
interface ProductOptionsProps {
|
||||||
options: ProductOption[]
|
options: ProductOption[]
|
||||||
selectedOptions: SelectedOptions
|
selectedOptions: SelectedOptions
|
||||||
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
|
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductOptions: React.FC<ProductOptionsProps> = ({
|
const ProductOptions: React.FC<ProductOptionsProps> = React.memo(
|
||||||
options,
|
({ options, selectedOptions, setSelectedOptions }) => {
|
||||||
selectedOptions,
|
|
||||||
setSelectedOptions,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{options.map((opt) => (
|
{options.map((opt) => (
|
||||||
@ -35,7 +31,8 @@ const ProductOptions: React.FC<ProductOptionsProps> = ({
|
|||||||
setSelectedOptions((selectedOptions) => {
|
setSelectedOptions((selectedOptions) => {
|
||||||
return {
|
return {
|
||||||
...selectedOptions,
|
...selectedOptions,
|
||||||
[opt.displayName.toLowerCase()]: v.label.toLowerCase(),
|
[opt.displayName.toLowerCase()]:
|
||||||
|
v.label.toLowerCase(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
@ -47,6 +44,7 @@ const ProductOptions: React.FC<ProductOptionsProps> = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export default memo(ProductOptions)
|
export default ProductOptions
|
||||||
|
@ -23,7 +23,7 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectDefaultOptionFromProduct(product, setSelectedOptions)
|
selectDefaultOptionFromProduct(product, setSelectedOptions)
|
||||||
}, [product])
|
}, [])
|
||||||
|
|
||||||
const variant = getProductVariant(product, selectedOptions)
|
const variant = getProductVariant(product, selectedOptions)
|
||||||
const addToCart = async () => {
|
const addToCart = async () => {
|
||||||
|
@ -66,13 +66,17 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
const slider = sliderContainerRef.current!
|
sliderContainerRef.current!.addEventListener(
|
||||||
|
'touchstart',
|
||||||
slider.addEventListener('touchstart', preventNavigation)
|
preventNavigation
|
||||||
|
)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (slider) {
|
if (sliderContainerRef.current) {
|
||||||
slider.removeEventListener('touchstart', preventNavigation)
|
sliderContainerRef.current!.removeEventListener(
|
||||||
|
'touchstart',
|
||||||
|
preventNavigation
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { FC, MouseEventHandler, memo } from 'react'
|
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
import React from 'react'
|
||||||
import s from './ProductSliderControl.module.css'
|
import s from './ProductSliderControl.module.css'
|
||||||
import { ArrowLeft, ArrowRight } from '@components/icons'
|
import { ArrowLeft, ArrowRight } from '@components/icons'
|
||||||
|
|
||||||
interface ProductSliderControl {
|
interface ProductSliderControl {
|
||||||
onPrev: MouseEventHandler<HTMLButtonElement>
|
onPrev: React.MouseEventHandler<HTMLButtonElement>
|
||||||
onNext: MouseEventHandler<HTMLButtonElement>
|
onNext: React.MouseEventHandler<HTMLButtonElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductSliderControl: FC<ProductSliderControl> = ({ onPrev, onNext }) => (
|
const ProductSliderControl: React.FC<ProductSliderControl> = React.memo(
|
||||||
|
({ onPrev, onNext }) => (
|
||||||
<div className={s.control}>
|
<div className={s.control}>
|
||||||
<button
|
<button
|
||||||
className={cn(s.leftControl)}
|
className={cn(s.leftControl)}
|
||||||
@ -25,6 +26,6 @@ const ProductSliderControl: FC<ProductSliderControl> = ({ onPrev, onNext }) => (
|
|||||||
<ArrowRight />
|
<ArrowRight />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
export default ProductSliderControl
|
||||||
export default memo(ProductSliderControl)
|
|
||||||
|
@ -5,11 +5,11 @@ import s from './ProductView.module.css'
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import type { Product } from '@commerce/types/product'
|
import type { Product } from '@commerce/types/product'
|
||||||
import usePrice from '@framework/product/use-price'
|
import usePrice from '@framework/product/use-price'
|
||||||
import { WishlistButton } from '@components/wishlist'
|
|
||||||
import { ProductSlider, ProductCard } from '@components/product'
|
import { ProductSlider, ProductCard } from '@components/product'
|
||||||
import { Container, Text } from '@components/ui'
|
import { Container, Text } from '@components/ui'
|
||||||
import ProductSidebar from '../ProductSidebar'
|
import ProductSidebar from '../ProductSidebar'
|
||||||
import ProductTag from '../ProductTag'
|
import ProductTag from '../ProductTag'
|
||||||
|
import { WishlistButton } from '@components/wishlist'
|
||||||
interface ProductViewProps {
|
interface ProductViewProps {
|
||||||
product: Product
|
product: Product
|
||||||
relatedProducts: Product[]
|
relatedProducts: Product[]
|
||||||
|
@ -1,439 +0,0 @@
|
|||||||
import cn from 'classnames'
|
|
||||||
import type { SearchPropsType } from '@lib/search-props'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
|
|
||||||
import { Layout } from '@components/common'
|
|
||||||
import { ProductCard } from '@components/product'
|
|
||||||
import type { Product } from '@commerce/types/product'
|
|
||||||
import { Container, Skeleton } from '@components/ui'
|
|
||||||
|
|
||||||
import useSearch from '@framework/product/use-search'
|
|
||||||
|
|
||||||
import getSlug from '@lib/get-slug'
|
|
||||||
import rangeMap from '@lib/range-map'
|
|
||||||
|
|
||||||
const SORT = {
|
|
||||||
'trending-desc': 'Trending',
|
|
||||||
'latest-desc': 'Latest arrivals',
|
|
||||||
'price-asc': 'Price: Low to high',
|
|
||||||
'price-desc': 'Price: High to low',
|
|
||||||
}
|
|
||||||
|
|
||||||
import {
|
|
||||||
filterQuery,
|
|
||||||
getCategoryPath,
|
|
||||||
getDesignerPath,
|
|
||||||
useSearchMeta,
|
|
||||||
} from '@lib/search'
|
|
||||||
|
|
||||||
export default function Search({ categories, brands }: SearchPropsType) {
|
|
||||||
const [activeFilter, setActiveFilter] = useState('')
|
|
||||||
const [toggleFilter, setToggleFilter] = useState(false)
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const { asPath, locale } = router
|
|
||||||
const { q, sort } = router.query
|
|
||||||
// `q` can be included but because categories and designers can't be searched
|
|
||||||
// in the same way of products, it's better to ignore the search input if one
|
|
||||||
// of those is selected
|
|
||||||
const query = filterQuery({ sort })
|
|
||||||
|
|
||||||
const { pathname, category, brand } = useSearchMeta(asPath)
|
|
||||||
const activeCategory = categories.find((cat: any) => cat.slug === category)
|
|
||||||
const activeBrand = brands.find(
|
|
||||||
(b: any) => getSlug(b.node.path) === `brands/${brand}`
|
|
||||||
)?.node
|
|
||||||
|
|
||||||
const { data } = useSearch({
|
|
||||||
search: typeof q === 'string' ? q : '',
|
|
||||||
categoryId: activeCategory?.id,
|
|
||||||
brandId: (activeBrand as any)?.entityId,
|
|
||||||
sort: typeof sort === 'string' ? sort : '',
|
|
||||||
locale,
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleClick = (event: any, filter: string) => {
|
|
||||||
if (filter !== activeFilter) {
|
|
||||||
setToggleFilter(true)
|
|
||||||
} else {
|
|
||||||
setToggleFilter(!toggleFilter)
|
|
||||||
}
|
|
||||||
setActiveFilter(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 mt-3 mb-20">
|
|
||||||
<div className="col-span-8 lg:col-span-2 order-1 lg:order-none">
|
|
||||||
{/* Categories */}
|
|
||||||
<div className="relative inline-block w-full">
|
|
||||||
<div className="lg:hidden">
|
|
||||||
<span className="rounded-md shadow-sm">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => handleClick(e, 'categories')}
|
|
||||||
className="flex justify-between w-full rounded-sm border border-accent-3 px-4 py-3 bg-accent-0 text-sm leading-5 font-medium text-accent-4 hover:text-accent-5 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-accent-1 active:text-accent-8 transition ease-in-out duration-150"
|
|
||||||
id="options-menu"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded="true"
|
|
||||||
>
|
|
||||||
{activeCategory?.name
|
|
||||||
? `Category: ${activeCategory?.name}`
|
|
||||||
: 'All Categories'}
|
|
||||||
<svg
|
|
||||||
className="-mr-1 ml-2 h-5 w-5"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`origin-top-left absolute lg:relative left-0 mt-2 w-full rounded-md shadow-lg lg:shadow-none z-10 mb-10 lg:block ${
|
|
||||||
activeFilter !== 'categories' || toggleFilter !== true
|
|
||||||
? 'hidden'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="rounded-sm bg-accent-0 shadow-xs lg:bg-none lg:shadow-none">
|
|
||||||
<div
|
|
||||||
role="menu"
|
|
||||||
aria-orientation="vertical"
|
|
||||||
aria-labelledby="options-menu"
|
|
||||||
>
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
className={cn(
|
|
||||||
'block text-sm leading-5 text-accent-4 lg:text-base lg:no-underline lg:font-bold lg:tracking-wide hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
|
|
||||||
{
|
|
||||||
underline: !activeCategory?.name,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={{ pathname: getCategoryPath('', brand), query }}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
onClick={(e) => handleClick(e, 'categories')}
|
|
||||||
className={
|
|
||||||
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
All Categories
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{categories.map((cat: any) => (
|
|
||||||
<li
|
|
||||||
key={cat.path}
|
|
||||||
className={cn(
|
|
||||||
'block text-sm leading-5 text-accent-4 hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
|
|
||||||
{
|
|
||||||
underline: activeCategory?.id === cat.id,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={{
|
|
||||||
pathname: getCategoryPath(cat.path, brand),
|
|
||||||
query,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
onClick={(e) => handleClick(e, 'categories')}
|
|
||||||
className={
|
|
||||||
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{cat.name}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Designs */}
|
|
||||||
<div className="relative inline-block w-full">
|
|
||||||
<div className="lg:hidden mt-3">
|
|
||||||
<span className="rounded-md shadow-sm">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => handleClick(e, 'brands')}
|
|
||||||
className="flex justify-between w-full rounded-sm border border-accent-3 px-4 py-3 bg-accent-0 text-sm leading-5 font-medium text-accent-8 hover:text-accent-5 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-accent-1 active:text-accent-8 transition ease-in-out duration-150"
|
|
||||||
id="options-menu"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded="true"
|
|
||||||
>
|
|
||||||
{activeBrand?.name
|
|
||||||
? `Design: ${activeBrand?.name}`
|
|
||||||
: 'All Designs'}
|
|
||||||
<svg
|
|
||||||
className="-mr-1 ml-2 h-5 w-5"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`origin-top-left absolute lg:relative left-0 mt-2 w-full rounded-md shadow-lg lg:shadow-none z-10 mb-10 lg:block ${
|
|
||||||
activeFilter !== 'brands' || toggleFilter !== true
|
|
||||||
? 'hidden'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="rounded-sm bg-accent-0 shadow-xs lg:bg-none lg:shadow-none">
|
|
||||||
<div
|
|
||||||
role="menu"
|
|
||||||
aria-orientation="vertical"
|
|
||||||
aria-labelledby="options-menu"
|
|
||||||
>
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
className={cn(
|
|
||||||
'block text-sm leading-5 text-accent-4 lg:text-base lg:no-underline lg:font-bold lg:tracking-wide hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
|
|
||||||
{
|
|
||||||
underline: !activeBrand?.name,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={{
|
|
||||||
pathname: getDesignerPath('', category),
|
|
||||||
query,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
onClick={(e) => handleClick(e, 'brands')}
|
|
||||||
className={
|
|
||||||
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
All Designers
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{brands.flatMap(({ node }: { node: any }) => (
|
|
||||||
<li
|
|
||||||
key={node.path}
|
|
||||||
className={cn(
|
|
||||||
'block text-sm leading-5 text-accent-4 hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
|
|
||||||
{
|
|
||||||
// @ts-ignore Shopify - Fix this types
|
|
||||||
underline: activeBrand?.entityId === node.entityId,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={{
|
|
||||||
pathname: getDesignerPath(node.path, category),
|
|
||||||
query,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
onClick={(e) => handleClick(e, 'brands')}
|
|
||||||
className={
|
|
||||||
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{node.name}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Products */}
|
|
||||||
<div className="col-span-8 order-3 lg:order-none">
|
|
||||||
{(q || activeCategory || activeBrand) && (
|
|
||||||
<div className="mb-12 transition ease-in duration-75">
|
|
||||||
{data ? (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className={cn('animated', {
|
|
||||||
fadeIn: data.found,
|
|
||||||
hidden: !data.found,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Showing {data.products.length} results{' '}
|
|
||||||
{q && (
|
|
||||||
<>
|
|
||||||
for "<strong>{q}</strong>"
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={cn('animated', {
|
|
||||||
fadeIn: !data.found,
|
|
||||||
hidden: data.found,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{q ? (
|
|
||||||
<>
|
|
||||||
There are no products that match "<strong>{q}</strong>"
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
There are no products that match the selected category.
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : q ? (
|
|
||||||
<>
|
|
||||||
Searching for: "<strong>{q}</strong>"
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>Searching...</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{data ? (
|
|
||||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{data.products.map((product: Product) => (
|
|
||||||
<ProductCard
|
|
||||||
variant="simple"
|
|
||||||
key={product.path}
|
|
||||||
className="animated fadeIn"
|
|
||||||
product={product}
|
|
||||||
imgProps={{
|
|
||||||
width: 480,
|
|
||||||
height: 480,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{rangeMap(12, (i) => (
|
|
||||||
<Skeleton key={i}>
|
|
||||||
<div className="w-60 h-60" />
|
|
||||||
</Skeleton>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}{' '}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sort */}
|
|
||||||
<div className="col-span-8 lg:col-span-2 order-2 lg:order-none">
|
|
||||||
<div className="relative inline-block w-full">
|
|
||||||
<div className="lg:hidden">
|
|
||||||
<span className="rounded-md shadow-sm">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => handleClick(e, 'sort')}
|
|
||||||
className="flex justify-between w-full rounded-sm border border-accent-3 px-4 py-3 bg-accent-0 text-sm leading-5 font-medium text-accent-4 hover:text-accent-5 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-accent-1 active:text-accent-8 transition ease-in-out duration-150"
|
|
||||||
id="options-menu"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded="true"
|
|
||||||
>
|
|
||||||
{sort ? SORT[sort as keyof typeof SORT] : 'Relevance'}
|
|
||||||
<svg
|
|
||||||
className="-mr-1 ml-2 h-5 w-5"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`origin-top-left absolute lg:relative left-0 mt-2 w-full rounded-md shadow-lg lg:shadow-none z-10 mb-10 lg:block ${
|
|
||||||
activeFilter !== 'sort' || toggleFilter !== true ? 'hidden' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="rounded-sm bg-accent-0 shadow-xs lg:bg-none lg:shadow-none">
|
|
||||||
<div
|
|
||||||
role="menu"
|
|
||||||
aria-orientation="vertical"
|
|
||||||
aria-labelledby="options-menu"
|
|
||||||
>
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
className={cn(
|
|
||||||
'block text-sm leading-5 text-accent-4 lg:text-base lg:no-underline lg:font-bold lg:tracking-wide hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
|
|
||||||
{
|
|
||||||
underline: !sort,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Link href={{ pathname, query: filterQuery({ q }) }}>
|
|
||||||
<a
|
|
||||||
onClick={(e) => handleClick(e, 'sort')}
|
|
||||||
className={
|
|
||||||
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Relevance
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{Object.entries(SORT).map(([key, text]) => (
|
|
||||||
<li
|
|
||||||
key={key}
|
|
||||||
className={cn(
|
|
||||||
'block text-sm leading-5 text-accent-4 hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
|
|
||||||
{
|
|
||||||
underline: sort === key,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={{
|
|
||||||
pathname,
|
|
||||||
query: filterQuery({ q, sort: key }),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
onClick={(e) => handleClick(e, 'sort')}
|
|
||||||
className={
|
|
||||||
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Search.Layout = Layout
|
|
@ -27,15 +27,13 @@ const Modal: FC<ModalProps> = ({ children, onClose }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const modal = ref.current
|
if (ref.current) {
|
||||||
|
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
|
||||||
if (modal) {
|
|
||||||
disableBodyScroll(modal, { reserveScrollBarGap: true })
|
|
||||||
window.addEventListener('keydown', handleKey)
|
window.addEventListener('keydown', handleKey)
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (modal) {
|
if (ref && ref.current) {
|
||||||
enableBodyScroll(modal)
|
enableBodyScroll(ref.current)
|
||||||
}
|
}
|
||||||
clearAllBodyScrollLocks()
|
clearAllBodyScrollLocks()
|
||||||
window.removeEventListener('keydown', handleKey)
|
window.removeEventListener('keydown', handleKey)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, memo } from 'react'
|
import React, { FC } from 'react'
|
||||||
import rangeMap from '@lib/range-map'
|
import rangeMap from '@lib/range-map'
|
||||||
import { Star } from '@components/icons'
|
import { Star } from '@components/icons'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
@ -7,7 +7,8 @@ export interface RatingProps {
|
|||||||
value: number
|
value: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const Quantity: FC<RatingProps> = ({ value = 5 }) => (
|
const Quantity: React.FC<RatingProps> = React.memo(({ value = 5 }) => {
|
||||||
|
return (
|
||||||
<div className="flex flex-row py-6 text-accent-9">
|
<div className="flex flex-row py-6 text-accent-9">
|
||||||
{rangeMap(5, (i) => (
|
{rangeMap(5, (i) => (
|
||||||
<span
|
<span
|
||||||
@ -20,6 +21,7 @@ const Quantity: FC<RatingProps> = ({ value = 5 }) => (
|
|||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
|
||||||
export default memo(Quantity)
|
export default Quantity
|
||||||
|
@ -16,14 +16,13 @@ const Sidebar: FC<SidebarProps> = ({ children, onClose }) => {
|
|||||||
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sidebar = ref.current
|
if (ref.current) {
|
||||||
|
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
|
||||||
if (sidebar) {
|
|
||||||
disableBodyScroll(sidebar, { reserveScrollBarGap: true })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (sidebar) enableBodyScroll(sidebar)
|
if (ref && ref.current) {
|
||||||
|
enableBodyScroll(ref.current)
|
||||||
|
}
|
||||||
clearAllBodyScrollLocks()
|
clearAllBodyScrollLocks()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -15,7 +15,7 @@ const initialState = {
|
|||||||
displayDropdown: false,
|
displayDropdown: false,
|
||||||
displayModal: false,
|
displayModal: false,
|
||||||
modalView: 'LOGIN_VIEW',
|
modalView: 'LOGIN_VIEW',
|
||||||
sidebarView: 'CART_VIEW',
|
sidebarView: 'CHECKOUT_VIEW',
|
||||||
userAvatar: '',
|
userAvatar: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
.root {
|
|
||||||
@apply grid grid-cols-12 w-full gap-6 px-3 py-6 border-b border-accent-2 transition duration-100 ease-in-out;
|
|
||||||
|
|
||||||
&:nth-child(3n + 1) {
|
|
||||||
& .productBg {
|
|
||||||
@apply bg-violet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(3n + 2) {
|
|
||||||
& .productBg {
|
|
||||||
@apply bg-pink;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(3n + 3) {
|
|
||||||
& .productBg {
|
|
||||||
@apply bg-blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
import { FC, useState } from 'react'
|
|
||||||
import cn from 'classnames'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import s from './WishlistCard.module.css'
|
|
||||||
import { Trash } from '@components/icons'
|
|
||||||
import { Button, Text } from '@components/ui'
|
|
||||||
|
|
||||||
import { useUI } from '@components/ui/context'
|
|
||||||
import type { Product } from '@commerce/types/product'
|
|
||||||
import usePrice from '@framework/product/use-price'
|
|
||||||
import useAddItem from '@framework/cart/use-add-item'
|
|
||||||
import useRemoveItem from '@framework/wishlist/use-remove-item'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
product: Product
|
|
||||||
}
|
|
||||||
|
|
||||||
const placeholderImg = '/product-img-placeholder.svg'
|
|
||||||
|
|
||||||
const WishlistCard: FC<Props> = ({ product }) => {
|
|
||||||
const { price } = usePrice({
|
|
||||||
amount: product.price?.value,
|
|
||||||
baseAmount: product.price?.retailPrice,
|
|
||||||
currencyCode: product.price?.currencyCode!,
|
|
||||||
})
|
|
||||||
// @ts-ignore Wishlist is not always enabled
|
|
||||||
const removeItem = useRemoveItem({ wishlist: { includeProducts: true } })
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [removing, setRemoving] = useState(false)
|
|
||||||
|
|
||||||
// TODO: fix this missing argument issue
|
|
||||||
/* @ts-ignore */
|
|
||||||
const addItem = useAddItem()
|
|
||||||
const { openSidebar } = useUI()
|
|
||||||
|
|
||||||
const handleRemove = async () => {
|
|
||||||
setRemoving(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// If this action succeeds then there's no need to do `setRemoving(true)`
|
|
||||||
// because the component will be removed from the view
|
|
||||||
await removeItem({ id: product.id! })
|
|
||||||
} catch (error) {
|
|
||||||
setRemoving(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const addToCart = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
await addItem({
|
|
||||||
productId: String(product.id),
|
|
||||||
variantId: String(product.variants[0].id),
|
|
||||||
})
|
|
||||||
openSidebar()
|
|
||||||
setLoading(false)
|
|
||||||
} catch (err) {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn(s.root, { 'opacity-75 pointer-events-none': removing })}>
|
|
||||||
<div className={`col-span-3 ${s.productBg}`}>
|
|
||||||
<Image
|
|
||||||
src={product.images[0]?.url || placeholderImg}
|
|
||||||
width={400}
|
|
||||||
height={400}
|
|
||||||
alt={product.images[0]?.alt || 'Product Image'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-7">
|
|
||||||
<h3 className="text-2xl mb-2">
|
|
||||||
<Link href={`/product${product.path}`}>
|
|
||||||
<a>{product.name}</a>
|
|
||||||
</Link>
|
|
||||||
</h3>
|
|
||||||
<div className="mb-4">
|
|
||||||
<Text html={product.description} />
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
aria-label="Add to Cart"
|
|
||||||
type="button"
|
|
||||||
className={
|
|
||||||
'py-1 px-3 border border-secondary rounded-md shadow-sm hover:bg-primary-hover'
|
|
||||||
}
|
|
||||||
onClick={addToCart}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
Add to Cart
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2 flex flex-col justify-between">
|
|
||||||
<div className="flex justify-end font-bold">{price}</div>
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<button onClick={handleRemove}>
|
|
||||||
<Trash />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WishlistCard
|
|
@ -1 +0,0 @@
|
|||||||
export { default } from './WishlistCard'
|
|
@ -1,2 +1 @@
|
|||||||
export { default as WishlistCard } from './WishlistCard'
|
|
||||||
export { default as WishlistButton } from './WishlistButton'
|
export { default as WishlistButton } from './WishlistButton'
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
COMMERCE_PROVIDER=bigcommerce
|
|
||||||
|
|
||||||
BIGCOMMERCE_STOREFRONT_API_URL=
|
|
||||||
BIGCOMMERCE_STOREFRONT_API_TOKEN=
|
|
||||||
BIGCOMMERCE_STORE_API_URL=
|
|
||||||
BIGCOMMERCE_STORE_API_TOKEN=
|
|
||||||
BIGCOMMERCE_STORE_API_CLIENT_ID=
|
|
||||||
BIGCOMMERCE_CHANNEL_ID=
|
|
@ -1,59 +0,0 @@
|
|||||||
# Bigcommerce Provider
|
|
||||||
|
|
||||||
**Demo:** https://bigcommerce.demo.vercel.store/
|
|
||||||
|
|
||||||
With the deploy button below you'll be able to have a [BigCommerce](https://www.bigcommerce.com/) account and a store that works with this starter:
|
|
||||||
|
|
||||||
[](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fcommerce&project-name=commerce&repo-name=commerce&demo-title=Next.js%20Commerce&demo-description=An%20all-in-one%20starter%20kit%20for%20high-performance%20e-commerce%20sites.&demo-url=https%3A%2F%2Fdemo.vercel.store&demo-image=https%3A%2F%2Fbigcommerce-demo-asset-ksvtgfvnd.vercel.app%2Fbigcommerce.png&integration-ids=oac_MuWZiE4jtmQ2ejZQaQ7ncuDT)
|
|
||||||
|
|
||||||
If you already have a BigCommerce account and want to use your current store, then copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp framework/bigcommerce/.env.template .env.local
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, set the environment variables in `.env.local` to match the ones from your store.
|
|
||||||
|
|
||||||
## Contribute
|
|
||||||
|
|
||||||
Our commitment to Open Source can be found [here](https://vercel.com/oss).
|
|
||||||
|
|
||||||
If you find an issue with the provider or want a new feature, feel free to open a PR or [create a new issue](https://github.com/vercel/commerce/issues).
|
|
||||||
|
|
||||||
## Troubleshoot
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>I already own a BigCommerce store. What should I do?</summary>
|
|
||||||
<br>
|
|
||||||
First thing you do is: <b>set your environment variables</b>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
```sh
|
|
||||||
BIGCOMMERCE_STOREFRONT_API_URL=<>
|
|
||||||
BIGCOMMERCE_STOREFRONT_API_TOKEN=<>
|
|
||||||
BIGCOMMERCE_STORE_API_URL=<>
|
|
||||||
BIGCOMMERCE_STORE_API_TOKEN=<>
|
|
||||||
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
|
|
||||||
BIGCOMMERCE_CHANNEL_ID=<>
|
|
||||||
```
|
|
||||||
|
|
||||||
If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials.
|
|
||||||
|
|
||||||
1. Install Vercel CLI: `npm i -g vercel`
|
|
||||||
2. Link local instance with Vercel and Github accounts (creates .vercel file): `vercel link`
|
|
||||||
3. Download your environment variables: `vercel env pull .env.local`
|
|
||||||
|
|
||||||
Next, you're free to customize the starter. More updates coming soon. Stay tuned.
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>BigCommerce shows a Coming Soon page and requests a Preview Code</summary>
|
|
||||||
<br>
|
|
||||||
After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard.
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
BigCommerce team has been notified and they plan to add more detailed about this subject.
|
|
||||||
</details>
|
|
File diff suppressed because it is too large
Load Diff
@ -1,329 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file was auto-generated by swagger-to-ts.
|
|
||||||
* Do not make direct changes to the file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface definitions {
|
|
||||||
blogPost_Full: {
|
|
||||||
/**
|
|
||||||
* ID of this blog post. (READ-ONLY)
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
} & definitions['blogPost_Base']
|
|
||||||
addresses: {
|
|
||||||
/**
|
|
||||||
* Full URL of where the resource is located.
|
|
||||||
*/
|
|
||||||
url?: string
|
|
||||||
/**
|
|
||||||
* Resource being accessed.
|
|
||||||
*/
|
|
||||||
resource?: string
|
|
||||||
}
|
|
||||||
formField: {
|
|
||||||
/**
|
|
||||||
* Name of the form field
|
|
||||||
*/
|
|
||||||
name?: string
|
|
||||||
/**
|
|
||||||
* Value of the form field
|
|
||||||
*/
|
|
||||||
value?: string
|
|
||||||
}
|
|
||||||
page_Full: {
|
|
||||||
/**
|
|
||||||
* ID of the page.
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
} & definitions['page_Base']
|
|
||||||
redirect: {
|
|
||||||
/**
|
|
||||||
* Numeric ID of the redirect.
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
/**
|
|
||||||
* The path from which to redirect.
|
|
||||||
*/
|
|
||||||
path: string
|
|
||||||
forward: definitions['forward']
|
|
||||||
/**
|
|
||||||
* URL of the redirect. READ-ONLY
|
|
||||||
*/
|
|
||||||
url?: string
|
|
||||||
}
|
|
||||||
forward: {
|
|
||||||
/**
|
|
||||||
* The type of redirect. If it is a `manual` redirect then type will always be manual. Dynamic redirects will have the type of the page. Such as product or category.
|
|
||||||
*/
|
|
||||||
type?: string
|
|
||||||
/**
|
|
||||||
* Reference of the redirect. Dynamic redirects will have the category or product number. Manual redirects will have the url that is being directed to.
|
|
||||||
*/
|
|
||||||
ref?: number
|
|
||||||
}
|
|
||||||
customer_Full: {
|
|
||||||
/**
|
|
||||||
* Unique numeric ID of this customer. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
/**
|
|
||||||
* Not returned in any responses, but accepts up to two fields allowing you to set the customer’s password. If a password is not supplied, it is generated automatically. For further information about using this object, please see the Customers resource documentation.
|
|
||||||
*/
|
|
||||||
_authentication?: {
|
|
||||||
force_reset?: string
|
|
||||||
password?: string
|
|
||||||
password_confirmation?: string
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* The name of the company for which the customer works.
|
|
||||||
*/
|
|
||||||
company?: string
|
|
||||||
/**
|
|
||||||
* First name of the customer.
|
|
||||||
*/
|
|
||||||
first_name: string
|
|
||||||
/**
|
|
||||||
* Last name of the customer.
|
|
||||||
*/
|
|
||||||
last_name: string
|
|
||||||
/**
|
|
||||||
* Email address of the customer.
|
|
||||||
*/
|
|
||||||
email: string
|
|
||||||
/**
|
|
||||||
* Phone number of the customer.
|
|
||||||
*/
|
|
||||||
phone?: string
|
|
||||||
/**
|
|
||||||
* Date on which the customer registered from the storefront or was created in the control panel. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
|
|
||||||
*/
|
|
||||||
date_created?: string
|
|
||||||
/**
|
|
||||||
* Date on which the customer updated their details in the storefront or was updated in the control panel. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
|
|
||||||
*/
|
|
||||||
date_modified?: string
|
|
||||||
/**
|
|
||||||
* The amount of credit the customer has. (Float, Float as String, Integer)
|
|
||||||
*/
|
|
||||||
store_credit?: string
|
|
||||||
/**
|
|
||||||
* The customer’s IP address when they signed up.
|
|
||||||
*/
|
|
||||||
registration_ip_address?: string
|
|
||||||
/**
|
|
||||||
* The group to which the customer belongs.
|
|
||||||
*/
|
|
||||||
customer_group_id?: number
|
|
||||||
/**
|
|
||||||
* Store-owner notes on the customer.
|
|
||||||
*/
|
|
||||||
notes?: string
|
|
||||||
/**
|
|
||||||
* Used to identify customers who fall into special sales-tax categories – in particular, those who are fully or partially exempt from paying sales tax. Can be blank, or can contain a single AvaTax code. (The codes are case-sensitive.) Stores that subscribe to BigCommerce’s Avalara Premium integration will use this code to determine how/whether to apply sales tax. Does not affect sales-tax calculations for stores that do not subscribe to Avalara Premium.
|
|
||||||
*/
|
|
||||||
tax_exempt_category?: string
|
|
||||||
/**
|
|
||||||
* Records whether the customer would like to receive marketing content from this store. READ-ONLY.This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
|
|
||||||
*/
|
|
||||||
accepts_marketing?: boolean
|
|
||||||
addresses?: definitions['addresses']
|
|
||||||
/**
|
|
||||||
* Array of custom fields. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
|
|
||||||
*/
|
|
||||||
form_fields?: definitions['formField'][]
|
|
||||||
/**
|
|
||||||
* Force a password change on next login.
|
|
||||||
*/
|
|
||||||
reset_pass_on_login?: boolean
|
|
||||||
}
|
|
||||||
categoryAccessLevel: {
|
|
||||||
/**
|
|
||||||
* + `all` - Customers can access all categories
|
|
||||||
* + `specific` - Customers can access a specific list of categories
|
|
||||||
* + `none` - Customers are prevented from viewing any of the categories in this group.
|
|
||||||
*/
|
|
||||||
type?: 'all' | 'specific' | 'none'
|
|
||||||
/**
|
|
||||||
* Is an array of category IDs and should be supplied only if `type` is specific.
|
|
||||||
*/
|
|
||||||
categories?: string[]
|
|
||||||
}
|
|
||||||
timeZone: {
|
|
||||||
/**
|
|
||||||
* a string identifying the time zone, in the format: <Continent-name>/<City-name>.
|
|
||||||
*/
|
|
||||||
name?: string
|
|
||||||
/**
|
|
||||||
* a negative or positive number, identifying the offset from UTC/GMT, in seconds, during winter/standard time.
|
|
||||||
*/
|
|
||||||
raw_offset?: number
|
|
||||||
/**
|
|
||||||
* "-/+" offset from UTC/GMT, in seconds, during summer/daylight saving time.
|
|
||||||
*/
|
|
||||||
dst_offset?: number
|
|
||||||
/**
|
|
||||||
* a boolean indicating whether this time zone observes daylight saving time.
|
|
||||||
*/
|
|
||||||
dst_correction?: boolean
|
|
||||||
date_format?: definitions['dateFormat']
|
|
||||||
}
|
|
||||||
count_Response: { count?: number }
|
|
||||||
dateFormat: {
|
|
||||||
/**
|
|
||||||
* string that defines dates’ display format, in the pattern: M jS Y
|
|
||||||
*/
|
|
||||||
display?: string
|
|
||||||
/**
|
|
||||||
* string that defines the CSV export format for orders, customers, and products, in the pattern: M jS Y
|
|
||||||
*/
|
|
||||||
export?: string
|
|
||||||
/**
|
|
||||||
* string that defines dates’ extended-display format, in the pattern: M jS Y @ g:i A.
|
|
||||||
*/
|
|
||||||
extended_display?: string
|
|
||||||
}
|
|
||||||
blogTags: { tag?: string; post_ids?: number[] }[]
|
|
||||||
blogPost_Base: {
|
|
||||||
/**
|
|
||||||
* Title of this blog post.
|
|
||||||
*/
|
|
||||||
title: string
|
|
||||||
/**
|
|
||||||
* URL for the public blog post.
|
|
||||||
*/
|
|
||||||
url?: string
|
|
||||||
/**
|
|
||||||
* URL to preview the blog post. (READ-ONLY)
|
|
||||||
*/
|
|
||||||
preview_url?: string
|
|
||||||
/**
|
|
||||||
* Text body of the blog post.
|
|
||||||
*/
|
|
||||||
body: string
|
|
||||||
/**
|
|
||||||
* Tags to characterize the blog post.
|
|
||||||
*/
|
|
||||||
tags?: string[]
|
|
||||||
/**
|
|
||||||
* Summary of the blog post. (READ-ONLY)
|
|
||||||
*/
|
|
||||||
summary?: string
|
|
||||||
/**
|
|
||||||
* Whether the blog post is published.
|
|
||||||
*/
|
|
||||||
is_published?: boolean
|
|
||||||
published_date?: definitions['publishedDate']
|
|
||||||
/**
|
|
||||||
* Published date in `ISO 8601` format.
|
|
||||||
*/
|
|
||||||
published_date_iso8601?: string
|
|
||||||
/**
|
|
||||||
* Description text for this blog post’s `<meta/>` element.
|
|
||||||
*/
|
|
||||||
meta_description?: string
|
|
||||||
/**
|
|
||||||
* Keywords for this blog post’s `<meta/>` element.
|
|
||||||
*/
|
|
||||||
meta_keywords?: string
|
|
||||||
/**
|
|
||||||
* Name of the blog post’s author.
|
|
||||||
*/
|
|
||||||
author?: string
|
|
||||||
/**
|
|
||||||
* Local path to a thumbnail uploaded to `product_images/` via [WebDav](https://support.bigcommerce.com/s/article/File-Access-WebDAV).
|
|
||||||
*/
|
|
||||||
thumbnail_path?: string
|
|
||||||
}
|
|
||||||
publishedDate: { timezone_type?: string; date?: string; timezone?: string }
|
|
||||||
/**
|
|
||||||
* Not returned in any responses, but accepts up to two fields allowing you to set the customer’s password. If a password is not supplied, it is generated automatically. For further information about using this object, please see the Customers resource documentation.
|
|
||||||
*/
|
|
||||||
authentication: {
|
|
||||||
force_reset?: string
|
|
||||||
password?: string
|
|
||||||
password_confirmation?: string
|
|
||||||
}
|
|
||||||
customer_Base: { [key: string]: any }
|
|
||||||
page_Base: {
|
|
||||||
/**
|
|
||||||
* ID of any parent Web page.
|
|
||||||
*/
|
|
||||||
parent_id?: number
|
|
||||||
/**
|
|
||||||
* `page`: free-text page
|
|
||||||
* `link`: link to another web address
|
|
||||||
* `rss_feed`: syndicated content from an RSS feed
|
|
||||||
* `contact_form`: When the store's contact form is used.
|
|
||||||
*/
|
|
||||||
type: 'page' | 'rss_feed' | 'contact_form' | 'raw' | 'link'
|
|
||||||
/**
|
|
||||||
* Where the page’s type is a contact form: object whose members are the fields enabled (in the control panel) for storefront display. Possible members are:`fullname`: full name of the customer submitting the form; `phone`: customer’s phone number, as submitted on the form; `companyname`: customer’s submitted company name; `orderno`: customer’s submitted order number; `rma`: customer’s submitted RMA (Return Merchandise Authorization) number.
|
|
||||||
*/
|
|
||||||
contact_fields?: string
|
|
||||||
/**
|
|
||||||
* Where the page’s type is a contact form: email address that receives messages sent via the form.
|
|
||||||
*/
|
|
||||||
email?: string
|
|
||||||
/**
|
|
||||||
* Page name, as displayed on the storefront.
|
|
||||||
*/
|
|
||||||
name: string
|
|
||||||
/**
|
|
||||||
* Relative URL on the storefront for this page.
|
|
||||||
*/
|
|
||||||
url?: string
|
|
||||||
/**
|
|
||||||
* Description contained within this page’s `<meta/>` element.
|
|
||||||
*/
|
|
||||||
meta_description?: string
|
|
||||||
/**
|
|
||||||
* HTML or variable that populates this page’s `<body>` element, in default/desktop view. Required in POST if page type is `raw`.
|
|
||||||
*/
|
|
||||||
body: string
|
|
||||||
/**
|
|
||||||
* HTML to use for this page's body when viewed in the mobile template (deprecated).
|
|
||||||
*/
|
|
||||||
mobile_body?: string
|
|
||||||
/**
|
|
||||||
* If true, this page has a mobile version.
|
|
||||||
*/
|
|
||||||
has_mobile_version?: boolean
|
|
||||||
/**
|
|
||||||
* If true, this page appears in the storefront’s navigation menu.
|
|
||||||
*/
|
|
||||||
is_visible?: boolean
|
|
||||||
/**
|
|
||||||
* If true, this page is the storefront’s home page.
|
|
||||||
*/
|
|
||||||
is_homepage?: boolean
|
|
||||||
/**
|
|
||||||
* Text specified for this page’s `<title>` element. (If empty, the value of the name property is used.)
|
|
||||||
*/
|
|
||||||
meta_title?: string
|
|
||||||
/**
|
|
||||||
* Layout template for this page. This field is writable only for stores with a Blueprint theme applied.
|
|
||||||
*/
|
|
||||||
layout_file?: string
|
|
||||||
/**
|
|
||||||
* Order in which this page should display on the storefront. (Lower integers specify earlier display.)
|
|
||||||
*/
|
|
||||||
sort_order?: number
|
|
||||||
/**
|
|
||||||
* Comma-separated list of keywords that shoppers can use to locate this page when searching the store.
|
|
||||||
*/
|
|
||||||
search_keywords?: string
|
|
||||||
/**
|
|
||||||
* Comma-separated list of SEO-relevant keywords to include in the page’s `<meta/>` element.
|
|
||||||
*/
|
|
||||||
meta_keywords?: string
|
|
||||||
/**
|
|
||||||
* If page type is `rss_feed` the n this field is visisble. Required in POST required for `rss page` type.
|
|
||||||
*/
|
|
||||||
feed: string
|
|
||||||
/**
|
|
||||||
* If page type is `link` this field is returned. Required in POST to create a `link` page.
|
|
||||||
*/
|
|
||||||
link: string
|
|
||||||
content_type?: 'application/json' | 'text/javascript' | 'text/html'
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,142 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file was auto-generated by swagger-to-ts.
|
|
||||||
* Do not make direct changes to the file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface definitions {
|
|
||||||
wishlist_Post: {
|
|
||||||
/**
|
|
||||||
* The customer id.
|
|
||||||
*/
|
|
||||||
customer_id: number
|
|
||||||
/**
|
|
||||||
* Whether the wishlist is available to the public.
|
|
||||||
*/
|
|
||||||
is_public?: boolean
|
|
||||||
/**
|
|
||||||
* The title of the wishlist.
|
|
||||||
*/
|
|
||||||
name?: string
|
|
||||||
/**
|
|
||||||
* Array of Wishlist items.
|
|
||||||
*/
|
|
||||||
items?: {
|
|
||||||
/**
|
|
||||||
* The ID of the product.
|
|
||||||
*/
|
|
||||||
product_id?: number
|
|
||||||
/**
|
|
||||||
* The variant ID of the product.
|
|
||||||
*/
|
|
||||||
variant_id?: number
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
wishlist_Put: {
|
|
||||||
/**
|
|
||||||
* The customer id.
|
|
||||||
*/
|
|
||||||
customer_id: number
|
|
||||||
/**
|
|
||||||
* Whether the wishlist is available to the public.
|
|
||||||
*/
|
|
||||||
is_public?: boolean
|
|
||||||
/**
|
|
||||||
* The title of the wishlist.
|
|
||||||
*/
|
|
||||||
name?: string
|
|
||||||
/**
|
|
||||||
* Array of Wishlist items.
|
|
||||||
*/
|
|
||||||
items?: {
|
|
||||||
/**
|
|
||||||
* The ID of the item
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
/**
|
|
||||||
* The ID of the product.
|
|
||||||
*/
|
|
||||||
product_id?: number
|
|
||||||
/**
|
|
||||||
* The variant ID of the item.
|
|
||||||
*/
|
|
||||||
variant_id?: number
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
wishlist_Full: {
|
|
||||||
/**
|
|
||||||
* Wishlist ID, provided after creating a wishlist with a POST.
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
/**
|
|
||||||
* The ID the customer to which the wishlist belongs.
|
|
||||||
*/
|
|
||||||
customer_id?: number
|
|
||||||
/**
|
|
||||||
* The Wishlist's name.
|
|
||||||
*/
|
|
||||||
name?: string
|
|
||||||
/**
|
|
||||||
* Whether the Wishlist is available to the public.
|
|
||||||
*/
|
|
||||||
is_public?: boolean
|
|
||||||
/**
|
|
||||||
* The token of the Wishlist. This is created internally within BigCommerce. The Wishlist ID is to be used for external apps. Read-Only
|
|
||||||
*/
|
|
||||||
token?: string
|
|
||||||
/**
|
|
||||||
* Array of Wishlist items
|
|
||||||
*/
|
|
||||||
items?: definitions['wishlistItem_Full'][]
|
|
||||||
}
|
|
||||||
wishlistItem_Full: {
|
|
||||||
/**
|
|
||||||
* The ID of the item
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
/**
|
|
||||||
* The ID of the product.
|
|
||||||
*/
|
|
||||||
product_id?: number
|
|
||||||
/**
|
|
||||||
* The variant ID of the item.
|
|
||||||
*/
|
|
||||||
variant_id?: number
|
|
||||||
}
|
|
||||||
wishlistItem_Post: {
|
|
||||||
/**
|
|
||||||
* The ID of the product.
|
|
||||||
*/
|
|
||||||
product_id?: number
|
|
||||||
/**
|
|
||||||
* The variant ID of the product.
|
|
||||||
*/
|
|
||||||
variant_id?: number
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Data about the response, including pagination and collection totals.
|
|
||||||
*/
|
|
||||||
pagination: {
|
|
||||||
/**
|
|
||||||
* Total number of items in the result set.
|
|
||||||
*/
|
|
||||||
total?: number
|
|
||||||
/**
|
|
||||||
* Total number of items in the collection response.
|
|
||||||
*/
|
|
||||||
count?: number
|
|
||||||
/**
|
|
||||||
* The amount of items returned in the collection per page, controlled by the limit parameter.
|
|
||||||
*/
|
|
||||||
per_page?: number
|
|
||||||
/**
|
|
||||||
* The page you are currently on within the collection.
|
|
||||||
*/
|
|
||||||
current_page?: number
|
|
||||||
/**
|
|
||||||
* The total number of pages in the collection.
|
|
||||||
*/
|
|
||||||
total_pages?: number
|
|
||||||
}
|
|
||||||
error: { status?: number; title?: string; type?: string }
|
|
||||||
metaCollection: { pagination?: definitions['pagination'] }
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
import { normalizeCart } from '../../../lib/normalize'
|
|
||||||
import { parseCartItem } from '../../utils/parse-item'
|
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
|
||||||
import type { CartEndpoint } from '.'
|
|
||||||
|
|
||||||
const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
|
||||||
res,
|
|
||||||
body: { cartId, item },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
if (!item) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Missing item' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!item.quantity) item.quantity = 1
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
line_items: [parseCartItem(item)],
|
|
||||||
...(!cartId && config.storeChannelId
|
|
||||||
? { channel_id: config.storeChannelId }
|
|
||||||
: {}),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
const { data } = cartId
|
|
||||||
? await config.storeApiFetch(
|
|
||||||
`/v3/carts/${cartId}/items?include=line_items.physical_items.options`,
|
|
||||||
options
|
|
||||||
)
|
|
||||||
: await config.storeApiFetch(
|
|
||||||
'/v3/carts?include=line_items.physical_items.options',
|
|
||||||
options
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create or update the cart cookie
|
|
||||||
res.setHeader(
|
|
||||||
'Set-Cookie',
|
|
||||||
getCartCookie(config.cartCookie, data.id, config.cartCookieMaxAge)
|
|
||||||
)
|
|
||||||
res.status(200).json({ data: normalizeCart(data) })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default addItem
|
|
@ -1,35 +0,0 @@
|
|||||||
import { normalizeCart } from '../../../lib/normalize'
|
|
||||||
import { BigcommerceApiError } from '../../utils/errors'
|
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
|
||||||
import type { BigcommerceCart } from '../../../types/cart'
|
|
||||||
import type { CartEndpoint } from '.'
|
|
||||||
|
|
||||||
// Return current cart info
|
|
||||||
const getCart: CartEndpoint['handlers']['getCart'] = async ({
|
|
||||||
res,
|
|
||||||
body: { cartId },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
let result: { data?: BigcommerceCart } = {}
|
|
||||||
|
|
||||||
if (cartId) {
|
|
||||||
try {
|
|
||||||
result = await config.storeApiFetch(
|
|
||||||
`/v3/carts/${cartId}?include=line_items.physical_items.options`
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof BigcommerceApiError && error.status === 404) {
|
|
||||||
// Remove the cookie if it exists but the cart wasn't found
|
|
||||||
res.setHeader('Set-Cookie', getCartCookie(config.cartCookie))
|
|
||||||
} else {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
data: result.data ? normalizeCart(result.data) : null,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getCart
|
|
@ -1,26 +0,0 @@
|
|||||||
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
|
||||||
import cartEndpoint from '@commerce/api/endpoints/cart'
|
|
||||||
import type { CartSchema } from '../../../types/cart'
|
|
||||||
import type { BigcommerceAPI } from '../..'
|
|
||||||
import getCart from './get-cart'
|
|
||||||
import addItem from './add-item'
|
|
||||||
import updateItem from './update-item'
|
|
||||||
import removeItem from './remove-item'
|
|
||||||
|
|
||||||
export type CartAPI = GetAPISchema<BigcommerceAPI, CartSchema>
|
|
||||||
|
|
||||||
export type CartEndpoint = CartAPI['endpoint']
|
|
||||||
|
|
||||||
export const handlers: CartEndpoint['handlers'] = {
|
|
||||||
getCart,
|
|
||||||
addItem,
|
|
||||||
updateItem,
|
|
||||||
removeItem,
|
|
||||||
}
|
|
||||||
|
|
||||||
const cartApi = createEndpoint<CartAPI>({
|
|
||||||
handler: cartEndpoint,
|
|
||||||
handlers,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default cartApi
|
|
@ -1,34 +0,0 @@
|
|||||||
import { normalizeCart } from '../../../lib/normalize'
|
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
|
||||||
import type { CartEndpoint } from '.'
|
|
||||||
|
|
||||||
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
|
||||||
res,
|
|
||||||
body: { cartId, itemId },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
if (!cartId || !itemId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await config.storeApiFetch<{ data: any } | null>(
|
|
||||||
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
|
|
||||||
{ method: 'DELETE' }
|
|
||||||
)
|
|
||||||
const data = result?.data ?? null
|
|
||||||
|
|
||||||
res.setHeader(
|
|
||||||
'Set-Cookie',
|
|
||||||
data
|
|
||||||
? // Update the cart cookie
|
|
||||||
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
|
||||||
: // Remove the cart cookie if the cart was removed (empty items)
|
|
||||||
getCartCookie(config.cartCookie)
|
|
||||||
)
|
|
||||||
res.status(200).json({ data: data && normalizeCart(data) })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default removeItem
|
|
@ -1,36 +0,0 @@
|
|||||||
import { normalizeCart } from '../../../lib/normalize'
|
|
||||||
import { parseCartItem } from '../../utils/parse-item'
|
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
|
||||||
import type { CartEndpoint } from '.'
|
|
||||||
|
|
||||||
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
|
||||||
res,
|
|
||||||
body: { cartId, itemId, item },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
if (!cartId || !itemId || !item) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await config.storeApiFetch(
|
|
||||||
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
|
|
||||||
{
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify({
|
|
||||||
line_item: parseCartItem(item),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Update the cart cookie
|
|
||||||
res.setHeader(
|
|
||||||
'Set-Cookie',
|
|
||||||
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
|
||||||
)
|
|
||||||
res.status(200).json({ data: normalizeCart(data) })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default updateItem
|
|
@ -1,79 +0,0 @@
|
|||||||
import { Product } from '@commerce/types/product'
|
|
||||||
import { ProductsEndpoint } from '.'
|
|
||||||
|
|
||||||
const SORT: { [key: string]: string | undefined } = {
|
|
||||||
latest: 'id',
|
|
||||||
trending: 'total_sold',
|
|
||||||
price: 'price',
|
|
||||||
}
|
|
||||||
|
|
||||||
const LIMIT = 12
|
|
||||||
|
|
||||||
// Return current cart info
|
|
||||||
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
|
||||||
res,
|
|
||||||
body: { search, categoryId, brandId, sort },
|
|
||||||
config,
|
|
||||||
commerce,
|
|
||||||
}) => {
|
|
||||||
// Use a dummy base as we only care about the relative path
|
|
||||||
const url = new URL('/v3/catalog/products', 'http://a')
|
|
||||||
|
|
||||||
url.searchParams.set('is_visible', 'true')
|
|
||||||
url.searchParams.set('limit', String(LIMIT))
|
|
||||||
|
|
||||||
if (search) url.searchParams.set('keyword', search)
|
|
||||||
|
|
||||||
if (categoryId && Number.isInteger(Number(categoryId)))
|
|
||||||
url.searchParams.set('categories:in', String(categoryId))
|
|
||||||
|
|
||||||
if (brandId && Number.isInteger(Number(brandId)))
|
|
||||||
url.searchParams.set('brand_id', String(brandId))
|
|
||||||
|
|
||||||
if (sort) {
|
|
||||||
const [_sort, direction] = sort.split('-')
|
|
||||||
const sortValue = SORT[_sort]
|
|
||||||
|
|
||||||
if (sortValue && direction) {
|
|
||||||
url.searchParams.set('sort', sortValue)
|
|
||||||
url.searchParams.set('direction', direction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only want the id of each product
|
|
||||||
url.searchParams.set('include_fields', 'id')
|
|
||||||
|
|
||||||
const { data } = await config.storeApiFetch<{ data: { id: number }[] }>(
|
|
||||||
url.pathname + url.search
|
|
||||||
)
|
|
||||||
|
|
||||||
const ids = data.map((p) => String(p.id))
|
|
||||||
const found = ids.length > 0
|
|
||||||
|
|
||||||
// We want the GraphQL version of each product
|
|
||||||
const graphqlData = await commerce.getAllProducts({
|
|
||||||
variables: { first: LIMIT, ids },
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Put the products in an object that we can use to get them by id
|
|
||||||
const productsById = graphqlData.products.reduce<{
|
|
||||||
[k: string]: Product
|
|
||||||
}>((prods, p) => {
|
|
||||||
prods[Number(p.id)] = p
|
|
||||||
return prods
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
const products: Product[] = found ? [] : graphqlData.products
|
|
||||||
|
|
||||||
// Populate the products array with the graphql products, in the order
|
|
||||||
// assigned by the list of entity ids
|
|
||||||
ids.forEach((id) => {
|
|
||||||
const product = productsById[id]
|
|
||||||
if (product) products.push(product)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.status(200).json({ data: { products, found } })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getProducts
|
|
@ -1,18 +0,0 @@
|
|||||||
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
|
||||||
import productsEndpoint from '@commerce/api/endpoints/catalog/products'
|
|
||||||
import type { ProductsSchema } from '../../../../types/product'
|
|
||||||
import type { BigcommerceAPI } from '../../..'
|
|
||||||
import getProducts from './get-products'
|
|
||||||
|
|
||||||
export type ProductsAPI = GetAPISchema<BigcommerceAPI, ProductsSchema>
|
|
||||||
|
|
||||||
export type ProductsEndpoint = ProductsAPI['endpoint']
|
|
||||||
|
|
||||||
export const handlers: ProductsEndpoint['handlers'] = { getProducts }
|
|
||||||
|
|
||||||
const productsApi = createEndpoint<ProductsAPI>({
|
|
||||||
handler: productsEndpoint,
|
|
||||||
handlers,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default productsApi
|
|
@ -1,90 +0,0 @@
|
|||||||
import type { CheckoutEndpoint } from '.'
|
|
||||||
import getCustomerId from '../../utils/get-customer-id'
|
|
||||||
import jwt from 'jsonwebtoken'
|
|
||||||
import { uuid } from 'uuidv4'
|
|
||||||
|
|
||||||
const fullCheckout = true
|
|
||||||
|
|
||||||
const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
const { cookies } = req
|
|
||||||
const cartId = cookies[config.cartCookie]
|
|
||||||
const customerToken = cookies[config.customerCookie]
|
|
||||||
if (!cartId) {
|
|
||||||
res.redirect('/cart')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const { data } = await config.storeApiFetch(
|
|
||||||
`/v3/carts/${cartId}/redirect_urls`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const customerId =
|
|
||||||
customerToken && (await getCustomerId({ customerToken, config }))
|
|
||||||
|
|
||||||
//if there is a customer create a jwt token
|
|
||||||
if (!customerId) {
|
|
||||||
if (fullCheckout) {
|
|
||||||
res.redirect(data.checkout_url)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const dateCreated = Math.round(new Date().getTime() / 1000)
|
|
||||||
const payload = {
|
|
||||||
iss: config.storeApiClientId,
|
|
||||||
iat: dateCreated,
|
|
||||||
jti: uuid(),
|
|
||||||
operation: 'customer_login',
|
|
||||||
store_hash: config.storeHash,
|
|
||||||
customer_id: customerId,
|
|
||||||
channel_id: config.storeChannelId,
|
|
||||||
redirect_to: data.checkout_url,
|
|
||||||
}
|
|
||||||
let token = jwt.sign(payload, config.storeApiClientSecret!, {
|
|
||||||
algorithm: 'HS256',
|
|
||||||
})
|
|
||||||
let checkouturl = `${config.storeUrl}/login/token/${token}`
|
|
||||||
console.log('checkouturl', checkouturl)
|
|
||||||
if (fullCheckout) {
|
|
||||||
res.redirect(checkouturl)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make the embedded checkout work too!
|
|
||||||
const html = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Checkout</title>
|
|
||||||
<script src="https://checkout-sdk.bigcommerce.com/v1/loader.js"></script>
|
|
||||||
<script>
|
|
||||||
window.onload = function() {
|
|
||||||
checkoutKitLoader.load('checkout-sdk').then(function (service) {
|
|
||||||
service.embedCheckout({
|
|
||||||
containerId: 'checkout',
|
|
||||||
url: '${data.embedded_checkout_url}'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="checkout"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
res.setHeader('Content-Type', 'text/html')
|
|
||||||
res.write(html)
|
|
||||||
res.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default checkout
|
|
@ -1,18 +0,0 @@
|
|||||||
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
|
||||||
import checkoutEndpoint from '@commerce/api/endpoints/checkout'
|
|
||||||
import type { CheckoutSchema } from '../../../types/checkout'
|
|
||||||
import type { BigcommerceAPI } from '../..'
|
|
||||||
import checkout from './checkout'
|
|
||||||
|
|
||||||
export type CheckoutAPI = GetAPISchema<BigcommerceAPI, CheckoutSchema>
|
|
||||||
|
|
||||||
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
|
||||||
|
|
||||||
export const handlers: CheckoutEndpoint['handlers'] = { checkout }
|
|
||||||
|
|
||||||
const checkoutApi = createEndpoint<CheckoutAPI>({
|
|
||||||
handler: checkoutEndpoint,
|
|
||||||
handlers,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default checkoutApi
|
|
@ -1,59 +0,0 @@
|
|||||||
import type { GetLoggedInCustomerQuery } from '../../../schema'
|
|
||||||
import type { CustomerEndpoint } from '.'
|
|
||||||
|
|
||||||
export const getLoggedInCustomerQuery = /* GraphQL */ `
|
|
||||||
query getLoggedInCustomer {
|
|
||||||
customer {
|
|
||||||
entityId
|
|
||||||
firstName
|
|
||||||
lastName
|
|
||||||
email
|
|
||||||
company
|
|
||||||
customerGroupId
|
|
||||||
notes
|
|
||||||
phone
|
|
||||||
addressCount
|
|
||||||
attributeCount
|
|
||||||
storeCredit {
|
|
||||||
value
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export type Customer = NonNullable<GetLoggedInCustomerQuery['customer']>
|
|
||||||
|
|
||||||
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] = async ({
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
const token = req.cookies[config.customerCookie]
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
const { data } = await config.fetch<GetLoggedInCustomerQuery>(
|
|
||||||
getLoggedInCustomerQuery,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
cookie: `${config.customerCookie}=${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const { customer } = data
|
|
||||||
|
|
||||||
if (!customer) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Customer not found', code: 'not_found' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json({ data: { customer } })
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({ data: null })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getLoggedInCustomer
|
|
@ -1,18 +0,0 @@
|
|||||||
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
|
||||||
import customerEndpoint from '@commerce/api/endpoints/customer'
|
|
||||||
import type { CustomerSchema } from '../../../types/customer'
|
|
||||||
import type { BigcommerceAPI } from '../..'
|
|
||||||
import getLoggedInCustomer from './get-logged-in-customer'
|
|
||||||
|
|
||||||
export type CustomerAPI = GetAPISchema<BigcommerceAPI, CustomerSchema>
|
|
||||||
|
|
||||||
export type CustomerEndpoint = CustomerAPI['endpoint']
|
|
||||||
|
|
||||||
export const handlers: CustomerEndpoint['handlers'] = { getLoggedInCustomer }
|
|
||||||
|
|
||||||
const customerApi = createEndpoint<CustomerAPI>({
|
|
||||||
handler: customerEndpoint,
|
|
||||||
handlers,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default customerApi
|
|
@ -1,18 +0,0 @@
|
|||||||
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
|
||||||
import loginEndpoint from '@commerce/api/endpoints/login'
|
|
||||||
import type { LoginSchema } from '../../../types/login'
|
|
||||||
import type { BigcommerceAPI } from '../..'
|
|
||||||
import login from './login'
|
|
||||||
|
|
||||||
export type LoginAPI = GetAPISchema<BigcommerceAPI, LoginSchema>
|
|
||||||
|
|
||||||
export type LoginEndpoint = LoginAPI['endpoint']
|
|
||||||
|
|
||||||
export const handlers: LoginEndpoint['handlers'] = { login }
|
|
||||||
|
|
||||||
const loginApi = createEndpoint<LoginAPI>({
|
|
||||||
handler: loginEndpoint,
|
|
||||||
handlers,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default loginApi
|
|
@ -1,49 +0,0 @@
|
|||||||
import { FetcherError } from '@commerce/utils/errors'
|
|
||||||
import type { LoginEndpoint } from '.'
|
|
||||||
|
|
||||||
const invalidCredentials = /invalid credentials/i
|
|
||||||
|
|
||||||
const login: LoginEndpoint['handlers']['login'] = async ({
|
|
||||||
res,
|
|
||||||
body: { email, password },
|
|
||||||
config,
|
|
||||||
commerce,
|
|
||||||
}) => {
|
|
||||||
// TODO: Add proper validations with something like Ajv
|
|
||||||
if (!(email && password)) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// TODO: validate the password and email
|
|
||||||
// Passwords must be at least 7 characters and contain both alphabetic
|
|
||||||
// and numeric characters.
|
|
||||||
|
|
||||||
try {
|
|
||||||
await commerce.login({ variables: { email, password }, config, res })
|
|
||||||
} catch (error) {
|
|
||||||
// Check if the email and password didn't match an existing account
|
|
||||||
if (
|
|
||||||
error instanceof FetcherError &&
|
|
||||||
invalidCredentials.test(error.message)
|
|
||||||
) {
|
|
||||||
return res.status(401).json({
|
|
||||||
data: null,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
message:
|
|
||||||
'Cannot find an account that matches the provided credentials',
|
|
||||||
code: 'invalid_credentials',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({ data: null })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default login
|
|
@ -1,18 +0,0 @@
|
|||||||
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
|
||||||
import logoutEndpoint from '@commerce/api/endpoints/logout'
|
|
||||||
import type { LogoutSchema } from '../../../types/logout'
|
|
||||||
import type { BigcommerceAPI } from '../..'
|
|
||||||
import logout from './logout'
|
|
||||||
|
|
||||||
export type LogoutAPI = GetAPISchema<BigcommerceAPI, LogoutSchema>
|
|
||||||
|
|
||||||
export type LogoutEndpoint = LogoutAPI['endpoint']
|
|
||||||
|
|
||||||
export const handlers: LogoutEndpoint['handlers'] = { logout }
|
|
||||||
|
|
||||||
const logoutApi = createEndpoint<LogoutAPI>({
|
|
||||||
handler: logoutEndpoint,
|
|
||||||
handlers,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default logoutApi
|
|
@ -1,23 +0,0 @@
|
|||||||
import { serialize } from 'cookie'
|
|
||||||
import type { LogoutEndpoint } from '.'
|
|
||||||
|
|
||||||
const logout: LogoutEndpoint['handlers']['logout'] = async ({
|
|
||||||
res,
|
|
||||||
body: { redirectTo },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
// Remove the cookie
|
|
||||||
res.setHeader(
|
|
||||||
'Set-Cookie',
|
|
||||||
serialize(config.customerCookie, '', { maxAge: -1, path: '/' })
|
|
||||||
)
|
|
||||||
|
|
||||||
// Only allow redirects to a relative URL
|
|
||||||
if (redirectTo?.startsWith('/')) {
|
|
||||||
res.redirect(redirectTo)
|
|
||||||
} else {
|
|
||||||
res.status(200).json({ data: null })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default logout
|
|
@ -1,18 +0,0 @@
|
|||||||
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
|
||||||
import signupEndpoint from '@commerce/api/endpoints/signup'
|
|
||||||
import type { SignupSchema } from '../../../types/signup'
|
|
||||||
import type { BigcommerceAPI } from '../..'
|
|
||||||
import signup from './signup'
|
|
||||||
|
|
||||||
export type SignupAPI = GetAPISchema<BigcommerceAPI, SignupSchema>
|
|
||||||
|
|
||||||
export type SignupEndpoint = SignupAPI['endpoint']
|
|
||||||
|
|
||||||
export const handlers: SignupEndpoint['handlers'] = { signup }
|
|
||||||
|
|
||||||
const singupApi = createEndpoint<SignupAPI>({
|
|
||||||
handler: signupEndpoint,
|
|
||||||
handlers,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default singupApi
|
|
@ -1,62 +0,0 @@
|
|||||||
import { BigcommerceApiError } from '../../utils/errors'
|
|
||||||
import type { SignupEndpoint } from '.'
|
|
||||||
|
|
||||||
const signup: SignupEndpoint['handlers']['signup'] = async ({
|
|
||||||
res,
|
|
||||||
body: { firstName, lastName, email, password },
|
|
||||||
config,
|
|
||||||
commerce,
|
|
||||||
}) => {
|
|
||||||
// TODO: Add proper validations with something like Ajv
|
|
||||||
if (!(firstName && lastName && email && password)) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// TODO: validate the password and email
|
|
||||||
// Passwords must be at least 7 characters and contain both alphabetic
|
|
||||||
// and numeric characters.
|
|
||||||
|
|
||||||
try {
|
|
||||||
await config.storeApiFetch('/v3/customers', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify([
|
|
||||||
{
|
|
||||||
first_name: firstName,
|
|
||||||
last_name: lastName,
|
|
||||||
email,
|
|
||||||
authentication: {
|
|
||||||
new_password: password,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof BigcommerceApiError && error.status === 422) {
|
|
||||||
const hasEmailError = '0.email' in error.data?.errors
|
|
||||||
|
|
||||||
// If there's an error with the email, it most likely means it's duplicated
|
|
||||||
if (hasEmailError) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
message: 'The email is already in use',
|
|
||||||
code: 'duplicated_email',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login the customer right after creating it
|
|
||||||
await commerce.login({ variables: { email, password }, res, config })
|
|
||||||
|
|
||||||
res.status(200).json({ data: null })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default signup
|
|
@ -1,57 +0,0 @@
|
|||||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
|
||||||
import { parseWishlistItem } from '../../utils/parse-item'
|
|
||||||
import getCustomerId from '../../utils/get-customer-id'
|
|
||||||
import type { WishlistEndpoint } from '.'
|
|
||||||
|
|
||||||
// Return wishlist info
|
|
||||||
const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
|
|
||||||
res,
|
|
||||||
body: { customerToken, item },
|
|
||||||
config,
|
|
||||||
commerce,
|
|
||||||
}) => {
|
|
||||||
if (!item) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Missing item' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const customerId =
|
|
||||||
customerToken && (await getCustomerId({ customerToken, config }))
|
|
||||||
|
|
||||||
if (!customerId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const { wishlist } = await commerce.getCustomerWishlist({
|
|
||||||
variables: { customerId },
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
const options = {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(
|
|
||||||
wishlist
|
|
||||||
? {
|
|
||||||
items: [parseWishlistItem(item)],
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
name: 'Wishlist',
|
|
||||||
customer_id: customerId,
|
|
||||||
items: [parseWishlistItem(item)],
|
|
||||||
is_public: false,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = wishlist
|
|
||||||
? await config.storeApiFetch(`/v3/wishlists/${wishlist.id}/items`, options)
|
|
||||||
: await config.storeApiFetch('/v3/wishlists', options)
|
|
||||||
|
|
||||||
res.status(200).json({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default addItem
|
|
@ -1,39 +0,0 @@
|
|||||||
import type { Wishlist } from '../../../types/wishlist'
|
|
||||||
import type { WishlistEndpoint } from '.'
|
|
||||||
import getCustomerId from '../../utils/get-customer-id'
|
|
||||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
|
||||||
|
|
||||||
// Return wishlist info
|
|
||||||
const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
|
|
||||||
res,
|
|
||||||
body: { customerToken, includeProducts },
|
|
||||||
config,
|
|
||||||
commerce,
|
|
||||||
}) => {
|
|
||||||
let result: { data?: Wishlist } = {}
|
|
||||||
|
|
||||||
if (customerToken) {
|
|
||||||
const customerId =
|
|
||||||
customerToken && (await getCustomerId({ customerToken, config }))
|
|
||||||
|
|
||||||
if (!customerId) {
|
|
||||||
// If the customerToken is invalid, then this request is too
|
|
||||||
return res.status(404).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Wishlist not found' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const { wishlist } = await commerce.getCustomerWishlist({
|
|
||||||
variables: { customerId },
|
|
||||||
includeProducts,
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
|
|
||||||
result = { data: wishlist }
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({ data: result.data ?? null })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getWishlist
|
|
@ -1,24 +0,0 @@
|
|||||||
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
|
||||||
import wishlistEndpoint from '@commerce/api/endpoints/wishlist'
|
|
||||||
import type { WishlistSchema } from '../../../types/wishlist'
|
|
||||||
import type { BigcommerceAPI } from '../..'
|
|
||||||
import getWishlist from './get-wishlist'
|
|
||||||
import addItem from './add-item'
|
|
||||||
import removeItem from './remove-item'
|
|
||||||
|
|
||||||
export type WishlistAPI = GetAPISchema<BigcommerceAPI, WishlistSchema>
|
|
||||||
|
|
||||||
export type WishlistEndpoint = WishlistAPI['endpoint']
|
|
||||||
|
|
||||||
export const handlers: WishlistEndpoint['handlers'] = {
|
|
||||||
getWishlist,
|
|
||||||
addItem,
|
|
||||||
removeItem,
|
|
||||||
}
|
|
||||||
|
|
||||||
const wishlistApi = createEndpoint<WishlistAPI>({
|
|
||||||
handler: wishlistEndpoint,
|
|
||||||
handlers,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default wishlistApi
|
|
@ -1,39 +0,0 @@
|
|||||||
import type { Wishlist } from '../../../types/wishlist'
|
|
||||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
|
||||||
import getCustomerId from '../../utils/get-customer-id'
|
|
||||||
import type { WishlistEndpoint } from '.'
|
|
||||||
|
|
||||||
// Return wishlist info
|
|
||||||
const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
|
|
||||||
res,
|
|
||||||
body: { customerToken, itemId },
|
|
||||||
config,
|
|
||||||
commerce,
|
|
||||||
}) => {
|
|
||||||
const customerId =
|
|
||||||
customerToken && (await getCustomerId({ customerToken, config }))
|
|
||||||
const { wishlist } =
|
|
||||||
(customerId &&
|
|
||||||
(await commerce.getCustomerWishlist({
|
|
||||||
variables: { customerId },
|
|
||||||
config,
|
|
||||||
}))) ||
|
|
||||||
{}
|
|
||||||
|
|
||||||
if (!wishlist || !itemId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await config.storeApiFetch<{ data: Wishlist } | null>(
|
|
||||||
`/v3/wishlists/${wishlist.id}/items/${itemId}`,
|
|
||||||
{ method: 'DELETE' }
|
|
||||||
)
|
|
||||||
const data = result?.data ?? null
|
|
||||||
|
|
||||||
res.status(200).json({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default removeItem
|
|
@ -1,9 +0,0 @@
|
|||||||
export const categoryTreeItemFragment = /* GraphQL */ `
|
|
||||||
fragment categoryTreeItem on CategoryTreeItem {
|
|
||||||
entityId
|
|
||||||
name
|
|
||||||
path
|
|
||||||
description
|
|
||||||
productCount
|
|
||||||
}
|
|
||||||
`
|
|
@ -1,113 +0,0 @@
|
|||||||
export const productPrices = /* GraphQL */ `
|
|
||||||
fragment productPrices on Prices {
|
|
||||||
price {
|
|
||||||
value
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
salePrice {
|
|
||||||
value
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
retailPrice {
|
|
||||||
value
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const swatchOptionFragment = /* GraphQL */ `
|
|
||||||
fragment swatchOption on SwatchOptionValue {
|
|
||||||
isDefault
|
|
||||||
hexColors
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const multipleChoiceOptionFragment = /* GraphQL */ `
|
|
||||||
fragment multipleChoiceOption on MultipleChoiceOption {
|
|
||||||
values {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
label
|
|
||||||
...swatchOption
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
${swatchOptionFragment}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const productInfoFragment = /* GraphQL */ `
|
|
||||||
fragment productInfo on Product {
|
|
||||||
entityId
|
|
||||||
name
|
|
||||||
path
|
|
||||||
brand {
|
|
||||||
entityId
|
|
||||||
}
|
|
||||||
description
|
|
||||||
prices {
|
|
||||||
...productPrices
|
|
||||||
}
|
|
||||||
images {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
urlOriginal
|
|
||||||
altText
|
|
||||||
isDefault
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variants {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
entityId
|
|
||||||
defaultImage {
|
|
||||||
urlOriginal
|
|
||||||
altText
|
|
||||||
isDefault
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
productOptions {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
__typename
|
|
||||||
entityId
|
|
||||||
displayName
|
|
||||||
...multipleChoiceOption
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
localeMeta: metafields(namespace: $locale, keys: ["name", "description"])
|
|
||||||
@include(if: $hasLocale) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
key
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
${productPrices}
|
|
||||||
${multipleChoiceOptionFragment}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const productConnectionFragment = /* GraphQL */ `
|
|
||||||
fragment productConnnection on ProductConnection {
|
|
||||||
pageInfo {
|
|
||||||
startCursor
|
|
||||||
endCursor
|
|
||||||
}
|
|
||||||
edges {
|
|
||||||
cursor
|
|
||||||
node {
|
|
||||||
...productInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
${productInfoFragment}
|
|
||||||
`
|
|
@ -1,120 +0,0 @@
|
|||||||
import type { RequestInit } from '@vercel/fetch'
|
|
||||||
import {
|
|
||||||
CommerceAPI,
|
|
||||||
CommerceAPIConfig,
|
|
||||||
getCommerceApi as commerceApi,
|
|
||||||
} from '@commerce/api'
|
|
||||||
import createFetchGraphqlApi from './utils/fetch-graphql-api'
|
|
||||||
import createFetchStoreApi from './utils/fetch-store-api'
|
|
||||||
|
|
||||||
import type { CartAPI } from './endpoints/cart'
|
|
||||||
import type { CustomerAPI } from './endpoints/customer'
|
|
||||||
import type { LoginAPI } from './endpoints/login'
|
|
||||||
import type { LogoutAPI } from './endpoints/logout'
|
|
||||||
import type { SignupAPI } from './endpoints/signup'
|
|
||||||
import type { ProductsAPI } from './endpoints/catalog/products'
|
|
||||||
import type { WishlistAPI } from './endpoints/wishlist'
|
|
||||||
|
|
||||||
import login from './operations/login'
|
|
||||||
import getAllPages from './operations/get-all-pages'
|
|
||||||
import getPage from './operations/get-page'
|
|
||||||
import getSiteInfo from './operations/get-site-info'
|
|
||||||
import getCustomerWishlist from './operations/get-customer-wishlist'
|
|
||||||
import getAllProductPaths from './operations/get-all-product-paths'
|
|
||||||
import getAllProducts from './operations/get-all-products'
|
|
||||||
import getProduct from './operations/get-product'
|
|
||||||
|
|
||||||
export interface BigcommerceConfig extends CommerceAPIConfig {
|
|
||||||
// Indicates if the returned metadata with translations should be applied to the
|
|
||||||
// data or returned as it is
|
|
||||||
applyLocale?: boolean
|
|
||||||
storeApiUrl: string
|
|
||||||
storeApiToken: string
|
|
||||||
storeApiClientId: string
|
|
||||||
storeChannelId?: string
|
|
||||||
storeUrl?: string
|
|
||||||
storeApiClientSecret?: string
|
|
||||||
storeHash?:string
|
|
||||||
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_URL = process.env.BIGCOMMERCE_STOREFRONT_API_URL
|
|
||||||
const API_TOKEN = process.env.BIGCOMMERCE_STOREFRONT_API_TOKEN
|
|
||||||
const STORE_API_URL = process.env.BIGCOMMERCE_STORE_API_URL
|
|
||||||
const STORE_API_TOKEN = process.env.BIGCOMMERCE_STORE_API_TOKEN
|
|
||||||
const STORE_API_CLIENT_ID = process.env.BIGCOMMERCE_STORE_API_CLIENT_ID
|
|
||||||
const STORE_CHANNEL_ID = process.env.BIGCOMMERCE_CHANNEL_ID
|
|
||||||
const STORE_URL = process.env.BIGCOMMERCE_STORE_URL
|
|
||||||
const CLIENT_SECRET = process.env.BIGCOMMERCE_STORE_API_CLIENT_SECRET
|
|
||||||
const STOREFRONT_HASH = process.env.BIGCOMMERCE_STORE_API_STORE_HASH
|
|
||||||
|
|
||||||
if (!API_URL) {
|
|
||||||
throw new Error(
|
|
||||||
`The environment variable BIGCOMMERCE_STOREFRONT_API_URL is missing and it's required to access your store`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!API_TOKEN) {
|
|
||||||
throw new Error(
|
|
||||||
`The environment variable BIGCOMMERCE_STOREFRONT_API_TOKEN is missing and it's required to access your store`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(STORE_API_URL && STORE_API_TOKEN && STORE_API_CLIENT_ID)) {
|
|
||||||
throw new Error(
|
|
||||||
`The environment variables BIGCOMMERCE_STORE_API_URL, BIGCOMMERCE_STORE_API_TOKEN, BIGCOMMERCE_STORE_API_CLIENT_ID have to be set in order to access the REST API of your store`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ONE_DAY = 60 * 60 * 24
|
|
||||||
|
|
||||||
const config: BigcommerceConfig = {
|
|
||||||
commerceUrl: API_URL,
|
|
||||||
apiToken: API_TOKEN,
|
|
||||||
customerCookie: 'SHOP_TOKEN',
|
|
||||||
cartCookie: process.env.BIGCOMMERCE_CART_COOKIE ?? 'bc_cartId',
|
|
||||||
cartCookieMaxAge: ONE_DAY * 30,
|
|
||||||
fetch: createFetchGraphqlApi(() => getCommerceApi().getConfig()),
|
|
||||||
applyLocale: true,
|
|
||||||
// REST API only
|
|
||||||
storeApiUrl: STORE_API_URL,
|
|
||||||
storeApiToken: STORE_API_TOKEN,
|
|
||||||
storeApiClientId: STORE_API_CLIENT_ID,
|
|
||||||
storeChannelId: STORE_CHANNEL_ID,
|
|
||||||
storeUrl:STORE_URL,
|
|
||||||
storeApiClientSecret:CLIENT_SECRET,
|
|
||||||
storeHash: STOREFRONT_HASH,
|
|
||||||
storeApiFetch: createFetchStoreApi(() => getCommerceApi().getConfig()),
|
|
||||||
}
|
|
||||||
|
|
||||||
const operations = {
|
|
||||||
login,
|
|
||||||
getAllPages,
|
|
||||||
getPage,
|
|
||||||
getSiteInfo,
|
|
||||||
getCustomerWishlist,
|
|
||||||
getAllProductPaths,
|
|
||||||
getAllProducts,
|
|
||||||
getProduct,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const provider = { config, operations }
|
|
||||||
|
|
||||||
export type Provider = typeof provider
|
|
||||||
|
|
||||||
export type APIs =
|
|
||||||
| CartAPI
|
|
||||||
| CustomerAPI
|
|
||||||
| LoginAPI
|
|
||||||
| LogoutAPI
|
|
||||||
| SignupAPI
|
|
||||||
| ProductsAPI
|
|
||||||
| WishlistAPI
|
|
||||||
|
|
||||||
export type BigcommerceAPI<P extends Provider = Provider> = CommerceAPI<P>
|
|
||||||
|
|
||||||
export function getCommerceApi<P extends Provider>(
|
|
||||||
customProvider: P = provider as any
|
|
||||||
): BigcommerceAPI<P> {
|
|
||||||
return commerceApi(customProvider)
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
import type {
|
|
||||||
OperationContext,
|
|
||||||
OperationOptions,
|
|
||||||
} from '@commerce/api/operations'
|
|
||||||
import type { Page, GetAllPagesOperation } from '../../types/page'
|
|
||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
|
||||||
import { BigcommerceConfig, Provider } from '..'
|
|
||||||
|
|
||||||
export default function getAllPagesOperation({
|
|
||||||
commerce,
|
|
||||||
}: OperationContext<Provider>) {
|
|
||||||
async function getAllPages<T extends GetAllPagesOperation>(opts?: {
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getAllPages<T extends GetAllPagesOperation>(
|
|
||||||
opts: {
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
} & OperationOptions
|
|
||||||
): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getAllPages<T extends GetAllPagesOperation>({
|
|
||||||
config,
|
|
||||||
preview,
|
|
||||||
}: {
|
|
||||||
url?: string
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
} = {}): Promise<T['data']> {
|
|
||||||
const cfg = commerce.getConfig(config)
|
|
||||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
|
||||||
// required in case there's a custom `url`
|
|
||||||
const { data } = await cfg.storeApiFetch<
|
|
||||||
RecursivePartial<{ data: Page[] }>
|
|
||||||
>('/v3/content/pages')
|
|
||||||
const pages = (data as RecursiveRequired<typeof data>) ?? []
|
|
||||||
|
|
||||||
return {
|
|
||||||
pages: preview ? pages : pages.filter((p) => p.is_visible),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getAllPages
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
import type {
|
|
||||||
OperationContext,
|
|
||||||
OperationOptions,
|
|
||||||
} from '@commerce/api/operations'
|
|
||||||
import type { GetAllProductPathsQuery } from '../../schema'
|
|
||||||
import type { GetAllProductPathsOperation } from '../../types/product'
|
|
||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
|
||||||
import filterEdges from '../utils/filter-edges'
|
|
||||||
import { BigcommerceConfig, Provider } from '..'
|
|
||||||
|
|
||||||
export const getAllProductPathsQuery = /* GraphQL */ `
|
|
||||||
query getAllProductPaths($first: Int = 100) {
|
|
||||||
site {
|
|
||||||
products(first: $first) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default function getAllProductPathsOperation({
|
|
||||||
commerce,
|
|
||||||
}: OperationContext<Provider>) {
|
|
||||||
async function getAllProductPaths<
|
|
||||||
T extends GetAllProductPathsOperation
|
|
||||||
>(opts?: {
|
|
||||||
variables?: T['variables']
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
}): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getAllProductPaths<T extends GetAllProductPathsOperation>(
|
|
||||||
opts: {
|
|
||||||
variables?: T['variables']
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
} & OperationOptions
|
|
||||||
): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
|
|
||||||
query = getAllProductPathsQuery,
|
|
||||||
variables,
|
|
||||||
config,
|
|
||||||
}: {
|
|
||||||
query?: string
|
|
||||||
variables?: T['variables']
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
} = {}): Promise<T['data']> {
|
|
||||||
config = commerce.getConfig(config)
|
|
||||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
|
||||||
// required in case there's a custom `query`
|
|
||||||
const { data } = await config.fetch<
|
|
||||||
RecursivePartial<GetAllProductPathsQuery>
|
|
||||||
>(query, { variables })
|
|
||||||
const products = data.site?.products?.edges
|
|
||||||
|
|
||||||
return {
|
|
||||||
products: filterEdges(products as RecursiveRequired<typeof products>).map(
|
|
||||||
({ node }) => node
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return getAllProductPaths
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
import type {
|
|
||||||
OperationContext,
|
|
||||||
OperationOptions,
|
|
||||||
} from '@commerce/api/operations'
|
|
||||||
import type {
|
|
||||||
GetAllProductsQuery,
|
|
||||||
GetAllProductsQueryVariables,
|
|
||||||
} from '../../schema'
|
|
||||||
import type { GetAllProductsOperation } from '../../types/product'
|
|
||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
|
||||||
import filterEdges from '../utils/filter-edges'
|
|
||||||
import setProductLocaleMeta from '../utils/set-product-locale-meta'
|
|
||||||
import { productConnectionFragment } from '../fragments/product'
|
|
||||||
import { BigcommerceConfig, Provider } from '..'
|
|
||||||
import { normalizeProduct } from '../../lib/normalize'
|
|
||||||
|
|
||||||
export const getAllProductsQuery = /* GraphQL */ `
|
|
||||||
query getAllProducts(
|
|
||||||
$hasLocale: Boolean = false
|
|
||||||
$locale: String = "null"
|
|
||||||
$entityIds: [Int!]
|
|
||||||
$first: Int = 10
|
|
||||||
$products: Boolean = false
|
|
||||||
$featuredProducts: Boolean = false
|
|
||||||
$bestSellingProducts: Boolean = false
|
|
||||||
$newestProducts: Boolean = false
|
|
||||||
) {
|
|
||||||
site {
|
|
||||||
products(first: $first, entityIds: $entityIds) @include(if: $products) {
|
|
||||||
...productConnnection
|
|
||||||
}
|
|
||||||
featuredProducts(first: $first) @include(if: $featuredProducts) {
|
|
||||||
...productConnnection
|
|
||||||
}
|
|
||||||
bestSellingProducts(first: $first) @include(if: $bestSellingProducts) {
|
|
||||||
...productConnnection
|
|
||||||
}
|
|
||||||
newestProducts(first: $first) @include(if: $newestProducts) {
|
|
||||||
...productConnnection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
${productConnectionFragment}
|
|
||||||
`
|
|
||||||
|
|
||||||
export type ProductEdge = NonNullable<
|
|
||||||
NonNullable<GetAllProductsQuery['site']['products']['edges']>[0]
|
|
||||||
>
|
|
||||||
|
|
||||||
export type ProductNode = ProductEdge['node']
|
|
||||||
|
|
||||||
export type GetAllProductsResult<
|
|
||||||
T extends Record<keyof GetAllProductsResult, any[]> = {
|
|
||||||
products: ProductEdge[]
|
|
||||||
}
|
|
||||||
> = T
|
|
||||||
|
|
||||||
function getProductsType(
|
|
||||||
relevance?: GetAllProductsOperation['variables']['relevance']
|
|
||||||
) {
|
|
||||||
switch (relevance) {
|
|
||||||
case 'featured':
|
|
||||||
return 'featuredProducts'
|
|
||||||
case 'best_selling':
|
|
||||||
return 'bestSellingProducts'
|
|
||||||
case 'newest':
|
|
||||||
return 'newestProducts'
|
|
||||||
default:
|
|
||||||
return 'products'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function getAllProductsOperation({
|
|
||||||
commerce,
|
|
||||||
}: OperationContext<Provider>) {
|
|
||||||
async function getAllProducts<T extends GetAllProductsOperation>(opts?: {
|
|
||||||
variables?: T['variables']
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getAllProducts<T extends GetAllProductsOperation>(
|
|
||||||
opts: {
|
|
||||||
variables?: T['variables']
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
} & OperationOptions
|
|
||||||
): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getAllProducts<T extends GetAllProductsOperation>({
|
|
||||||
query = getAllProductsQuery,
|
|
||||||
variables: vars = {},
|
|
||||||
config: cfg,
|
|
||||||
}: {
|
|
||||||
query?: string
|
|
||||||
variables?: T['variables']
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
} = {}): Promise<T['data']> {
|
|
||||||
const config = commerce.getConfig(cfg)
|
|
||||||
const { locale } = config
|
|
||||||
const field = getProductsType(vars.relevance)
|
|
||||||
const variables: GetAllProductsQueryVariables = {
|
|
||||||
locale,
|
|
||||||
hasLocale: !!locale,
|
|
||||||
}
|
|
||||||
|
|
||||||
variables[field] = true
|
|
||||||
|
|
||||||
if (vars.first) variables.first = vars.first
|
|
||||||
if (vars.ids) variables.entityIds = vars.ids.map((id) => Number(id))
|
|
||||||
|
|
||||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
|
||||||
// required in case there's a custom `query`
|
|
||||||
const { data } = await config.fetch<RecursivePartial<GetAllProductsQuery>>(
|
|
||||||
query,
|
|
||||||
{ variables }
|
|
||||||
)
|
|
||||||
const edges = data.site?.[field]?.edges
|
|
||||||
const products = filterEdges(edges as RecursiveRequired<typeof edges>)
|
|
||||||
|
|
||||||
if (locale && config.applyLocale) {
|
|
||||||
products.forEach((product: RecursivePartial<ProductEdge>) => {
|
|
||||||
if (product.node) setProductLocaleMeta(product.node)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
products: products.map(({ node }) => normalizeProduct(node as any)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getAllProducts
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
import type {
|
|
||||||
OperationContext,
|
|
||||||
OperationOptions,
|
|
||||||
} from '@commerce/api/operations'
|
|
||||||
import type {
|
|
||||||
GetCustomerWishlistOperation,
|
|
||||||
Wishlist,
|
|
||||||
} from '../../types/wishlist'
|
|
||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
|
||||||
import { BigcommerceConfig, Provider } from '..'
|
|
||||||
import getAllProducts, { ProductEdge } from './get-all-products'
|
|
||||||
|
|
||||||
export default function getCustomerWishlistOperation({
|
|
||||||
commerce,
|
|
||||||
}: OperationContext<Provider>) {
|
|
||||||
async function getCustomerWishlist<
|
|
||||||
T extends GetCustomerWishlistOperation
|
|
||||||
>(opts: {
|
|
||||||
variables: T['variables']
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
includeProducts?: boolean
|
|
||||||
}): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getCustomerWishlist<T extends GetCustomerWishlistOperation>(
|
|
||||||
opts: {
|
|
||||||
variables: T['variables']
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
includeProducts?: boolean
|
|
||||||
} & OperationOptions
|
|
||||||
): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getCustomerWishlist<T extends GetCustomerWishlistOperation>({
|
|
||||||
config,
|
|
||||||
variables,
|
|
||||||
includeProducts,
|
|
||||||
}: {
|
|
||||||
url?: string
|
|
||||||
variables: T['variables']
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
includeProducts?: boolean
|
|
||||||
}): Promise<T['data']> {
|
|
||||||
config = commerce.getConfig(config)
|
|
||||||
|
|
||||||
const { data = [] } = await config.storeApiFetch<
|
|
||||||
RecursivePartial<{ data: Wishlist[] }>
|
|
||||||
>(`/v3/wishlists?customer_id=${variables.customerId}`)
|
|
||||||
const wishlist = data[0]
|
|
||||||
|
|
||||||
if (includeProducts && wishlist?.items?.length) {
|
|
||||||
const ids = wishlist.items
|
|
||||||
?.map((item) => (item?.product_id ? String(item?.product_id) : null))
|
|
||||||
.filter((id): id is string => !!id)
|
|
||||||
|
|
||||||
if (ids?.length) {
|
|
||||||
const graphqlData = await commerce.getAllProducts({
|
|
||||||
variables: { first: 100, ids },
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
// Put the products in an object that we can use to get them by id
|
|
||||||
const productsById = graphqlData.products.reduce<{
|
|
||||||
[k: number]: ProductEdge
|
|
||||||
}>((prods, p) => {
|
|
||||||
prods[Number(p.id)] = p as any
|
|
||||||
return prods
|
|
||||||
}, {})
|
|
||||||
// Populate the wishlist items with the graphql products
|
|
||||||
wishlist.items.forEach((item) => {
|
|
||||||
const product = item && productsById[item.product_id!]
|
|
||||||
if (item && product) {
|
|
||||||
// @ts-ignore Fix this type when the wishlist type is properly defined
|
|
||||||
item.product = product
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { wishlist: wishlist as RecursiveRequired<typeof wishlist> }
|
|
||||||
}
|
|
||||||
|
|
||||||
return getCustomerWishlist
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
import type {
|
|
||||||
OperationContext,
|
|
||||||
OperationOptions,
|
|
||||||
} from '@commerce/api/operations'
|
|
||||||
import type { GetPageOperation, Page } from '../../types/page'
|
|
||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
|
||||||
import type { BigcommerceConfig, Provider } from '..'
|
|
||||||
import { normalizePage } from '../../lib/normalize'
|
|
||||||
|
|
||||||
export default function getPageOperation({
|
|
||||||
commerce,
|
|
||||||
}: OperationContext<Provider>) {
|
|
||||||
async function getPage<T extends GetPageOperation>(opts: {
|
|
||||||
variables: T['variables']
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getPage<T extends GetPageOperation>(
|
|
||||||
opts: {
|
|
||||||
variables: T['variables']
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
} & OperationOptions
|
|
||||||
): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getPage<T extends GetPageOperation>({
|
|
||||||
url,
|
|
||||||
variables,
|
|
||||||
config,
|
|
||||||
preview,
|
|
||||||
}: {
|
|
||||||
url?: string
|
|
||||||
variables: T['variables']
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<T['data']> {
|
|
||||||
const cfg = commerce.getConfig(config)
|
|
||||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
|
||||||
// required in case there's a custom `url`
|
|
||||||
const { data } = await cfg.storeApiFetch<
|
|
||||||
RecursivePartial<{ data: Page[] }>
|
|
||||||
>(url || `/v3/content/pages?id=${variables.id}&include=body`)
|
|
||||||
const firstPage = data?.[0]
|
|
||||||
const page = firstPage as RecursiveRequired<typeof firstPage>
|
|
||||||
|
|
||||||
if (preview || page?.is_visible) {
|
|
||||||
return { page: normalizePage(page as any) }
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getPage
|
|
||||||
}
|
|
@ -1,119 +0,0 @@
|
|||||||
import type {
|
|
||||||
OperationContext,
|
|
||||||
OperationOptions,
|
|
||||||
} from '@commerce/api/operations'
|
|
||||||
import type { GetProductOperation } from '../../types/product'
|
|
||||||
import type { GetProductQuery, GetProductQueryVariables } from '../../schema'
|
|
||||||
import setProductLocaleMeta from '../utils/set-product-locale-meta'
|
|
||||||
import { productInfoFragment } from '../fragments/product'
|
|
||||||
import { BigcommerceConfig, Provider } from '..'
|
|
||||||
import { normalizeProduct } from '../../lib/normalize'
|
|
||||||
|
|
||||||
export const getProductQuery = /* GraphQL */ `
|
|
||||||
query getProduct(
|
|
||||||
$hasLocale: Boolean = false
|
|
||||||
$locale: String = "null"
|
|
||||||
$path: String!
|
|
||||||
) {
|
|
||||||
site {
|
|
||||||
route(path: $path) {
|
|
||||||
node {
|
|
||||||
__typename
|
|
||||||
... on Product {
|
|
||||||
...productInfo
|
|
||||||
variants {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
entityId
|
|
||||||
defaultImage {
|
|
||||||
urlOriginal
|
|
||||||
altText
|
|
||||||
isDefault
|
|
||||||
}
|
|
||||||
prices {
|
|
||||||
...productPrices
|
|
||||||
}
|
|
||||||
inventory {
|
|
||||||
aggregated {
|
|
||||||
availableToSell
|
|
||||||
warningLevel
|
|
||||||
}
|
|
||||||
isInStock
|
|
||||||
}
|
|
||||||
productOptions {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
__typename
|
|
||||||
entityId
|
|
||||||
displayName
|
|
||||||
...multipleChoiceOption
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
${productInfoFragment}
|
|
||||||
`
|
|
||||||
|
|
||||||
// TODO: See if this type is useful for defining the Product type
|
|
||||||
// export type ProductNode = Extract<
|
|
||||||
// GetProductQuery['site']['route']['node'],
|
|
||||||
// { __typename: 'Product' }
|
|
||||||
// >
|
|
||||||
|
|
||||||
export default function getAllProductPathsOperation({
|
|
||||||
commerce,
|
|
||||||
}: OperationContext<Provider>) {
|
|
||||||
async function getProduct<T extends GetProductOperation>(opts: {
|
|
||||||
variables: T['variables']
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getProduct<T extends GetProductOperation>(
|
|
||||||
opts: {
|
|
||||||
variables: T['variables']
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
} & OperationOptions
|
|
||||||
): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getProduct<T extends GetProductOperation>({
|
|
||||||
query = getProductQuery,
|
|
||||||
variables: { slug, ...vars },
|
|
||||||
config: cfg,
|
|
||||||
}: {
|
|
||||||
query?: string
|
|
||||||
variables: T['variables']
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<T['data']> {
|
|
||||||
const config = commerce.getConfig(cfg)
|
|
||||||
const { locale } = config
|
|
||||||
const variables: GetProductQueryVariables = {
|
|
||||||
locale,
|
|
||||||
hasLocale: !!locale,
|
|
||||||
path: slug ? `/${slug}/` : vars.path!,
|
|
||||||
}
|
|
||||||
const { data } = await config.fetch<GetProductQuery>(query, { variables })
|
|
||||||
const product = data.site?.route?.node
|
|
||||||
|
|
||||||
if (product?.__typename === 'Product') {
|
|
||||||
if (locale && config.applyLocale) {
|
|
||||||
setProductLocaleMeta(product)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { product: normalizeProduct(product as any) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
return getProduct
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
import type {
|
|
||||||
OperationContext,
|
|
||||||
OperationOptions,
|
|
||||||
} from '@commerce/api/operations'
|
|
||||||
import type { GetSiteInfoOperation } from '../../types/site'
|
|
||||||
import type { GetSiteInfoQuery } from '../../schema'
|
|
||||||
import filterEdges from '../utils/filter-edges'
|
|
||||||
import type { BigcommerceConfig, Provider } from '..'
|
|
||||||
import { categoryTreeItemFragment } from '../fragments/category-tree'
|
|
||||||
import { normalizeCategory } from '../../lib/normalize'
|
|
||||||
|
|
||||||
// Get 3 levels of categories
|
|
||||||
export const getSiteInfoQuery = /* GraphQL */ `
|
|
||||||
query getSiteInfo {
|
|
||||||
site {
|
|
||||||
categoryTree {
|
|
||||||
...categoryTreeItem
|
|
||||||
children {
|
|
||||||
...categoryTreeItem
|
|
||||||
children {
|
|
||||||
...categoryTreeItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
brands {
|
|
||||||
pageInfo {
|
|
||||||
startCursor
|
|
||||||
endCursor
|
|
||||||
}
|
|
||||||
edges {
|
|
||||||
cursor
|
|
||||||
node {
|
|
||||||
entityId
|
|
||||||
name
|
|
||||||
defaultImage {
|
|
||||||
urlOriginal
|
|
||||||
altText
|
|
||||||
}
|
|
||||||
pageTitle
|
|
||||||
metaDesc
|
|
||||||
metaKeywords
|
|
||||||
searchKeywords
|
|
||||||
path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${categoryTreeItemFragment}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default function getSiteInfoOperation({
|
|
||||||
commerce,
|
|
||||||
}: OperationContext<Provider>) {
|
|
||||||
async function getSiteInfo<T extends GetSiteInfoOperation>(opts?: {
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getSiteInfo<T extends GetSiteInfoOperation>(
|
|
||||||
opts: {
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
} & OperationOptions
|
|
||||||
): Promise<T['data']>
|
|
||||||
|
|
||||||
async function getSiteInfo<T extends GetSiteInfoOperation>({
|
|
||||||
query = getSiteInfoQuery,
|
|
||||||
config,
|
|
||||||
}: {
|
|
||||||
query?: string
|
|
||||||
config?: Partial<BigcommerceConfig>
|
|
||||||
preview?: boolean
|
|
||||||
} = {}): Promise<T['data']> {
|
|
||||||
const cfg = commerce.getConfig(config)
|
|
||||||
const { data } = await cfg.fetch<GetSiteInfoQuery>(query)
|
|
||||||
const categories = data.site.categoryTree.map(normalizeCategory)
|
|
||||||
const brands = data.site?.brands?.edges
|
|
||||||
|
|
||||||
return {
|
|
||||||
categories: categories ?? [],
|
|
||||||
brands: filterEdges(brands),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getSiteInfo
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
import type { ServerResponse } from 'http'
|
|
||||||
import type {
|
|
||||||
OperationContext,
|
|
||||||
OperationOptions,
|
|
||||||
} from '@commerce/api/operations'
|
|
||||||
import type { LoginOperation } from '../../types/login'
|
|
||||||
import type { LoginMutation } from '../../schema'
|
|
||||||
import type { RecursivePartial } from '../utils/types'
|
|
||||||
import concatHeader from '../utils/concat-cookie'
|
|
||||||
import type { BigcommerceConfig, Provider } from '..'
|
|
||||||
|
|
||||||
export const loginMutation = /* GraphQL */ `
|
|
||||||
mutation login($email: String!, $password: String!) {
|
|
||||||
login(email: $email, password: $password) {
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default function loginOperation({
|
|
||||||
commerce,
|
|
||||||
}: OperationContext<Provider>) {
|
|
||||||
async function login<T extends LoginOperation>(opts: {
|
|
||||||
variables: T['variables']
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
res: ServerResponse
|
|
||||||
}): Promise<T['data']>
|
|
||||||
|
|
||||||
async function login<T extends LoginOperation>(
|
|
||||||
opts: {
|
|
||||||
variables: T['variables']
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
res: ServerResponse
|
|
||||||
} & OperationOptions
|
|
||||||
): Promise<T['data']>
|
|
||||||
|
|
||||||
async function login<T extends LoginOperation>({
|
|
||||||
query = loginMutation,
|
|
||||||
variables,
|
|
||||||
res: response,
|
|
||||||
config,
|
|
||||||
}: {
|
|
||||||
query?: string
|
|
||||||
variables: T['variables']
|
|
||||||
res: ServerResponse
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
}): Promise<T['data']> {
|
|
||||||
config = commerce.getConfig(config)
|
|
||||||
|
|
||||||
const { data, res } = await config.fetch<RecursivePartial<LoginMutation>>(
|
|
||||||
query,
|
|
||||||
{ variables }
|
|
||||||
)
|
|
||||||
// Bigcommerce returns a Set-Cookie header with the auth cookie
|
|
||||||
let cookie = res.headers.get('Set-Cookie')
|
|
||||||
|
|
||||||
if (cookie && typeof cookie === 'string') {
|
|
||||||
// In development, don't set a secure cookie or the browser will ignore it
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
cookie = cookie.replace('; Secure', '')
|
|
||||||
// SameSite=none can't be set unless the cookie is Secure
|
|
||||||
// bc seems to sometimes send back SameSite=None rather than none so make
|
|
||||||
// this case insensitive
|
|
||||||
cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax')
|
|
||||||
}
|
|
||||||
|
|
||||||
response.setHeader(
|
|
||||||
'Set-Cookie',
|
|
||||||
concatHeader(response.getHeader('Set-Cookie'), cookie)!
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
result: data.login?.result,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return login
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
type Header = string | number | string[] | undefined
|
|
||||||
|
|
||||||
export default function concatHeader(prev: Header, val: Header) {
|
|
||||||
if (!val) return prev
|
|
||||||
if (!prev) return val
|
|
||||||
|
|
||||||
if (Array.isArray(prev)) return prev.concat(String(val))
|
|
||||||
|
|
||||||
prev = String(prev)
|
|
||||||
|
|
||||||
if (Array.isArray(val)) return [prev].concat(val)
|
|
||||||
|
|
||||||
return [prev, String(val)]
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import type { Response } from '@vercel/fetch'
|
|
||||||
|
|
||||||
// Used for GraphQL errors
|
|
||||||
export class BigcommerceGraphQLError extends Error {}
|
|
||||||
|
|
||||||
export class BigcommerceApiError extends Error {
|
|
||||||
status: number
|
|
||||||
res: Response
|
|
||||||
data: any
|
|
||||||
|
|
||||||
constructor(msg: string, res: Response, data?: any) {
|
|
||||||
super(msg)
|
|
||||||
this.name = 'BigcommerceApiError'
|
|
||||||
this.status = res.status
|
|
||||||
this.res = res
|
|
||||||
this.data = data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BigcommerceNetworkError extends Error {
|
|
||||||
constructor(msg: string) {
|
|
||||||
super(msg)
|
|
||||||
this.name = 'BigcommerceNetworkError'
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
import { FetcherError } from '@commerce/utils/errors'
|
|
||||||
import type { GraphQLFetcher } from '@commerce/api'
|
|
||||||
import type { BigcommerceConfig } from '../index'
|
|
||||||
import fetch from './fetch'
|
|
||||||
|
|
||||||
const fetchGraphqlApi: (getConfig: () => BigcommerceConfig) => GraphQLFetcher =
|
|
||||||
(getConfig) =>
|
|
||||||
async (query: string, { variables, preview } = {}, fetchOptions) => {
|
|
||||||
// log.warn(query)
|
|
||||||
const config = getConfig()
|
|
||||||
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
|
|
||||||
...fetchOptions,
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${config.apiToken}`,
|
|
||||||
...fetchOptions?.headers,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
query,
|
|
||||||
variables,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const json = await res.json()
|
|
||||||
if (json.errors) {
|
|
||||||
throw new FetcherError({
|
|
||||||
errors: json.errors ?? [{ message: 'Failed to fetch Bigcommerce API' }],
|
|
||||||
status: res.status,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { data: json.data, res }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default fetchGraphqlApi
|
|
@ -1,71 +0,0 @@
|
|||||||
import type { RequestInit, Response } from '@vercel/fetch'
|
|
||||||
import type { BigcommerceConfig } from '../index'
|
|
||||||
import { BigcommerceApiError, BigcommerceNetworkError } from './errors'
|
|
||||||
import fetch from './fetch'
|
|
||||||
|
|
||||||
const fetchStoreApi =
|
|
||||||
<T>(getConfig: () => BigcommerceConfig) =>
|
|
||||||
async (endpoint: string, options?: RequestInit): Promise<T> => {
|
|
||||||
const config = getConfig()
|
|
||||||
let res: Response
|
|
||||||
|
|
||||||
try {
|
|
||||||
res = await fetch(config.storeApiUrl + endpoint, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
...options?.headers,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Auth-Token': config.storeApiToken,
|
|
||||||
'X-Auth-Client': config.storeApiClientId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
throw new BigcommerceNetworkError(
|
|
||||||
`Fetch to Bigcommerce failed: ${error.message}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentType = res.headers.get('Content-Type')
|
|
||||||
const isJSON = contentType?.includes('application/json')
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const data = isJSON ? await res.json() : await getTextOrNull(res)
|
|
||||||
const headers = getRawHeaders(res)
|
|
||||||
const msg = `Big Commerce API error (${
|
|
||||||
res.status
|
|
||||||
}) \nHeaders: ${JSON.stringify(headers, null, 2)}\n${
|
|
||||||
typeof data === 'string' ? data : JSON.stringify(data, null, 2)
|
|
||||||
}`
|
|
||||||
|
|
||||||
throw new BigcommerceApiError(msg, res, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.status !== 204 && !isJSON) {
|
|
||||||
throw new BigcommerceApiError(
|
|
||||||
`Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`,
|
|
||||||
res
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If something was removed, the response will be empty
|
|
||||||
return res.status === 204 ? null : await res.json()
|
|
||||||
}
|
|
||||||
export default fetchStoreApi
|
|
||||||
|
|
||||||
function getRawHeaders(res: Response) {
|
|
||||||
const headers: { [key: string]: string } = {}
|
|
||||||
|
|
||||||
res.headers.forEach((value, key) => {
|
|
||||||
headers[key] = value
|
|
||||||
})
|
|
||||||
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTextOrNull(res: Response) {
|
|
||||||
try {
|
|
||||||
return res.text()
|
|
||||||
} catch (err) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
import zeitFetch from '@vercel/fetch'
|
|
||||||
|
|
||||||
export default zeitFetch()
|
|
@ -1,5 +0,0 @@
|
|||||||
export default function filterEdges<T>(
|
|
||||||
edges: (T | null | undefined)[] | null | undefined
|
|
||||||
) {
|
|
||||||
return edges?.filter((edge): edge is T => !!edge) ?? []
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { serialize, CookieSerializeOptions } from 'cookie'
|
|
||||||
|
|
||||||
export default function getCartCookie(
|
|
||||||
name: string,
|
|
||||||
cartId?: string,
|
|
||||||
maxAge?: number
|
|
||||||
) {
|
|
||||||
const options: CookieSerializeOptions =
|
|
||||||
cartId && maxAge
|
|
||||||
? {
|
|
||||||
maxAge,
|
|
||||||
expires: new Date(Date.now() + maxAge * 1000),
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
path: '/',
|
|
||||||
sameSite: 'lax',
|
|
||||||
}
|
|
||||||
: { maxAge: -1, path: '/' } // Removes the cookie
|
|
||||||
|
|
||||||
return serialize(name, cartId || '', options)
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
import type { GetCustomerIdQuery } from '../../schema'
|
|
||||||
import type { BigcommerceConfig } from '../'
|
|
||||||
|
|
||||||
export const getCustomerIdQuery = /* GraphQL */ `
|
|
||||||
query getCustomerId {
|
|
||||||
customer {
|
|
||||||
entityId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
async function getCustomerId({
|
|
||||||
customerToken,
|
|
||||||
config,
|
|
||||||
}: {
|
|
||||||
customerToken: string
|
|
||||||
config: BigcommerceConfig
|
|
||||||
}): Promise<string | undefined> {
|
|
||||||
const { data } = await config.fetch<GetCustomerIdQuery>(
|
|
||||||
getCustomerIdQuery,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
cookie: `${config.customerCookie}=${customerToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return String(data?.customer?.entityId)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getCustomerId
|
|
@ -1,28 +0,0 @@
|
|||||||
import type { WishlistItemBody } from '../../types/wishlist'
|
|
||||||
import type { CartItemBody, OptionSelections } from '../../types/cart'
|
|
||||||
|
|
||||||
type BCWishlistItemBody = {
|
|
||||||
product_id: number
|
|
||||||
variant_id: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type BCCartItemBody = {
|
|
||||||
product_id: number
|
|
||||||
variant_id: number
|
|
||||||
quantity?: number
|
|
||||||
option_selections?: OptionSelections[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseWishlistItem = (
|
|
||||||
item: WishlistItemBody
|
|
||||||
): BCWishlistItemBody => ({
|
|
||||||
product_id: Number(item.productId),
|
|
||||||
variant_id: Number(item.variantId),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const parseCartItem = (item: CartItemBody): BCCartItemBody => ({
|
|
||||||
quantity: item.quantity,
|
|
||||||
product_id: Number(item.productId),
|
|
||||||
variant_id: Number(item.variantId),
|
|
||||||
option_selections: item.optionSelections,
|
|
||||||
})
|
|
@ -1,21 +0,0 @@
|
|||||||
import type { ProductNode } from '../operations/get-all-products'
|
|
||||||
import type { RecursivePartial } from './types'
|
|
||||||
|
|
||||||
export default function setProductLocaleMeta(
|
|
||||||
node: RecursivePartial<ProductNode>
|
|
||||||
) {
|
|
||||||
if (node.localeMeta?.edges) {
|
|
||||||
node.localeMeta.edges = node.localeMeta.edges.filter((edge) => {
|
|
||||||
const { key, value } = edge?.node ?? {}
|
|
||||||
if (key && key in node) {
|
|
||||||
;(node as any)[key] = value
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!node.localeMeta.edges.length) {
|
|
||||||
delete node.localeMeta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
export type RecursivePartial<T> = {
|
|
||||||
[P in keyof T]?: RecursivePartial<T[P]>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RecursiveRequired<T> = {
|
|
||||||
[P in keyof T]-?: RecursiveRequired<T[P]>
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
export { default as useLogin } from './use-login'
|
|
||||||
export { default as useLogout } from './use-logout'
|
|
||||||
export { default as useSignup } from './use-signup'
|
|
@ -1,40 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import type { MutationHook } from '@commerce/utils/types'
|
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
|
||||||
import useLogin, { UseLogin } from '@commerce/auth/use-login'
|
|
||||||
import type { LoginHook } from '../types/login'
|
|
||||||
import useCustomer from '../customer/use-customer'
|
|
||||||
|
|
||||||
export default useLogin as UseLogin<typeof handler>
|
|
||||||
|
|
||||||
export const handler: MutationHook<LoginHook> = {
|
|
||||||
fetchOptions: {
|
|
||||||
url: '/api/login',
|
|
||||||
method: 'POST',
|
|
||||||
},
|
|
||||||
async fetcher({ input: { email, password }, options, fetch }) {
|
|
||||||
if (!(email && password)) {
|
|
||||||
throw new CommerceError({
|
|
||||||
message:
|
|
||||||
'An email and password are required to login',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetch({
|
|
||||||
...options,
|
|
||||||
body: { email, password },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
useHook: ({ fetch }) => () => {
|
|
||||||
const { revalidate } = useCustomer()
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
async function login(input) {
|
|
||||||
const data = await fetch({ input })
|
|
||||||
await revalidate()
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
[fetch, revalidate]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import type { MutationHook } from '@commerce/utils/types'
|
|
||||||
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
|
|
||||||
import type { LogoutHook } from '../types/logout'
|
|
||||||
import useCustomer from '../customer/use-customer'
|
|
||||||
|
|
||||||
export default useLogout as UseLogout<typeof handler>
|
|
||||||
|
|
||||||
export const handler: MutationHook<LogoutHook> = {
|
|
||||||
fetchOptions: {
|
|
||||||
url: '/api/logout',
|
|
||||||
method: 'GET',
|
|
||||||
},
|
|
||||||
useHook: ({ fetch }) => () => {
|
|
||||||
const { mutate } = useCustomer()
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
async function logout() {
|
|
||||||
const data = await fetch()
|
|
||||||
await mutate(null, false)
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
[fetch, mutate]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import type { MutationHook } from '@commerce/utils/types'
|
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
|
||||||
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
|
|
||||||
import type { SignupHook } from '../types/signup'
|
|
||||||
import useCustomer from '../customer/use-customer'
|
|
||||||
|
|
||||||
export default useSignup as UseSignup<typeof handler>
|
|
||||||
|
|
||||||
export const handler: MutationHook<SignupHook> = {
|
|
||||||
fetchOptions: {
|
|
||||||
url: '/api/signup',
|
|
||||||
method: 'POST',
|
|
||||||
},
|
|
||||||
async fetcher({
|
|
||||||
input: { firstName, lastName, email, password },
|
|
||||||
options,
|
|
||||||
fetch,
|
|
||||||
}) {
|
|
||||||
if (!(firstName && lastName && email && password)) {
|
|
||||||
throw new CommerceError({
|
|
||||||
message:
|
|
||||||
'A first name, last name, email and password are required to signup',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetch({
|
|
||||||
...options,
|
|
||||||
body: { firstName, lastName, email, password },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
useHook: ({ fetch }) => () => {
|
|
||||||
const { revalidate } = useCustomer()
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
async function signup(input) {
|
|
||||||
const data = await fetch({ input })
|
|
||||||
await revalidate()
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
[fetch, revalidate]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export { default as useCart } from './use-cart'
|
|
||||||
export { default as useAddItem } from './use-add-item'
|
|
||||||
export { default as useRemoveItem } from './use-remove-item'
|
|
||||||
export { default as useUpdateItem } from './use-update-item'
|
|
@ -1,44 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import type { MutationHook } from '@commerce/utils/types'
|
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
|
||||||
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
|
|
||||||
import type { AddItemHook } from '@commerce/types/cart'
|
|
||||||
import useCart from './use-cart'
|
|
||||||
|
|
||||||
export default useAddItem as UseAddItem<typeof handler>
|
|
||||||
|
|
||||||
export const handler: MutationHook<AddItemHook> = {
|
|
||||||
fetchOptions: {
|
|
||||||
url: '/api/cart',
|
|
||||||
method: 'POST',
|
|
||||||
},
|
|
||||||
async fetcher({ input: item, options, fetch }) {
|
|
||||||
if (
|
|
||||||
item.quantity &&
|
|
||||||
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
|
||||||
) {
|
|
||||||
throw new CommerceError({
|
|
||||||
message: 'The item quantity has to be a valid integer greater than 0',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await fetch({
|
|
||||||
...options,
|
|
||||||
body: { item },
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
useHook: ({ fetch }) => () => {
|
|
||||||
const { mutate } = useCart()
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
async function addItem(input) {
|
|
||||||
const data = await fetch({ input })
|
|
||||||
await mutate(data, false)
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
[fetch, mutate]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
import { useMemo } from 'react'
|
|
||||||
import { SWRHook } from '@commerce/utils/types'
|
|
||||||
import useCart, { UseCart } from '@commerce/cart/use-cart'
|
|
||||||
import type { GetCartHook } from '@commerce/types/cart'
|
|
||||||
|
|
||||||
export default useCart as UseCart<typeof handler>
|
|
||||||
|
|
||||||
export const handler: SWRHook<GetCartHook> = {
|
|
||||||
fetchOptions: {
|
|
||||||
url: '/api/cart',
|
|
||||||
method: 'GET',
|
|
||||||
},
|
|
||||||
useHook: ({ useData }) => (input) => {
|
|
||||||
const response = useData({
|
|
||||||
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
|
|
||||||
})
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() =>
|
|
||||||
Object.create(response, {
|
|
||||||
isEmpty: {
|
|
||||||
get() {
|
|
||||||
return (response.data?.lineItems.length ?? 0) <= 0
|
|
||||||
},
|
|
||||||
enumerable: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[response]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import type {
|
|
||||||
MutationHookContext,
|
|
||||||
HookFetcherContext,
|
|
||||||
} from '@commerce/utils/types'
|
|
||||||
import { ValidationError } from '@commerce/utils/errors'
|
|
||||||
import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
|
|
||||||
import type { Cart, LineItem, RemoveItemHook } from '@commerce/types/cart'
|
|
||||||
import useCart from './use-cart'
|
|
||||||
|
|
||||||
export type RemoveItemFn<T = any> = T extends LineItem
|
|
||||||
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
|
|
||||||
: (input: RemoveItemActionInput<T>) => Promise<Cart | null>
|
|
||||||
|
|
||||||
export type RemoveItemActionInput<T = any> = T extends LineItem
|
|
||||||
? Partial<RemoveItemHook['actionInput']>
|
|
||||||
: RemoveItemHook['actionInput']
|
|
||||||
|
|
||||||
export default useRemoveItem as UseRemoveItem<typeof handler>
|
|
||||||
|
|
||||||
export const handler = {
|
|
||||||
fetchOptions: {
|
|
||||||
url: '/api/cart',
|
|
||||||
method: 'DELETE',
|
|
||||||
},
|
|
||||||
async fetcher({
|
|
||||||
input: { itemId },
|
|
||||||
options,
|
|
||||||
fetch,
|
|
||||||
}: HookFetcherContext<RemoveItemHook>) {
|
|
||||||
return await fetch({ ...options, body: { itemId } })
|
|
||||||
},
|
|
||||||
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) => <
|
|
||||||
T extends LineItem | undefined = undefined
|
|
||||||
>(
|
|
||||||
ctx: { item?: T } = {}
|
|
||||||
) => {
|
|
||||||
const { item } = ctx
|
|
||||||
const { mutate } = useCart()
|
|
||||||
const removeItem: RemoveItemFn<LineItem> = async (input) => {
|
|
||||||
const itemId = input?.id ?? item?.id
|
|
||||||
|
|
||||||
if (!itemId) {
|
|
||||||
throw new ValidationError({
|
|
||||||
message: 'Invalid input used for this operation',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await fetch({ input: { itemId } })
|
|
||||||
await mutate(data, false)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import debounce from 'lodash.debounce'
|
|
||||||
import type {
|
|
||||||
MutationHookContext,
|
|
||||||
HookFetcherContext,
|
|
||||||
} from '@commerce/utils/types'
|
|
||||||
import { ValidationError } from '@commerce/utils/errors'
|
|
||||||
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
|
|
||||||
import type { LineItem, UpdateItemHook } from '@commerce/types/cart'
|
|
||||||
import { handler as removeItemHandler } from './use-remove-item'
|
|
||||||
import useCart from './use-cart'
|
|
||||||
|
|
||||||
export type UpdateItemActionInput<T = any> = T extends LineItem
|
|
||||||
? Partial<UpdateItemHook['actionInput']>
|
|
||||||
: UpdateItemHook['actionInput']
|
|
||||||
|
|
||||||
export default useUpdateItem as UseUpdateItem<typeof handler>
|
|
||||||
|
|
||||||
export const handler = {
|
|
||||||
fetchOptions: {
|
|
||||||
url: '/api/cart',
|
|
||||||
method: 'PUT',
|
|
||||||
},
|
|
||||||
async fetcher({
|
|
||||||
input: { itemId, item },
|
|
||||||
options,
|
|
||||||
fetch,
|
|
||||||
}: HookFetcherContext<UpdateItemHook>) {
|
|
||||||
if (Number.isInteger(item.quantity)) {
|
|
||||||
// Also allow the update hook to remove an item if the quantity is lower than 1
|
|
||||||
if (item.quantity! < 1) {
|
|
||||||
return removeItemHandler.fetcher({
|
|
||||||
options: removeItemHandler.fetchOptions,
|
|
||||||
input: { itemId },
|
|
||||||
fetch,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if (item.quantity) {
|
|
||||||
throw new ValidationError({
|
|
||||||
message: 'The item quantity has to be a valid integer',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return await fetch({
|
|
||||||
...options,
|
|
||||||
body: { itemId, item },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) => <
|
|
||||||
T extends LineItem | undefined = undefined
|
|
||||||
>(
|
|
||||||
ctx: {
|
|
||||||
item?: T
|
|
||||||
wait?: number
|
|
||||||
} = {}
|
|
||||||
) => {
|
|
||||||
const { item } = ctx
|
|
||||||
const { mutate } = useCart() as any
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
debounce(async (input: UpdateItemActionInput<T>) => {
|
|
||||||
const itemId = input.id ?? item?.id
|
|
||||||
const productId = input.productId ?? item?.productId
|
|
||||||
const variantId = input.productId ?? item?.variantId
|
|
||||||
|
|
||||||
if (!itemId || !productId || !variantId) {
|
|
||||||
throw new ValidationError({
|
|
||||||
message: 'Invalid input used for this operation',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await fetch({
|
|
||||||
input: {
|
|
||||||
itemId,
|
|
||||||
item: { productId, variantId, quantity: input.quantity },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await mutate(data, false)
|
|
||||||
return data
|
|
||||||
}, ctx.wait ?? 500),
|
|
||||||
[fetch, mutate]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"provider": "bigcommerce",
|
|
||||||
"features": {
|
|
||||||
"wishlist": true,
|
|
||||||
"customerAuth": true
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export { default as useCustomer } from './use-customer'
|
|
@ -1,24 +0,0 @@
|
|||||||
import { SWRHook } from '@commerce/utils/types'
|
|
||||||
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
|
|
||||||
import type { CustomerHook } from '../types/customer'
|
|
||||||
|
|
||||||
export default useCustomer as UseCustomer<typeof handler>
|
|
||||||
|
|
||||||
export const handler: SWRHook<CustomerHook> = {
|
|
||||||
fetchOptions: {
|
|
||||||
url: '/api/customer',
|
|
||||||
method: 'GET',
|
|
||||||
},
|
|
||||||
async fetcher({ options, fetch }) {
|
|
||||||
const data = await fetch(options)
|
|
||||||
return data?.customer ?? null
|
|
||||||
},
|
|
||||||
useHook: ({ useData }) => (input) => {
|
|
||||||
return useData({
|
|
||||||
swrOptions: {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
...input?.swrOptions,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
import { FetcherError } from '@commerce/utils/errors'
|
|
||||||
import type { Fetcher } from '@commerce/utils/types'
|
|
||||||
|
|
||||||
async function getText(res: Response) {
|
|
||||||
try {
|
|
||||||
return (await res.text()) || res.statusText
|
|
||||||
} catch (error) {
|
|
||||||
return res.statusText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getError(res: Response) {
|
|
||||||
if (res.headers.get('Content-Type')?.includes('application/json')) {
|
|
||||||
const data = await res.json()
|
|
||||||
return new FetcherError({ errors: data.errors, status: res.status })
|
|
||||||
}
|
|
||||||
return new FetcherError({ message: await getText(res), status: res.status })
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetcher: Fetcher = async ({
|
|
||||||
url,
|
|
||||||
method = 'GET',
|
|
||||||
variables,
|
|
||||||
body: bodyObj,
|
|
||||||
}) => {
|
|
||||||
const hasBody = Boolean(variables || bodyObj)
|
|
||||||
const body = hasBody
|
|
||||||
? JSON.stringify(variables ? { variables } : bodyObj)
|
|
||||||
: undefined
|
|
||||||
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
|
|
||||||
const res = await fetch(url!, { method, body, headers })
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const { data } = await res.json()
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
throw await getError(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default fetcher
|
|
@ -1,36 +0,0 @@
|
|||||||
import type { ReactNode } from 'react'
|
|
||||||
import {
|
|
||||||
CommerceConfig,
|
|
||||||
CommerceProvider as CoreCommerceProvider,
|
|
||||||
useCommerce as useCoreCommerce,
|
|
||||||
} from '@commerce'
|
|
||||||
import { bigcommerceProvider } from './provider'
|
|
||||||
import type { BigcommerceProvider } from './provider'
|
|
||||||
|
|
||||||
export { bigcommerceProvider }
|
|
||||||
export type { BigcommerceProvider }
|
|
||||||
|
|
||||||
export const bigcommerceConfig: CommerceConfig = {
|
|
||||||
locale: 'en-us',
|
|
||||||
cartCookie: 'bc_cartId',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BigcommerceConfig = Partial<CommerceConfig>
|
|
||||||
|
|
||||||
export type BigcommerceProps = {
|
|
||||||
children?: ReactNode
|
|
||||||
locale: string
|
|
||||||
} & BigcommerceConfig
|
|
||||||
|
|
||||||
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
|
||||||
return (
|
|
||||||
<CoreCommerceProvider
|
|
||||||
provider={bigcommerceProvider}
|
|
||||||
config={{ ...bigcommerceConfig, ...config }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</CoreCommerceProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()
|
|
@ -1,5 +0,0 @@
|
|||||||
// Remove trailing and leading slash, usually included in nodes
|
|
||||||
// returned by the BigCommerce API
|
|
||||||
const getSlug = (path: string) => path.replace(/^\/|\/$/g, '')
|
|
||||||
|
|
||||||
export default getSlug
|
|
@ -1,13 +0,0 @@
|
|||||||
import update, { Context } from 'immutability-helper'
|
|
||||||
|
|
||||||
const c = new Context()
|
|
||||||
|
|
||||||
c.extend('$auto', function (value, object) {
|
|
||||||
return object ? c.update(object, value) : c.update({}, value)
|
|
||||||
})
|
|
||||||
|
|
||||||
c.extend('$autoArray', function (value, object) {
|
|
||||||
return object ? c.update(object, value) : c.update([], value)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default c.update
|
|
@ -1,136 +0,0 @@
|
|||||||
import type { Product } from '../types/product'
|
|
||||||
import type { Cart, BigcommerceCart, LineItem } from '../types/cart'
|
|
||||||
import type { Page } from '../types/page'
|
|
||||||
import type { BCCategory, Category } from '../types/site'
|
|
||||||
import { definitions } from '../api/definitions/store-content'
|
|
||||||
import update from './immutability'
|
|
||||||
import getSlug from './get-slug'
|
|
||||||
|
|
||||||
function normalizeProductOption(productOption: any) {
|
|
||||||
const {
|
|
||||||
node: {
|
|
||||||
entityId,
|
|
||||||
values: { edges = [] } = {},
|
|
||||||
...rest
|
|
||||||
},
|
|
||||||
} = productOption
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: entityId,
|
|
||||||
values: edges?.map(({ node }: any) => node),
|
|
||||||
...rest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeProduct(productNode: any): Product {
|
|
||||||
const {
|
|
||||||
entityId: id,
|
|
||||||
productOptions,
|
|
||||||
prices,
|
|
||||||
path,
|
|
||||||
id: _,
|
|
||||||
options: _0,
|
|
||||||
} = productNode
|
|
||||||
|
|
||||||
return update(productNode, {
|
|
||||||
id: { $set: String(id) },
|
|
||||||
images: {
|
|
||||||
$apply: ({ edges }: any) =>
|
|
||||||
edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
|
||||||
url: urlOriginal,
|
|
||||||
alt: altText,
|
|
||||||
...rest,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
$apply: ({ edges }: any) =>
|
|
||||||
edges?.map(({ node: { entityId, productOptions, ...rest } }: any) => ({
|
|
||||||
id: entityId,
|
|
||||||
options: productOptions?.edges
|
|
||||||
? productOptions.edges.map(normalizeProductOption)
|
|
||||||
: [],
|
|
||||||
...rest,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
$set: productOptions.edges
|
|
||||||
? productOptions?.edges.map(normalizeProductOption)
|
|
||||||
: [],
|
|
||||||
},
|
|
||||||
brand: {
|
|
||||||
$apply: (brand: any) => (brand?.entityId ? brand?.entityId : null),
|
|
||||||
},
|
|
||||||
slug: {
|
|
||||||
$set: path?.replace(/^\/+|\/+$/g, ''),
|
|
||||||
},
|
|
||||||
price: {
|
|
||||||
$set: {
|
|
||||||
value: prices?.price.value,
|
|
||||||
currencyCode: prices?.price.currencyCode,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
$unset: ['entityId'],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizePage(page: definitions['page_Full']): Page {
|
|
||||||
return {
|
|
||||||
id: String(page.id),
|
|
||||||
name: page.name,
|
|
||||||
is_visible: page.is_visible,
|
|
||||||
sort_order: page.sort_order,
|
|
||||||
body: page.body,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeCart(data: BigcommerceCart): Cart {
|
|
||||||
return {
|
|
||||||
id: data.id,
|
|
||||||
customerId: String(data.customer_id),
|
|
||||||
email: data.email,
|
|
||||||
createdAt: data.created_time,
|
|
||||||
currency: data.currency,
|
|
||||||
taxesIncluded: data.tax_included,
|
|
||||||
lineItems: data.line_items.physical_items.map(normalizeLineItem),
|
|
||||||
lineItemsSubtotalPrice: data.base_amount,
|
|
||||||
subtotalPrice: data.base_amount + data.discount_amount,
|
|
||||||
totalPrice: data.cart_amount,
|
|
||||||
discounts: data.discounts?.map((discount) => ({
|
|
||||||
value: discount.discounted_amount,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeLineItem(item: any): LineItem {
|
|
||||||
return {
|
|
||||||
id: item.id,
|
|
||||||
variantId: String(item.variant_id),
|
|
||||||
productId: String(item.product_id),
|
|
||||||
name: item.name,
|
|
||||||
quantity: item.quantity,
|
|
||||||
variant: {
|
|
||||||
id: String(item.variant_id),
|
|
||||||
sku: item.sku,
|
|
||||||
name: item.name,
|
|
||||||
image: {
|
|
||||||
url: item.image_url,
|
|
||||||
},
|
|
||||||
requiresShipping: item.is_require_shipping,
|
|
||||||
price: item.sale_price,
|
|
||||||
listPrice: item.list_price,
|
|
||||||
},
|
|
||||||
path: item.url.split('/')[3],
|
|
||||||
discounts: item.discounts.map((discount: any) => ({
|
|
||||||
value: discount.discounted_amount,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeCategory(category: BCCategory): Category {
|
|
||||||
return {
|
|
||||||
id: `${category.entityId}`,
|
|
||||||
name: category.name,
|
|
||||||
slug: getSlug(category.path),
|
|
||||||
path: category.path,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
const commerce = require('./commerce.config.json')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
commerce,
|
|
||||||
images: {
|
|
||||||
domains: ['cdn11.bigcommerce.com'],
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
export { default as usePrice } from './use-price'
|
|
||||||
export { default as useSearch } from './use-search'
|
|
@ -1,2 +0,0 @@
|
|||||||
export * from '@commerce/product/use-price'
|
|
||||||
export { default } from '@commerce/product/use-price'
|
|
@ -1,50 +0,0 @@
|
|||||||
import { SWRHook } from '@commerce/utils/types'
|
|
||||||
import useSearch, { UseSearch } from '@commerce/product/use-search'
|
|
||||||
import type { SearchProductsHook } from '../types/product'
|
|
||||||
|
|
||||||
export default useSearch as UseSearch<typeof handler>
|
|
||||||
|
|
||||||
export type SearchProductsInput = {
|
|
||||||
search?: string
|
|
||||||
categoryId?: number | string
|
|
||||||
brandId?: number
|
|
||||||
sort?: string
|
|
||||||
locale?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handler: SWRHook<SearchProductsHook> = {
|
|
||||||
fetchOptions: {
|
|
||||||
url: '/api/catalog/products',
|
|
||||||
method: 'GET',
|
|
||||||
},
|
|
||||||
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
|
|
||||||
// Use a dummy base as we only care about the relative path
|
|
||||||
const url = new URL(options.url!, 'http://a')
|
|
||||||
|
|
||||||
if (search) url.searchParams.set('search', search)
|
|
||||||
if (Number.isInteger(Number(categoryId)))
|
|
||||||
url.searchParams.set('categoryId', String(categoryId))
|
|
||||||
if (Number.isInteger(brandId))
|
|
||||||
url.searchParams.set('brandId', String(brandId))
|
|
||||||
if (sort) url.searchParams.set('sort', sort)
|
|
||||||
|
|
||||||
return fetch({
|
|
||||||
url: url.pathname + url.search,
|
|
||||||
method: options.method,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
useHook: ({ useData }) => (input = {}) => {
|
|
||||||
return useData({
|
|
||||||
input: [
|
|
||||||
['search', input.search],
|
|
||||||
['categoryId', input.categoryId],
|
|
||||||
['brandId', input.brandId],
|
|
||||||
['sort', input.sort],
|
|
||||||
],
|
|
||||||
swrOptions: {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
...input.swrOptions,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import { handler as useCart } from './cart/use-cart'
|
|
||||||
import { handler as useAddItem } from './cart/use-add-item'
|
|
||||||
import { handler as useUpdateItem } from './cart/use-update-item'
|
|
||||||
import { handler as useRemoveItem } from './cart/use-remove-item'
|
|
||||||
|
|
||||||
import { handler as useWishlist } from './wishlist/use-wishlist'
|
|
||||||
import { handler as useWishlistAddItem } from './wishlist/use-add-item'
|
|
||||||
import { handler as useWishlistRemoveItem } from './wishlist/use-remove-item'
|
|
||||||
|
|
||||||
import { handler as useCustomer } from './customer/use-customer'
|
|
||||||
import { handler as useSearch } from './product/use-search'
|
|
||||||
|
|
||||||
import { handler as useLogin } from './auth/use-login'
|
|
||||||
import { handler as useLogout } from './auth/use-logout'
|
|
||||||
import { handler as useSignup } from './auth/use-signup'
|
|
||||||
|
|
||||||
import fetcher from './fetcher'
|
|
||||||
|
|
||||||
export const bigcommerceProvider = {
|
|
||||||
locale: 'en-us',
|
|
||||||
cartCookie: 'bc_cartId',
|
|
||||||
fetcher,
|
|
||||||
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
|
||||||
wishlist: {
|
|
||||||
useWishlist,
|
|
||||||
useAddItem: useWishlistAddItem,
|
|
||||||
useRemoveItem: useWishlistRemoveItem,
|
|
||||||
},
|
|
||||||
customer: { useCustomer },
|
|
||||||
products: { useSearch },
|
|
||||||
auth: { useLogin, useLogout, useSignup },
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BigcommerceProvider = typeof bigcommerceProvider
|
|
2064
framework/bigcommerce/schema.d.ts
vendored
2064
framework/bigcommerce/schema.d.ts
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Generates definitions for REST API endpoints that are being
|
|
||||||
* used by ../api using https://github.com/drwpow/swagger-to-ts
|
|
||||||
*/
|
|
||||||
const { readFileSync, promises } = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
const fetch = require('node-fetch')
|
|
||||||
const swaggerToTS = require('@manifoldco/swagger-to-ts').default
|
|
||||||
|
|
||||||
async function getSchema(filename) {
|
|
||||||
const url = `https://next-api.stoplight.io/projects/8433/files/${filename}`
|
|
||||||
const res = await fetch(url)
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`Request failed with ${res.status}: ${res.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
const schemas = Object.entries({
|
|
||||||
'../api/definitions/catalog.ts':
|
|
||||||
'BigCommerce_Catalog_API.oas2.yml?ref=version%2F20.930',
|
|
||||||
'../api/definitions/store-content.ts':
|
|
||||||
'BigCommerce_Store_Content_API.oas2.yml?ref=version%2F20.930',
|
|
||||||
'../api/definitions/wishlist.ts':
|
|
||||||
'BigCommerce_Wishlist_API.oas2.yml?ref=version%2F20.930',
|
|
||||||
// swagger-to-ts is not working for the schema of the cart API
|
|
||||||
// '../api/definitions/cart.ts':
|
|
||||||
// 'BigCommerce_Server_to_Server_Cart_API.oas2.yml',
|
|
||||||
})
|
|
||||||
|
|
||||||
async function writeDefinitions() {
|
|
||||||
const ops = schemas.map(async ([dest, filename]) => {
|
|
||||||
const destination = path.join(__dirname, dest)
|
|
||||||
const schema = await getSchema(filename)
|
|
||||||
const definition = swaggerToTS(schema.content, {
|
|
||||||
prettierConfig: 'package.json',
|
|
||||||
})
|
|
||||||
|
|
||||||
await promises.writeFile(destination, definition)
|
|
||||||
|
|
||||||
console.log(`✔️ Added definitions for: ${dest}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(ops)
|
|
||||||
}
|
|
||||||
|
|
||||||
writeDefinitions()
|
|
@ -1,66 +0,0 @@
|
|||||||
import * as Core from '@commerce/types/cart'
|
|
||||||
|
|
||||||
export * from '@commerce/types/cart'
|
|
||||||
|
|
||||||
// TODO: this type should match:
|
|
||||||
// https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses
|
|
||||||
export type BigcommerceCart = {
|
|
||||||
id: string
|
|
||||||
parent_id?: string
|
|
||||||
customer_id: number
|
|
||||||
email: string
|
|
||||||
currency: { code: string }
|
|
||||||
tax_included: boolean
|
|
||||||
base_amount: number
|
|
||||||
discount_amount: number
|
|
||||||
cart_amount: number
|
|
||||||
line_items: {
|
|
||||||
custom_items: any[]
|
|
||||||
digital_items: any[]
|
|
||||||
gift_certificates: any[]
|
|
||||||
physical_items: any[]
|
|
||||||
}
|
|
||||||
created_time: string
|
|
||||||
discounts?: { id: number; discounted_amount: number }[]
|
|
||||||
// TODO: add missing fields
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extend core cart types
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type Cart = Core.Cart & {
|
|
||||||
lineItems: Core.LineItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OptionSelections = {
|
|
||||||
option_id: number
|
|
||||||
option_value: number | string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CartItemBody = Core.CartItemBody & {
|
|
||||||
productId: string // The product id is always required for BC
|
|
||||||
optionSelections?: OptionSelections[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CartTypes = {
|
|
||||||
cart: Cart
|
|
||||||
item: Core.LineItem
|
|
||||||
itemBody: CartItemBody
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CartHooks = Core.CartHooks<CartTypes>
|
|
||||||
|
|
||||||
export type GetCartHook = CartHooks['getCart']
|
|
||||||
export type AddItemHook = CartHooks['addItem']
|
|
||||||
export type UpdateItemHook = CartHooks['updateItem']
|
|
||||||
export type RemoveItemHook = CartHooks['removeItem']
|
|
||||||
|
|
||||||
export type CartSchema = Core.CartSchema<CartTypes>
|
|
||||||
|
|
||||||
export type CartHandlers = Core.CartHandlers<CartTypes>
|
|
||||||
|
|
||||||
export type GetCartHandler = CartHandlers['getCart']
|
|
||||||
export type AddItemHandler = CartHandlers['addItem']
|
|
||||||
export type UpdateItemHandler = CartHandlers['updateItem']
|
|
||||||
export type RemoveItemHandler = CartHandlers['removeItem']
|
|
@ -1 +0,0 @@
|
|||||||
export * from '@commerce/types/checkout'
|
|
@ -1 +0,0 @@
|
|||||||
export * from '@commerce/types/common'
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user