forked from crowetic/commerce
Merge branch 'agnostic' of github.com:vercel/commerce into agnostic
This commit is contained in:
commit
6cacfec8c7
@ -90,8 +90,8 @@ Our commitment to Open Source can be found [here](https://vercel.com/oss).
|
|||||||
5. Duplicate `.env.template` and rename it to `.env.local`.
|
5. Duplicate `.env.template` and rename it to `.env.local`.
|
||||||
6. Add proper store values to `.env.local`.
|
6. Add proper store values to `.env.local`.
|
||||||
7. Run `yarn dev` to build and watch for code changes
|
7. Run `yarn dev` to build and watch for code changes
|
||||||
8. The development branch is `development` (this is the branch pull requests should be made against).
|
8. The development branch is `canary` (this is the branch pull requests should be made against).
|
||||||
On a release, `develop` branch is rebased into `master`.
|
On a release, `canary` branch is rebased into `master`.
|
||||||
|
|
||||||
## Framework
|
## Framework
|
||||||
|
|
||||||
|
@ -102,8 +102,6 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.animated {
|
.animated {
|
||||||
-webkit-animation-duration: 1s;
|
|
||||||
animation-duration: 1s;
|
|
||||||
-webkit-animation-duration: 1s;
|
-webkit-animation-duration: 1s;
|
||||||
animation-duration: 1s;
|
animation-duration: 1s;
|
||||||
-webkit-animation-fill-mode: both;
|
-webkit-animation-fill-mode: both;
|
||||||
|
@ -10,7 +10,14 @@ import usePrice from '@framework/product/use-price'
|
|||||||
import useUpdateItem from '@framework/cart/use-update-item'
|
import useUpdateItem from '@framework/cart/use-update-item'
|
||||||
import useRemoveItem from '@framework/cart/use-remove-item'
|
import useRemoveItem from '@framework/cart/use-remove-item'
|
||||||
|
|
||||||
const Item = ({
|
type ItemOption = {
|
||||||
|
name: string
|
||||||
|
nameId: number
|
||||||
|
value: string
|
||||||
|
valueId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const CartItem = ({
|
||||||
item,
|
item,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
...rest
|
...rest
|
||||||
@ -63,7 +70,7 @@ const Item = ({
|
|||||||
try {
|
try {
|
||||||
// If this action succeeds then there's no need to do `setRemoving(true)`
|
// If this action succeeds then there's no need to do `setRemoving(true)`
|
||||||
// because the component will be removed from the view
|
// because the component will be removed from the view
|
||||||
await removeItem({ id: String(item.id) })
|
await removeItem(item)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setRemoving(false)
|
setRemoving(false)
|
||||||
}
|
}
|
||||||
@ -96,14 +103,26 @@ const Item = ({
|
|||||||
<div className="flex-1 flex flex-col text-base">
|
<div className="flex-1 flex flex-col text-base">
|
||||||
<Link href={`/product/${item.path}`}>
|
<Link href={`/product/${item.path}`}>
|
||||||
<span
|
<span
|
||||||
className="font-bold mb-5 text-lg cursor-pointer"
|
className="font-bold text-lg cursor-pointer leading-6"
|
||||||
onClick={() => closeSidebarIfPresent()}
|
onClick={() => closeSidebarIfPresent()}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
{item.options && item.options.length > 0 ? (
|
||||||
<div className="flex items-center">
|
<div className="">
|
||||||
|
{item.options.map((option: ItemOption, i: number) => (
|
||||||
|
<span
|
||||||
|
key={`${item.id}-${option.name}`}
|
||||||
|
className="text-sm font-semibold text-accents-7"
|
||||||
|
>
|
||||||
|
{option.value}
|
||||||
|
{i === item.options.length - 1 ? '' : ', '}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="flex items-center mt-3">
|
||||||
<button type="button" onClick={() => increaseQuantity(-1)}>
|
<button type="button" onClick={() => increaseQuantity(-1)}>
|
||||||
<Minus width={18} height={18} />
|
<Minus width={18} height={18} />
|
||||||
</button>
|
</button>
|
||||||
@ -136,4 +155,4 @@ const Item = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Item
|
export default CartItem
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import cn from 'classnames'
|
|
||||||
import { FC, useState, useMemo, useRef, useEffect } from 'react'
|
import { FC, useState, useMemo, useRef, useEffect } from 'react'
|
||||||
import { getRandomPairOfColors } from '@lib/colors'
|
import { getRandomPairOfColors } from '@lib/colors'
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
@apply flex cursor-pointer px-6 py-3 flex transition ease-in-out duration-150 text-primary leading-6 font-medium items-center;
|
@apply flex cursor-pointer px-6 py-3 transition ease-in-out duration-150 text-primary leading-6 font-medium items-center;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,16 +62,16 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
|
|||||||
<main className="fit">{children}</main>
|
<main className="fit">{children}</main>
|
||||||
<Footer pages={pageProps.pages} />
|
<Footer pages={pageProps.pages} />
|
||||||
|
|
||||||
<Sidebar open={displaySidebar} onClose={closeSidebar}>
|
|
||||||
<CartSidebarView />
|
|
||||||
</Sidebar>
|
|
||||||
|
|
||||||
<Modal open={displayModal} onClose={closeModal}>
|
<Modal open={displayModal} onClose={closeModal}>
|
||||||
{modalView === 'LOGIN_VIEW' && <LoginView />}
|
{modalView === 'LOGIN_VIEW' && <LoginView />}
|
||||||
{modalView === 'SIGNUP_VIEW' && <SignUpView />}
|
{modalView === 'SIGNUP_VIEW' && <SignUpView />}
|
||||||
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
|
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Sidebar open={displaySidebar} onClose={closeSidebar}>
|
||||||
|
<CartSidebarView />
|
||||||
|
</Sidebar>
|
||||||
|
|
||||||
<FeatureBar
|
<FeatureBar
|
||||||
title="This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy."
|
title="This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy."
|
||||||
hide={acceptedCookies}
|
hide={acceptedCookies}
|
||||||
|
@ -12,76 +12,74 @@ interface Props {
|
|||||||
imgProps?: Omit<ImageProps, 'src'>
|
imgProps?: Omit<ImageProps, 'src'>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const placeholderImg = '/product-img-placeholder.svg'
|
||||||
|
|
||||||
const ProductCard: FC<Props> = ({
|
const ProductCard: FC<Props> = ({
|
||||||
className,
|
className,
|
||||||
product,
|
product,
|
||||||
variant,
|
variant,
|
||||||
imgProps,
|
imgProps,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => (
|
||||||
return (
|
<Link href={`/product/${product.slug}`} {...props}>
|
||||||
<Link href={`/product/${product.slug}`} {...props}>
|
<a className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}>
|
||||||
<a
|
{variant === 'slim' ? (
|
||||||
className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}
|
<div className="relative overflow-hidden box-border">
|
||||||
>
|
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
|
||||||
{variant === 'slim' ? (
|
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
|
||||||
<div className="relative overflow-hidden box-border">
|
{product.name}
|
||||||
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
|
</span>
|
||||||
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
|
</div>
|
||||||
{product.name}
|
{product?.images && (
|
||||||
|
<Image
|
||||||
|
quality="85"
|
||||||
|
src={product.images[0].url || placeholderImg}
|
||||||
|
alt={product.name || 'Product Image'}
|
||||||
|
height={320}
|
||||||
|
width={320}
|
||||||
|
layout="fixed"
|
||||||
|
{...imgProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className={s.squareBg} />
|
||||||
|
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
|
||||||
|
<div className="absolute top-0 left-0 pr-16 max-w-full">
|
||||||
|
<h3 className={s.productTitle}>
|
||||||
|
<span>{product.name}</span>
|
||||||
|
</h3>
|
||||||
|
<span className={s.productPrice}>
|
||||||
|
{product.price.value}
|
||||||
|
|
||||||
|
{product.price.currencyCode}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{product?.images && (
|
{/* <WishlistButton
|
||||||
<Image
|
|
||||||
quality="85"
|
|
||||||
alt={product.name}
|
|
||||||
src={product.images[0].url!}
|
|
||||||
height={320}
|
|
||||||
width={320}
|
|
||||||
layout="fixed"
|
|
||||||
{...imgProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className={s.squareBg} />
|
|
||||||
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
|
|
||||||
<div className="absolute top-0 left-0 pr-16 max-w-full">
|
|
||||||
<h3 className={s.productTitle}>
|
|
||||||
<span>{product.name}</span>
|
|
||||||
</h3>
|
|
||||||
<span className={s.productPrice}>
|
|
||||||
{product.price.value}
|
|
||||||
|
|
||||||
{product.price.currencyCode}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{/* <WishlistButton
|
|
||||||
className={s.wishlistButton}
|
className={s.wishlistButton}
|
||||||
productId={product.id}
|
productId={product.id}
|
||||||
variant={product.variants[0]}
|
variant={product.variants[0]}
|
||||||
/> */}
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
<div className={s.imageContainer}>
|
<div className={s.imageContainer}>
|
||||||
{product?.images && (
|
{product?.images && (
|
||||||
<Image
|
<Image
|
||||||
alt={product.name}
|
alt={product.name || 'Product Image'}
|
||||||
className={s.productImage}
|
className={s.productImage}
|
||||||
src={product.images[0].url}
|
src={product.images[0].url || placeholderImg}
|
||||||
height={540}
|
height={540}
|
||||||
width={540}
|
width={540}
|
||||||
quality="85"
|
quality="85"
|
||||||
layout="responsive"
|
layout="responsive"
|
||||||
{...imgProps}
|
{...imgProps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
export default ProductCard
|
export default ProductCard
|
||||||
|
@ -40,10 +40,8 @@ const ProductView: FC<Props> = ({ product }) => {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await addItem({
|
await addItem({
|
||||||
productId: Number(product.id),
|
productId: product.id,
|
||||||
variantId: variant
|
variantId: variant ? variant.id : product.variants[0].id,
|
||||||
? Number(variant.id)
|
|
||||||
: Number(product.variants[0].id),
|
|
||||||
})
|
})
|
||||||
openSidebar()
|
openSidebar()
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
@ -90,6 +90,7 @@ function uiReducer(state: State, action: Action) {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
displayModal: true,
|
displayModal: true,
|
||||||
|
displaySidebar: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'CLOSE_MODAL': {
|
case 'CLOSE_MODAL': {
|
||||||
|
@ -25,8 +25,14 @@ const addItem: CartHandlers['addItem'] = async ({
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
const { data } = cartId
|
const { data } = cartId
|
||||||
? await config.storeApiFetch(`/v3/carts/${cartId}/items`, options)
|
? await config.storeApiFetch(
|
||||||
: await config.storeApiFetch('/v3/carts', options)
|
`/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
|
// Create or update the cart cookie
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import type { BigcommerceCart } from '../../../types'
|
||||||
import { BigcommerceApiError } from '../../utils/errors'
|
import { BigcommerceApiError } from '../../utils/errors'
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
import type { Cart, CartHandlers } from '..'
|
import type { CartHandlers } from '../'
|
||||||
|
|
||||||
// Return current cart info
|
// Return current cart info
|
||||||
const getCart: CartHandlers['getCart'] = async ({
|
const getCart: CartHandlers['getCart'] = async ({
|
||||||
@ -8,11 +9,13 @@ const getCart: CartHandlers['getCart'] = async ({
|
|||||||
body: { cartId },
|
body: { cartId },
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
let result: { data?: Cart } = {}
|
let result: { data?: BigcommerceCart } = {}
|
||||||
|
|
||||||
if (cartId) {
|
if (cartId) {
|
||||||
try {
|
try {
|
||||||
result = await config.storeApiFetch(`/v3/carts/${cartId}`)
|
result = await config.storeApiFetch(
|
||||||
|
`/v3/carts/${cartId}?include=line_items.physical_items.options`
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof BigcommerceApiError && error.status === 404) {
|
if (error instanceof BigcommerceApiError && error.status === 404) {
|
||||||
// Remove the cookie if it exists but the cart wasn't found
|
// Remove the cookie if it exists but the cart wasn't found
|
||||||
|
@ -14,7 +14,7 @@ const removeItem: CartHandlers['removeItem'] = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await config.storeApiFetch<{ data: any } | null>(
|
const result = await config.storeApiFetch<{ data: any } | null>(
|
||||||
`/v3/carts/${cartId}/items/${itemId}`,
|
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
|
||||||
{ method: 'DELETE' }
|
{ method: 'DELETE' }
|
||||||
)
|
)
|
||||||
const data = result?.data ?? null
|
const data = result?.data ?? null
|
||||||
|
@ -14,11 +14,8 @@ const updateItem: CartHandlers['updateItem'] = async ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('ITEM', item)
|
|
||||||
console.log('AFTER', parseCartItem(item))
|
|
||||||
|
|
||||||
const { data } = await config.storeApiFetch(
|
const { data } = await config.storeApiFetch(
|
||||||
`/v3/carts/${cartId}/items/${itemId}`,
|
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
@ -8,38 +8,25 @@ import getCart from './handlers/get-cart'
|
|||||||
import addItem from './handlers/add-item'
|
import addItem from './handlers/add-item'
|
||||||
import updateItem from './handlers/update-item'
|
import updateItem from './handlers/update-item'
|
||||||
import removeItem from './handlers/remove-item'
|
import removeItem from './handlers/remove-item'
|
||||||
import type { Cart, UpdateCartItemHandlerBody } from '../../types'
|
import type {
|
||||||
|
BigcommerceCart,
|
||||||
type OptionSelections = {
|
GetCartHandlerBody,
|
||||||
option_id: Number
|
AddCartItemHandlerBody,
|
||||||
option_value: Number | String
|
UpdateCartItemHandlerBody,
|
||||||
}
|
RemoveCartItemHandlerBody,
|
||||||
|
} from '../../types'
|
||||||
export type ItemBody = {
|
|
||||||
productId: number
|
|
||||||
variantId: number
|
|
||||||
quantity?: number
|
|
||||||
optionSelections?: OptionSelections
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AddItemBody = { item: ItemBody }
|
|
||||||
|
|
||||||
export type RemoveItemBody = { itemId: string }
|
|
||||||
|
|
||||||
export type CartHandlers = {
|
export type CartHandlers = {
|
||||||
getCart: BigcommerceHandler<Cart, { cartId?: string }>
|
getCart: BigcommerceHandler<BigcommerceCart, GetCartHandlerBody>
|
||||||
addItem: BigcommerceHandler<Cart, { cartId?: string } & Partial<AddItemBody>>
|
addItem: BigcommerceHandler<BigcommerceCart, AddCartItemHandlerBody>
|
||||||
updateItem: BigcommerceHandler<Cart, UpdateCartItemHandlerBody>
|
updateItem: BigcommerceHandler<BigcommerceCart, UpdateCartItemHandlerBody>
|
||||||
removeItem: BigcommerceHandler<
|
removeItem: BigcommerceHandler<BigcommerceCart, RemoveCartItemHandlerBody>
|
||||||
Cart,
|
|
||||||
{ cartId?: string } & Partial<RemoveItemBody>
|
|
||||||
>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
|
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
|
||||||
|
|
||||||
// TODO: a complete implementation should have schema validation for `req.body`
|
// TODO: a complete implementation should have schema validation for `req.body`
|
||||||
const cartApi: BigcommerceApiHandler<Cart, CartHandlers> = async (
|
const cartApi: BigcommerceApiHandler<BigcommerceCart, CartHandlers> = async (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
config,
|
config,
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import type { HookFetcher } from '@commerce/utils/types'
|
import type { HookFetcher } from '@commerce/utils/types'
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
import useCartAddItem from '@commerce/cart/use-add-item'
|
import useCartAddItem, {
|
||||||
|
AddItemInput as UseAddItemInput,
|
||||||
|
} from '@commerce/cart/use-add-item'
|
||||||
import { normalizeCart } from '../lib/normalize'
|
import { normalizeCart } from '../lib/normalize'
|
||||||
import type {
|
import type {
|
||||||
ItemBody,
|
AddCartItemBody,
|
||||||
AddItemBody,
|
Cart,
|
||||||
Cart as BigcommerceCart,
|
BigcommerceCart,
|
||||||
} from '../api/cart'
|
CartItemBody,
|
||||||
|
} from '../types'
|
||||||
import useCart from './use-cart'
|
import useCart from './use-cart'
|
||||||
|
|
||||||
const defaultOpts = {
|
const defaultOpts = {
|
||||||
@ -15,9 +18,9 @@ const defaultOpts = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AddItemInput = ItemBody
|
export type AddItemInput = UseAddItemInput<CartItemBody>
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Cart, AddItemBody> = async (
|
export const fetcher: HookFetcher<Cart, AddCartItemBody> = async (
|
||||||
options,
|
options,
|
||||||
{ item },
|
{ item },
|
||||||
fetch
|
fetch
|
||||||
@ -31,7 +34,7 @@ export const fetcher: HookFetcher<Cart, AddItemBody> = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fetch<BigcommerceCart>({
|
const data = await fetch<BigcommerceCart, AddCartItemBody>({
|
||||||
...defaultOpts,
|
...defaultOpts,
|
||||||
...options,
|
...options,
|
||||||
body: { item },
|
body: { item },
|
||||||
|
@ -3,7 +3,7 @@ import type { SwrOptions } from '@commerce/utils/use-data'
|
|||||||
import useResponse from '@commerce/utils/use-response'
|
import useResponse from '@commerce/utils/use-response'
|
||||||
import useCommerceCart, { CartInput } from '@commerce/cart/use-cart'
|
import useCommerceCart, { CartInput } from '@commerce/cart/use-cart'
|
||||||
import { normalizeCart } from '../lib/normalize'
|
import { normalizeCart } from '../lib/normalize'
|
||||||
import type { Cart as BigcommerceCart } from '../api/cart'
|
import type { Cart, BigcommerceCart } from '../types'
|
||||||
|
|
||||||
const defaultOpts = {
|
const defaultOpts = {
|
||||||
url: '/api/bigcommerce/cart',
|
url: '/api/bigcommerce/cart',
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { HookFetcher } from '@commerce/utils/types'
|
import { HookFetcher } from '@commerce/utils/types'
|
||||||
import useCartRemoveItem from '@commerce/cart/use-remove-item'
|
import { ValidationError } from '@commerce/utils/errors'
|
||||||
|
import useCartRemoveItem, {
|
||||||
|
RemoveItemInput as UseRemoveItemInput,
|
||||||
|
} from '@commerce/cart/use-remove-item'
|
||||||
import { normalizeCart } from '../lib/normalize'
|
import { normalizeCart } from '../lib/normalize'
|
||||||
import type { RemoveItemBody, Cart as BigcommerceCart } from '../api/cart'
|
import type {
|
||||||
|
RemoveCartItemBody,
|
||||||
|
Cart,
|
||||||
|
BigcommerceCart,
|
||||||
|
LineItem,
|
||||||
|
} from '../types'
|
||||||
import useCart from './use-cart'
|
import useCart from './use-cart'
|
||||||
|
|
||||||
const defaultOpts = {
|
const defaultOpts = {
|
||||||
@ -10,11 +18,15 @@ const defaultOpts = {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RemoveItemInput = {
|
export type RemoveItemFn<T = any> = T extends LineItem
|
||||||
id: string
|
? (input?: RemoveItemInput<T>) => Promise<Cart | null>
|
||||||
}
|
: (input: RemoveItemInput<T>) => Promise<Cart | null>
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Cart | null, RemoveItemBody> = async (
|
export type RemoveItemInput<T = any> = T extends LineItem
|
||||||
|
? Partial<UseRemoveItemInput>
|
||||||
|
: UseRemoveItemInput
|
||||||
|
|
||||||
|
export const fetcher: HookFetcher<Cart | null, RemoveCartItemBody> = async (
|
||||||
options,
|
options,
|
||||||
{ itemId },
|
{ itemId },
|
||||||
fetch
|
fetch
|
||||||
@ -28,21 +40,29 @@ export const fetcher: HookFetcher<Cart | null, RemoveItemBody> = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
export function extendHook(customFetcher: typeof fetcher) {
|
||||||
const useRemoveItem = (item?: any) => {
|
const useRemoveItem = <T extends LineItem | undefined = undefined>(
|
||||||
|
item?: T
|
||||||
|
) => {
|
||||||
const { mutate } = useCart()
|
const { mutate } = useCart()
|
||||||
const fn = useCartRemoveItem<Cart | null, RemoveItemBody>(
|
const fn = useCartRemoveItem<Cart | null, RemoveCartItemBody>(
|
||||||
defaultOpts,
|
defaultOpts,
|
||||||
customFetcher
|
customFetcher
|
||||||
)
|
)
|
||||||
|
const removeItem: RemoveItemFn<LineItem> = async (input) => {
|
||||||
|
const itemId = input?.id ?? item?.id
|
||||||
|
|
||||||
return useCallback(
|
if (!itemId) {
|
||||||
async function removeItem(input: RemoveItemInput) {
|
throw new ValidationError({
|
||||||
const data = await fn({ itemId: input.id ?? item?.id })
|
message: 'Invalid input used for this operation',
|
||||||
await mutate(data, false)
|
})
|
||||||
return data
|
}
|
||||||
},
|
|
||||||
[fn, mutate]
|
const data = await fn({ itemId })
|
||||||
)
|
await mutate(data, false)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
return useCallback(removeItem as RemoveItemFn<T>, [fn, mutate])
|
||||||
}
|
}
|
||||||
|
|
||||||
useRemoveItem.extend = extendHook
|
useRemoveItem.extend = extendHook
|
||||||
|
@ -2,11 +2,12 @@ import { useCallback } from 'react'
|
|||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
import type { HookFetcher } from '@commerce/utils/types'
|
import type { HookFetcher } from '@commerce/utils/types'
|
||||||
import { ValidationError } from '@commerce/utils/errors'
|
import { ValidationError } from '@commerce/utils/errors'
|
||||||
import useCartUpdateItem from '@commerce/cart/use-update-item'
|
import useCartUpdateItem, {
|
||||||
|
UpdateItemInput as UseUpdateItemInput,
|
||||||
|
} from '@commerce/cart/use-update-item'
|
||||||
import { normalizeCart } from '../lib/normalize'
|
import { normalizeCart } from '../lib/normalize'
|
||||||
import type {
|
import type {
|
||||||
UpdateCartItemBody,
|
UpdateCartItemBody,
|
||||||
UpdateCartItemInput,
|
|
||||||
Cart,
|
Cart,
|
||||||
BigcommerceCart,
|
BigcommerceCart,
|
||||||
LineItem,
|
LineItem,
|
||||||
@ -19,6 +20,10 @@ const defaultOpts = {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdateItemInput<T = any> = T extends LineItem
|
||||||
|
? Partial<UseUpdateItemInput<LineItem>>
|
||||||
|
: UseUpdateItemInput<LineItem>
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Cart | null, UpdateCartItemBody> = async (
|
export const fetcher: HookFetcher<Cart | null, UpdateCartItemBody> = async (
|
||||||
options,
|
options,
|
||||||
{ itemId, item },
|
{ itemId, item },
|
||||||
@ -55,31 +60,24 @@ function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
debounce(
|
debounce(async (input: UpdateItemInput<T>) => {
|
||||||
async (
|
const itemId = input.id ?? item?.id
|
||||||
input: T extends LineItem
|
const productId = input.productId ?? item?.productId
|
||||||
? Partial<UpdateCartItemInput>
|
const variantId = input.productId ?? item?.variantId
|
||||||
: UpdateCartItemInput
|
|
||||||
) => {
|
|
||||||
const itemId = input.id ?? item?.id
|
|
||||||
const productId = input.productId ?? item?.productId
|
|
||||||
const variantId = input.productId ?? item?.variantId
|
|
||||||
|
|
||||||
if (!itemId || !productId || !variantId) {
|
if (!itemId || !productId || !variantId) {
|
||||||
throw new ValidationError({
|
throw new ValidationError({
|
||||||
message: 'Invalid input used for this operation',
|
message: 'Invalid input used for this operation',
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await fn({
|
|
||||||
itemId,
|
|
||||||
item: { productId, variantId, quantity: input.quantity },
|
|
||||||
})
|
})
|
||||||
await mutate(data, false)
|
}
|
||||||
return data
|
|
||||||
},
|
const data = await fn({
|
||||||
cfg?.wait ?? 500
|
itemId,
|
||||||
),
|
item: { productId, variantId, quantity: input.quantity },
|
||||||
|
})
|
||||||
|
await mutate(data, false)
|
||||||
|
return data
|
||||||
|
}, cfg?.wait ?? 500),
|
||||||
[fn, mutate]
|
[fn, mutate]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,7 @@ export const fetcher: HookFetcher<SearchProductsData, SearchProductsInput> = (
|
|||||||
if (search) url.searchParams.set('search', search)
|
if (search) url.searchParams.set('search', search)
|
||||||
if (Number.isInteger(categoryId))
|
if (Number.isInteger(categoryId))
|
||||||
url.searchParams.set('category', String(categoryId))
|
url.searchParams.set('category', String(categoryId))
|
||||||
if (Number.isInteger(categoryId))
|
if (Number.isInteger(brandId)) url.searchParams.set('brand', String(brandId))
|
||||||
url.searchParams.set('brand', String(brandId))
|
|
||||||
if (sort) url.searchParams.set('sort', sort)
|
if (sort) url.searchParams.set('sort', sort)
|
||||||
|
|
||||||
return fetch({
|
return fetch({
|
||||||
|
@ -43,12 +43,20 @@ export interface CartItemBody extends Core.CartItemBody {
|
|||||||
optionSelections?: OptionSelections
|
optionSelections?: OptionSelections
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateCartItemBody extends Core.UpdateCartItemBody {
|
export interface GetCartHandlerBody extends Core.GetCartHandlerBody {}
|
||||||
item: CartItemBody
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateCartItemInput
|
export interface AddCartItemBody extends Core.AddCartItemBody<CartItemBody> {}
|
||||||
extends Core.UpdateCartItemInput<CartItemBody> {}
|
|
||||||
|
export interface AddCartItemHandlerBody
|
||||||
|
extends Core.AddCartItemHandlerBody<CartItemBody> {}
|
||||||
|
|
||||||
|
export interface UpdateCartItemBody
|
||||||
|
extends Core.UpdateCartItemBody<CartItemBody> {}
|
||||||
|
|
||||||
export interface UpdateCartItemHandlerBody
|
export interface UpdateCartItemHandlerBody
|
||||||
extends Core.UpdateCartItemHandlerBody {}
|
extends Core.UpdateCartItemHandlerBody<CartItemBody> {}
|
||||||
|
|
||||||
|
export interface RemoveCartItemBody extends Core.RemoveCartItemBody {}
|
||||||
|
|
||||||
|
export interface RemoveCartItemHandlerBody
|
||||||
|
extends Core.RemoveCartItemHandlerBody {}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import useAction from '../utils/use-action'
|
import useAction from '../utils/use-action'
|
||||||
|
import type { CartItemBody } from '../types'
|
||||||
|
|
||||||
|
// Input expected by the action returned by the `useAddItem` hook
|
||||||
|
export type AddItemInput<T extends CartItemBody> = T
|
||||||
|
|
||||||
const useAddItem = useAction
|
const useAddItem = useAction
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import { useCommerce } from '..'
|
|||||||
|
|
||||||
export type CartResponse<Data> = ResponseState<Data> & { isEmpty?: boolean }
|
export type CartResponse<Data> = ResponseState<Data> & { isEmpty?: boolean }
|
||||||
|
|
||||||
|
// Input expected by the `useCart` hook
|
||||||
export type CartInput = {
|
export type CartInput = {
|
||||||
cartId?: Cart['id']
|
cartId?: Cart['id']
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import useAction from '../utils/use-action'
|
import useAction from '../utils/use-action'
|
||||||
|
|
||||||
|
// Input expected by the action returned by the `useRemoveItem` hook
|
||||||
|
export interface RemoveItemInput {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
const useRemoveItem = useAction
|
const useRemoveItem = useAction
|
||||||
|
|
||||||
export default useRemoveItem
|
export default useRemoveItem
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import useAction from '../utils/use-action'
|
import useAction from '../utils/use-action'
|
||||||
|
import type { CartItemBody } from '../types'
|
||||||
|
|
||||||
|
// Input expected by the action returned by the `useUpdateItem` hook
|
||||||
|
export type UpdateItemInput<T extends CartItemBody> = T & {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
const useUpdateItem = useAction
|
const useUpdateItem = useAction
|
||||||
|
|
||||||
|
@ -87,6 +87,10 @@ export interface Cart {
|
|||||||
discounts?: Discount[]
|
discounts?: Discount[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cart mutations
|
||||||
|
*/
|
||||||
|
|
||||||
// Base cart item body used for cart mutations
|
// Base cart item body used for cart mutations
|
||||||
export interface CartItemBody {
|
export interface CartItemBody {
|
||||||
variantId: string
|
variantId: string
|
||||||
@ -94,18 +98,40 @@ export interface CartItemBody {
|
|||||||
quantity?: number
|
quantity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// Body by the update operation
|
// Body used by the `getCart` operation handler
|
||||||
export interface UpdateCartItemBody {
|
export interface GetCartHandlerBody {
|
||||||
itemId: string
|
cartId?: string
|
||||||
item: CartItemBody
|
}
|
||||||
}
|
|
||||||
|
// Body used by the add item to cart operation
|
||||||
// Input expected by the `useUpdateItem` hook
|
export interface AddCartItemBody<T extends CartItemBody> {
|
||||||
export type UpdateCartItemInput<T extends CartItemBody> = T & {
|
item: T
|
||||||
id: string
|
}
|
||||||
}
|
|
||||||
|
// Body expected by the add item to cart operation handler
|
||||||
// Body expected by the update operation handler
|
export interface AddCartItemHandlerBody<T extends CartItemBody>
|
||||||
export interface UpdateCartItemHandlerBody extends Partial<UpdateCartItemBody> {
|
extends Partial<AddCartItemBody<T>> {
|
||||||
|
cartId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body used by the update cart item operation
|
||||||
|
export interface UpdateCartItemBody<T extends CartItemBody> {
|
||||||
|
itemId: string
|
||||||
|
item: T
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body expected by the update cart item operation handler
|
||||||
|
export interface UpdateCartItemHandlerBody<T extends CartItemBody>
|
||||||
|
extends Partial<UpdateCartItemBody<T>> {
|
||||||
|
cartId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body used by the remove cart item operation
|
||||||
|
export interface RemoveCartItemBody {
|
||||||
|
itemId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body expected by the remove cart item operation handler
|
||||||
|
export interface RemoveCartItemHandlerBody extends Partial<RemoveCartItemBody> {
|
||||||
cartId?: string
|
cartId?: string
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ export default function Pages({
|
|||||||
page,
|
page,
|
||||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto py-20">
|
<div className="max-w-2xl mx-8 sm:mx-auto py-20">
|
||||||
{page?.body && <Text html={page.body} />}
|
{page?.body && <Text html={page.body} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -4,10 +4,9 @@ import getAllPages from '@framework/common/get-all-pages'
|
|||||||
import useCart from '@framework/cart/use-cart'
|
import useCart from '@framework/cart/use-cart'
|
||||||
import usePrice from '@framework/product/use-price'
|
import usePrice from '@framework/product/use-price'
|
||||||
import { Layout } from '@components/common'
|
import { Layout } from '@components/common'
|
||||||
import { Button } from '@components/ui'
|
import { Button, Text } from '@components/ui'
|
||||||
import { Bag, Cross, Check } from '@components/icons'
|
import { Bag, Cross, Check } from '@components/icons'
|
||||||
import { CartItem } from '@components/cart'
|
import { CartItem } from '@components/cart'
|
||||||
import { Text } from '@components/ui'
|
|
||||||
|
|
||||||
export async function getStaticProps({
|
export async function getStaticProps({
|
||||||
preview,
|
preview,
|
||||||
|
7
public/product-img-placeholder.svg
Normal file
7
public/product-img-placeholder.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800">
|
||||||
|
<defs/>
|
||||||
|
<g fill="none" fill-rule="nonzero">
|
||||||
|
<path fill="#EAEAEA" d="M0 0h800v800H0z"/>
|
||||||
|
<path fill="#FFF" d="M366.333 365.833c0 5.695-1.993 10.535-5.979 14.521-3.986 3.986-8.826 5.98-14.52 5.98-5.695 0-10.535-1.994-14.522-5.98-3.986-3.986-5.979-8.826-5.979-14.52 0-5.695 1.993-10.535 5.98-14.522 3.986-3.986 8.826-5.979 14.52-5.979 5.695 0 10.535 1.993 14.521 5.98 3.986 3.986 5.98 8.826 5.98 14.52zm109.334 41v47.834H325.333v-20.5L359.5 400l17.083 17.083 54.667-54.666 44.417 44.416zm10.25-75.166H315.083c-.925 0-1.726.338-2.402 1.014-.676.676-1.014 1.477-1.014 2.402v129.834c0 .925.338 1.726 1.014 2.402.676.676 1.477 1.014 2.402 1.014h170.834c.925 0 1.726-.338 2.402-1.014.676-.676 1.014-1.477 1.014-2.402V335.083c0-.925-.338-1.726-1.014-2.402-.676-.676-1.477-1.014-2.402-1.014zM503 335.083v129.834c0 4.698-1.673 8.72-5.018 12.065-3.346 3.345-7.367 5.018-12.065 5.018H315.083c-4.698 0-8.72-1.673-12.065-5.018-3.345-3.346-5.018-7.367-5.018-12.065V335.083c0-4.698 1.673-8.72 5.018-12.065 3.346-3.345 7.367-5.018 12.065-5.018h170.834c4.698 0 8.72 1.673 12.065 5.018 3.345 3.346 5.018 7.367 5.018 12.065z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
Loading…
x
Reference in New Issue
Block a user