forked from crowetic/commerce
Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic
This commit is contained in:
commit
44081dddb6
@ -75,6 +75,8 @@ const CartItem = ({
|
|||||||
setRemoving(false)
|
setRemoving(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// TODO: Add a type for this
|
||||||
|
const options = (item as any).options
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset the quantity state if the item quantity changes
|
// Reset the quantity state if the item quantity changes
|
||||||
@ -95,8 +97,8 @@ const CartItem = ({
|
|||||||
className={s.productImage}
|
className={s.productImage}
|
||||||
width={150}
|
width={150}
|
||||||
height={150}
|
height={150}
|
||||||
src={item.variant.image.url}
|
src={item.variant.image!.url}
|
||||||
alt={item.variant.image.altText}
|
alt={item.variant.image!.altText}
|
||||||
unoptimized
|
unoptimized
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -109,15 +111,15 @@ const CartItem = ({
|
|||||||
{item.name}
|
{item.name}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
{item.options && item.options.length > 0 ? (
|
{options && options.length > 0 ? (
|
||||||
<div className="">
|
<div className="">
|
||||||
{item.options.map((option: ItemOption, i: number) => (
|
{options.map((option: ItemOption, i: number) => (
|
||||||
<span
|
<span
|
||||||
key={`${item.id}-${option.name}`}
|
key={`${item.id}-${option.name}`}
|
||||||
className="text-sm font-semibold text-accents-7"
|
className="text-sm font-semibold text-accents-7"
|
||||||
>
|
>
|
||||||
{option.value}
|
{option.value}
|
||||||
{i === item.options.length - 1 ? '' : ', '}
|
{i === options.length - 1 ? '' : ', '}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import type { Product } from '@commerce/types'
|
||||||
import { Grid } from '@components/ui'
|
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'
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import type { Product } from '@commerce/types'
|
||||||
import s from './ProductCard.module.css'
|
import s from './ProductCard.module.css'
|
||||||
import Image, { ImageProps } from 'next/image'
|
import Image, { ImageProps } from 'next/image'
|
||||||
// import WishlistButton from '@components/wishlist/WishlistButton'
|
// import WishlistButton from '@components/wishlist/WishlistButton'
|
||||||
|
@ -8,6 +8,7 @@ import { useUI } from '@components/ui'
|
|||||||
import { Swatch, ProductSlider } from '@components/product'
|
import { Swatch, ProductSlider } from '@components/product'
|
||||||
import { Button, Container, Text } from '@components/ui'
|
import { Button, Container, Text } from '@components/ui'
|
||||||
|
|
||||||
|
import type { Product } from '@commerce/types'
|
||||||
import usePrice from '@framework/product/use-price'
|
import usePrice from '@framework/product/use-price'
|
||||||
import { useAddItem } from '@framework/cart'
|
import { useAddItem } from '@framework/cart'
|
||||||
|
|
||||||
@ -41,8 +42,8 @@ const ProductView: FC<Props> = ({ product }) => {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await addItem({
|
await addItem({
|
||||||
productId: product.id,
|
productId: String(product.id),
|
||||||
variantId: variant ? variant.id : product.variants[0].id,
|
variantId: String(variant ? variant.id : product.variants[0].id),
|
||||||
})
|
})
|
||||||
openSidebar()
|
openSidebar()
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import type { Product } from '@commerce/types'
|
||||||
|
|
||||||
export type SelectedOptions = {
|
export type SelectedOptions = {
|
||||||
size: string | null
|
size: string | null
|
||||||
color: string | null
|
color: string | null
|
||||||
|
@ -3,6 +3,7 @@ import cn from 'classnames'
|
|||||||
import { Heart } from '@components/icons'
|
import { Heart } from '@components/icons'
|
||||||
|
|
||||||
import { useUI } from '@components/ui'
|
import { useUI } from '@components/ui'
|
||||||
|
import type { Product, ProductVariant } from '@commerce/types'
|
||||||
import useCustomer from '@framework/customer/use-customer'
|
import useCustomer from '@framework/customer/use-customer'
|
||||||
import useAddItem from '@framework/wishlist/use-add-item'
|
import useAddItem from '@framework/wishlist/use-add-item'
|
||||||
import useRemoveItem from '@framework/wishlist/use-remove-item'
|
import useRemoveItem from '@framework/wishlist/use-remove-item'
|
||||||
|
@ -7,6 +7,7 @@ import { Trash } from '@components/icons'
|
|||||||
import { Button, Text } from '@components/ui'
|
import { Button, Text } from '@components/ui'
|
||||||
|
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
|
import type { Product } from '@commerce/types'
|
||||||
import usePrice from '@framework/product/use-price'
|
import usePrice from '@framework/product/use-price'
|
||||||
import useAddItem from '@framework/cart/use-add-item'
|
import useAddItem from '@framework/cart/use-add-item'
|
||||||
import useRemoveItem from '@framework/wishlist/use-remove-item'
|
import useRemoveItem from '@framework/wishlist/use-remove-item'
|
||||||
@ -42,8 +43,8 @@ const WishlistCard: FC<Props> = ({ product }) => {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await addItem({
|
await addItem({
|
||||||
productId: product.id,
|
productId: String(product.id),
|
||||||
variantId: product.variants[0].id,
|
variantId: String(product.variants[0].id),
|
||||||
})
|
})
|
||||||
openSidebar()
|
openSidebar()
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Product } from 'framework/types'
|
import { Product } from '@commerce/types'
|
||||||
import getAllProducts, { ProductEdge } from '../../../product/get-all-products'
|
import getAllProducts, { ProductEdge } from '../../../product/get-all-products'
|
||||||
import type { ProductsHandlers } from '../products'
|
import type { ProductsHandlers } from '../products'
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ const getProducts: ProductsHandlers['getProducts'] = async ({
|
|||||||
const productsById = graphqlData.products.reduce<{
|
const productsById = graphqlData.products.reduce<{
|
||||||
[k: number]: Product
|
[k: number]: Product
|
||||||
}>((prods, p) => {
|
}>((prods, p) => {
|
||||||
prods[p.id] = p
|
prods[Number(p.id)] = p
|
||||||
return prods
|
return prods
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { Product } from '@commerce/types'
|
||||||
import isAllowedMethod from '../utils/is-allowed-method'
|
import isAllowedMethod from '../utils/is-allowed-method'
|
||||||
import createApiHandler, {
|
import createApiHandler, {
|
||||||
BigcommerceApiHandler,
|
BigcommerceApiHandler,
|
||||||
@ -5,7 +6,6 @@ import createApiHandler, {
|
|||||||
} from '../utils/create-api-handler'
|
} from '../utils/create-api-handler'
|
||||||
import { BigcommerceApiError } from '../utils/errors'
|
import { BigcommerceApiError } from '../utils/errors'
|
||||||
import getProducts from './handlers/get-products'
|
import getProducts from './handlers/get-products'
|
||||||
import { Product } from 'framework/types'
|
|
||||||
|
|
||||||
export type SearchProductsData = {
|
export type SearchProductsData = {
|
||||||
products: Product[]
|
products: Product[]
|
||||||
|
@ -11,6 +11,7 @@ import type {
|
|||||||
import getWishlist from './handlers/get-wishlist'
|
import getWishlist from './handlers/get-wishlist'
|
||||||
import addItem from './handlers/add-item'
|
import addItem from './handlers/add-item'
|
||||||
import removeItem from './handlers/remove-item'
|
import removeItem from './handlers/remove-item'
|
||||||
|
import type { Product, ProductVariant, Customer } from '@commerce/types'
|
||||||
|
|
||||||
export type { Wishlist, WishlistItem }
|
export type { Wishlist, WishlistItem }
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ export type AddItemBody = { item: ItemBody }
|
|||||||
export type RemoveItemBody = { itemId: Product['id'] }
|
export type RemoveItemBody = { itemId: Product['id'] }
|
||||||
|
|
||||||
export type WishlistBody = {
|
export type WishlistBody = {
|
||||||
customer_id: Customer['id']
|
customer_id: Customer['entityId']
|
||||||
is_public: number
|
is_public: number
|
||||||
name: string
|
name: string
|
||||||
items: any[]
|
items: any[]
|
||||||
|
@ -1,4 +1,42 @@
|
|||||||
import useCart, { UseCart } from '@commerce/cart/use-cart'
|
import { useMemo } from 'react'
|
||||||
|
import { HookHandler } from '@commerce/utils/types'
|
||||||
|
import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart'
|
||||||
|
import { normalizeCart } from '../lib/normalize'
|
||||||
|
import type { Cart } from '../types'
|
||||||
import type { BigcommerceProvider } from '..'
|
import type { BigcommerceProvider } from '..'
|
||||||
|
|
||||||
export default useCart as UseCart<BigcommerceProvider>
|
export default useCart as UseCart<BigcommerceProvider>
|
||||||
|
|
||||||
|
export const handler: HookHandler<
|
||||||
|
Cart | null,
|
||||||
|
{},
|
||||||
|
FetchCartInput,
|
||||||
|
{ isEmpty?: boolean }
|
||||||
|
> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/bigcommerce/cart',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
async fetcher({ input: { cartId }, options, fetch }) {
|
||||||
|
const data = cartId ? await fetch(options) : null
|
||||||
|
return data && normalizeCart(data)
|
||||||
|
},
|
||||||
|
useHook({ input, useData }) {
|
||||||
|
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]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -68,7 +68,7 @@ async function getCustomerWishlist({
|
|||||||
const productsById = graphqlData.products.reduce<{
|
const productsById = graphqlData.products.reduce<{
|
||||||
[k: number]: ProductEdge
|
[k: number]: ProductEdge
|
||||||
}>((prods, p) => {
|
}>((prods, p) => {
|
||||||
prods[p.node.entityId] = p
|
prods[Number(p.node.entityId)] = p as any
|
||||||
return prods
|
return prods
|
||||||
}, {})
|
}, {})
|
||||||
// Populate the wishlist items with the graphql products
|
// Populate the wishlist items with the graphql products
|
||||||
|
@ -1,4 +1,25 @@
|
|||||||
|
import { HookHandler } from '@commerce/utils/types'
|
||||||
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
|
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
|
||||||
|
import type { Customer, CustomerData } from '../api/customers'
|
||||||
import type { BigcommerceProvider } from '..'
|
import type { BigcommerceProvider } from '..'
|
||||||
|
|
||||||
export default useCustomer as UseCustomer<BigcommerceProvider>
|
export default useCustomer as UseCustomer<BigcommerceProvider>
|
||||||
|
|
||||||
|
export const handler: HookHandler<Customer | null> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/bigcommerce/customers',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
async fetcher({ options, fetch }) {
|
||||||
|
const data = await fetch<CustomerData | null>(options)
|
||||||
|
return data?.customer ?? null
|
||||||
|
},
|
||||||
|
useHook({ input, useData }) {
|
||||||
|
return useData({
|
||||||
|
swrOptions: {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
...input.swrOptions,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
41
framework/bigcommerce/fetcher.ts
Normal file
41
framework/bigcommerce/fetcher.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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,3 +1,4 @@
|
|||||||
|
import type { Product } from '@commerce/types'
|
||||||
import type { Cart, BigcommerceCart, LineItem } from '../types'
|
import type { Cart, BigcommerceCart, LineItem } from '../types'
|
||||||
import update from './immutability'
|
import update from './immutability'
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import type {
|
|||||||
GetAllProductsQuery,
|
GetAllProductsQuery,
|
||||||
GetAllProductsQueryVariables,
|
GetAllProductsQueryVariables,
|
||||||
} from '../schema'
|
} from '../schema'
|
||||||
|
import type { Product } from '@commerce/types'
|
||||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
||||||
import filterEdges from '../api/utils/filter-edges'
|
import filterEdges from '../api/utils/filter-edges'
|
||||||
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
||||||
@ -94,6 +95,7 @@ async function getAllProducts({
|
|||||||
variables?: ProductVariables
|
variables?: ProductVariables
|
||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
preview?: boolean
|
preview?: boolean
|
||||||
|
// TODO: fix the product type here
|
||||||
} = {}): Promise<{ products: Product[] | any[] }> {
|
} = {}): Promise<{ products: Product[] | any[] }> {
|
||||||
config = getConfig(config)
|
config = getConfig(config)
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
|||||||
import { productInfoFragment } from '../api/fragments/product'
|
import { productInfoFragment } from '../api/fragments/product'
|
||||||
import { BigcommerceConfig, getConfig } from '../api'
|
import { BigcommerceConfig, getConfig } from '../api'
|
||||||
import { normalizeProduct } from '@framework/lib/normalize'
|
import { normalizeProduct } from '@framework/lib/normalize'
|
||||||
|
import type { Product } from '@commerce/types'
|
||||||
|
|
||||||
export const getProductQuery = /* GraphQL */ `
|
export const getProductQuery = /* GraphQL */ `
|
||||||
query getProduct(
|
query getProduct(
|
||||||
|
@ -1,4 +1,54 @@
|
|||||||
|
import { HookHandler } from '@commerce/utils/types'
|
||||||
import useSearch, { UseSearch } from '@commerce/products/use-search'
|
import useSearch, { UseSearch } from '@commerce/products/use-search'
|
||||||
|
import type { SearchProductsData } from '../api/catalog/products'
|
||||||
import type { BigcommerceProvider } from '..'
|
import type { BigcommerceProvider } from '..'
|
||||||
|
|
||||||
export default useSearch as UseSearch<BigcommerceProvider>
|
export default useSearch as UseSearch<BigcommerceProvider>
|
||||||
|
|
||||||
|
export type SearchProductsInput = {
|
||||||
|
search?: string
|
||||||
|
categoryId?: number
|
||||||
|
brandId?: number
|
||||||
|
sort?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handler: HookHandler<
|
||||||
|
SearchProductsData,
|
||||||
|
SearchProductsInput,
|
||||||
|
SearchProductsInput
|
||||||
|
> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/bigcommerce/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(categoryId))
|
||||||
|
url.searchParams.set('category', String(categoryId))
|
||||||
|
if (Number.isInteger(brandId))
|
||||||
|
url.searchParams.set('brand', String(brandId))
|
||||||
|
if (sort) url.searchParams.set('sort', sort)
|
||||||
|
|
||||||
|
return fetch({
|
||||||
|
url: url.pathname + url.search,
|
||||||
|
method: options.method,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
useHook({ input, useData }) {
|
||||||
|
return useData({
|
||||||
|
input: [
|
||||||
|
['search', input.search],
|
||||||
|
['categoryId', input.categoryId],
|
||||||
|
['brandId', input.brandId],
|
||||||
|
['sort', input.sort],
|
||||||
|
],
|
||||||
|
swrOptions: {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
...input.swrOptions,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
17
framework/bigcommerce/provider.ts
Normal file
17
framework/bigcommerce/provider.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { handler as useCart } from './cart/use-cart'
|
||||||
|
import { handler as useWishlist } from './wishlist/use-wishlist'
|
||||||
|
import { handler as useCustomer } from './customer/use-customer'
|
||||||
|
import { handler as useSearch } from './product/use-search'
|
||||||
|
import fetcher from './fetcher'
|
||||||
|
|
||||||
|
export const bigcommerceProvider = {
|
||||||
|
locale: 'en-us',
|
||||||
|
cartCookie: 'bc_cartId',
|
||||||
|
fetcher,
|
||||||
|
cart: { useCart },
|
||||||
|
wishlist: { useWishlist },
|
||||||
|
customer: { useCustomer },
|
||||||
|
products: { useSearch },
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BigcommerceProvider = typeof bigcommerceProvider
|
@ -1,212 +0,0 @@
|
|||||||
import { useMemo } from 'react'
|
|
||||||
import { FetcherError } from '@commerce/utils/errors'
|
|
||||||
import type { Fetcher, HookHandler } from '@commerce/utils/types'
|
|
||||||
import type { FetchCartInput } from '@commerce/cart/use-cart'
|
|
||||||
import { normalizeCart } from './lib/normalize'
|
|
||||||
import type { Wishlist } from './api/wishlist'
|
|
||||||
import type { Customer, CustomerData } from './api/customers'
|
|
||||||
import type { SearchProductsData } from './api/catalog/products'
|
|
||||||
import useCustomer from './customer/use-customer'
|
|
||||||
import type { Cart } from './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)
|
|
||||||
}
|
|
||||||
|
|
||||||
const useCart: HookHandler<
|
|
||||||
Cart | null,
|
|
||||||
{},
|
|
||||||
FetchCartInput,
|
|
||||||
{ isEmpty?: boolean }
|
|
||||||
> = {
|
|
||||||
fetchOptions: {
|
|
||||||
url: '/api/bigcommerce/cart',
|
|
||||||
method: 'GET',
|
|
||||||
},
|
|
||||||
async fetcher({ input: { cartId }, options, fetch }) {
|
|
||||||
const data = cartId ? await fetch(options) : null
|
|
||||||
return data && normalizeCart(data)
|
|
||||||
},
|
|
||||||
useHook({ input, useData }) {
|
|
||||||
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]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const useWishlist: HookHandler<
|
|
||||||
Wishlist | null,
|
|
||||||
{ includeProducts?: boolean },
|
|
||||||
{ customerId?: number; includeProducts: boolean },
|
|
||||||
{ isEmpty?: boolean }
|
|
||||||
> = {
|
|
||||||
fetchOptions: {
|
|
||||||
url: '/api/bigcommerce/wishlist',
|
|
||||||
method: 'GET',
|
|
||||||
},
|
|
||||||
fetcher({ input: { customerId, includeProducts }, options, fetch }) {
|
|
||||||
if (!customerId) return null
|
|
||||||
|
|
||||||
// Use a dummy base as we only care about the relative path
|
|
||||||
const url = new URL(options.url!, 'http://a')
|
|
||||||
|
|
||||||
if (includeProducts) url.searchParams.set('products', '1')
|
|
||||||
|
|
||||||
return fetch({
|
|
||||||
url: url.pathname + url.search,
|
|
||||||
method: options.method,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
useHook({ input, useData }) {
|
|
||||||
const { data: customer } = useCustomer()
|
|
||||||
const response = useData({
|
|
||||||
input: [
|
|
||||||
['customerId', customer?.id],
|
|
||||||
['includeProducts', input.includeProducts],
|
|
||||||
],
|
|
||||||
swrOptions: {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
...input.swrOptions,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() =>
|
|
||||||
Object.create(response, {
|
|
||||||
isEmpty: {
|
|
||||||
get() {
|
|
||||||
return (response.data?.items?.length || 0) <= 0
|
|
||||||
},
|
|
||||||
enumerable: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[response]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const useCustomerHandler: HookHandler<Customer | null> = {
|
|
||||||
fetchOptions: {
|
|
||||||
url: '/api/bigcommerce/customers',
|
|
||||||
method: 'GET',
|
|
||||||
},
|
|
||||||
async fetcher({ options, fetch }) {
|
|
||||||
const data = await fetch<CustomerData | null>(options)
|
|
||||||
return data?.customer ?? null
|
|
||||||
},
|
|
||||||
useHook({ input, useData }) {
|
|
||||||
return useData({
|
|
||||||
swrOptions: {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
...input.swrOptions,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SearchProductsInput = {
|
|
||||||
search?: string
|
|
||||||
categoryId?: number
|
|
||||||
brandId?: number
|
|
||||||
sort?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const useSearch: HookHandler<
|
|
||||||
SearchProductsData,
|
|
||||||
SearchProductsInput,
|
|
||||||
SearchProductsInput
|
|
||||||
> = {
|
|
||||||
fetchOptions: {
|
|
||||||
url: '/api/bigcommerce/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(categoryId))
|
|
||||||
url.searchParams.set('category', String(categoryId))
|
|
||||||
if (Number.isInteger(brandId))
|
|
||||||
url.searchParams.set('brand', String(brandId))
|
|
||||||
if (sort) url.searchParams.set('sort', sort)
|
|
||||||
|
|
||||||
return fetch({
|
|
||||||
url: url.pathname + url.search,
|
|
||||||
method: options.method,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
useHook({ input, useData }) {
|
|
||||||
return useData({
|
|
||||||
input: [
|
|
||||||
['search', input.search],
|
|
||||||
['categoryId', input.categoryId],
|
|
||||||
['brandId', input.brandId],
|
|
||||||
['sort', input.sort],
|
|
||||||
],
|
|
||||||
swrOptions: {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
...input.swrOptions,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const bigcommerceProvider = {
|
|
||||||
locale: 'en-us',
|
|
||||||
cartCookie: 'bc_cartId',
|
|
||||||
fetcher,
|
|
||||||
cartNormalizer: normalizeCart,
|
|
||||||
cart: { useCart },
|
|
||||||
wishlist: { useWishlist },
|
|
||||||
customer: { useCustomer: useCustomerHandler },
|
|
||||||
products: { useSearch },
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BigcommerceProvider = typeof bigcommerceProvider
|
|
@ -1,19 +1,23 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { HookFetcher } from '@commerce/utils/types'
|
import { HookFetcher } from '@commerce/utils/types'
|
||||||
import { CommerceError } from '@commerce/utils/errors'
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
import useWishlistAddItem from '@commerce/wishlist/use-add-item'
|
import useWishlistAddItem, {
|
||||||
|
AddItemInput,
|
||||||
|
} from '@commerce/wishlist/use-add-item'
|
||||||
|
import { UseWishlistInput } from '@commerce/wishlist/use-wishlist'
|
||||||
import type { ItemBody, AddItemBody } from '../api/wishlist'
|
import type { ItemBody, AddItemBody } from '../api/wishlist'
|
||||||
import useCustomer from '../customer/use-customer'
|
import useCustomer from '../customer/use-customer'
|
||||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
import useWishlist from './use-wishlist'
|
||||||
|
import type { BigcommerceProvider } from '..'
|
||||||
|
|
||||||
const defaultOpts = {
|
const defaultOpts = {
|
||||||
url: '/api/bigcommerce/wishlist',
|
url: '/api/bigcommerce/wishlist',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AddItemInput = ItemBody
|
// export type AddItemInput = ItemBody
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
|
export const fetcher: HookFetcher<any, AddItemBody> = (
|
||||||
options,
|
options,
|
||||||
{ item },
|
{ item },
|
||||||
fetch
|
fetch
|
||||||
@ -27,13 +31,13 @@ export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
export function extendHook(customFetcher: typeof fetcher) {
|
||||||
const useAddItem = (opts?: UseWishlistOptions) => {
|
const useAddItem = (opts?: UseWishlistInput<BigcommerceProvider>) => {
|
||||||
const { data: customer } = useCustomer()
|
const { data: customer } = useCustomer()
|
||||||
const { revalidate } = useWishlist(opts)
|
const { revalidate } = useWishlist(opts)
|
||||||
const fn = useWishlistAddItem(defaultOpts, customFetcher)
|
const fn = useWishlistAddItem(defaultOpts, customFetcher)
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function addItem(input: AddItemInput) {
|
async function addItem(input: AddItemInput<any>) {
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
// A signed customer is required in order to have a wishlist
|
// A signed customer is required in order to have a wishlist
|
||||||
throw new CommerceError({
|
throw new CommerceError({
|
||||||
|
@ -4,7 +4,7 @@ import { CommerceError } from '@commerce/utils/errors'
|
|||||||
import useWishlistRemoveItem from '@commerce/wishlist/use-remove-item'
|
import useWishlistRemoveItem from '@commerce/wishlist/use-remove-item'
|
||||||
import type { RemoveItemBody } from '../api/wishlist'
|
import type { RemoveItemBody } from '../api/wishlist'
|
||||||
import useCustomer from '../customer/use-customer'
|
import useCustomer from '../customer/use-customer'
|
||||||
import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
|
import useWishlist from './use-wishlist'
|
||||||
|
|
||||||
const defaultOpts = {
|
const defaultOpts = {
|
||||||
url: '/api/bigcommerce/wishlist',
|
url: '/api/bigcommerce/wishlist',
|
||||||
@ -15,7 +15,7 @@ export type RemoveItemInput = {
|
|||||||
id: string | number
|
id: string | number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = (
|
export const fetcher: HookFetcher<any | null, RemoveItemBody> = (
|
||||||
options,
|
options,
|
||||||
{ itemId },
|
{ itemId },
|
||||||
fetch
|
fetch
|
||||||
@ -28,10 +28,10 @@ export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
export function extendHook(customFetcher: typeof fetcher) {
|
||||||
const useRemoveItem = (opts?: UseWishlistOptions) => {
|
const useRemoveItem = (opts?: any) => {
|
||||||
const { data: customer } = useCustomer()
|
const { data: customer } = useCustomer()
|
||||||
const { revalidate } = useWishlist(opts)
|
const { revalidate } = useWishlist(opts)
|
||||||
const fn = useWishlistRemoveItem<Wishlist | null, RemoveItemBody>(
|
const fn = useWishlistRemoveItem<any | null, RemoveItemBody>(
|
||||||
defaultOpts,
|
defaultOpts,
|
||||||
customFetcher
|
customFetcher
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,59 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { HookHandler } from '@commerce/utils/types'
|
||||||
import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
|
import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
|
||||||
|
import type { Wishlist } from '../api/wishlist'
|
||||||
|
import useCustomer from '../customer/use-customer'
|
||||||
import type { BigcommerceProvider } from '..'
|
import type { BigcommerceProvider } from '..'
|
||||||
|
|
||||||
export default useWishlist as UseWishlist<BigcommerceProvider>
|
export default useWishlist as UseWishlist<BigcommerceProvider>
|
||||||
|
|
||||||
|
export const handler: HookHandler<
|
||||||
|
Wishlist | null,
|
||||||
|
{ includeProducts?: boolean },
|
||||||
|
{ customerId?: number; includeProducts: boolean },
|
||||||
|
{ isEmpty?: boolean }
|
||||||
|
> = {
|
||||||
|
fetchOptions: {
|
||||||
|
url: '/api/bigcommerce/wishlist',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
fetcher({ input: { customerId, includeProducts }, options, fetch }) {
|
||||||
|
if (!customerId) return null
|
||||||
|
|
||||||
|
// Use a dummy base as we only care about the relative path
|
||||||
|
const url = new URL(options.url!, 'http://a')
|
||||||
|
|
||||||
|
if (includeProducts) url.searchParams.set('products', '1')
|
||||||
|
|
||||||
|
return fetch({
|
||||||
|
url: url.pathname + url.search,
|
||||||
|
method: options.method,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
useHook({ input, useData }) {
|
||||||
|
const { data: customer } = useCustomer()
|
||||||
|
const response = useData({
|
||||||
|
input: [
|
||||||
|
['customerId', (customer as any)?.id],
|
||||||
|
['includeProducts', input.includeProducts],
|
||||||
|
],
|
||||||
|
swrOptions: {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
...input.swrOptions,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
Object.create(response, {
|
||||||
|
isEmpty: {
|
||||||
|
get() {
|
||||||
|
return (response.data?.items?.length || 0) <= 0
|
||||||
|
},
|
||||||
|
enumerable: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[response]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
75
framework/commerce/types.d.ts
vendored
75
framework/commerce/types.d.ts
vendored
@ -1,75 +0,0 @@
|
|||||||
interface Entity {
|
|
||||||
id: string | number
|
|
||||||
[prop: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Product extends Entity {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
slug?: string
|
|
||||||
path?: string
|
|
||||||
images: ProductImage[]
|
|
||||||
variants: ProductVariant[]
|
|
||||||
price: ProductPrice
|
|
||||||
options: ProductOption[]
|
|
||||||
sku?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProductOption extends Entity {
|
|
||||||
displayName: string
|
|
||||||
values: ProductOptionValues[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProductOptionValues {
|
|
||||||
label: string
|
|
||||||
hexColors?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProductImage {
|
|
||||||
url: string
|
|
||||||
alt?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProductVariant {
|
|
||||||
id: string | number
|
|
||||||
options: ProductOption[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProductPrice {
|
|
||||||
value: number
|
|
||||||
currencyCode: 'USD' | 'ARS' | string | undefined
|
|
||||||
retailPrice?: number
|
|
||||||
salePrice?: number
|
|
||||||
listPrice?: number
|
|
||||||
extendedSalePrice?: number
|
|
||||||
extendedListPrice?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CartItem extends Entity {
|
|
||||||
quantity: number
|
|
||||||
productId: Product['id']
|
|
||||||
variantId: ProductVariant['id']
|
|
||||||
images: ProductImage[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Wishlist extends Entity {
|
|
||||||
products: Pick<Product, 'id' | 'name' | 'prices'>[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Order {}
|
|
||||||
|
|
||||||
interface Customer extends Entity {}
|
|
||||||
|
|
||||||
type UseCustomerResponse = {
|
|
||||||
customer: Customer
|
|
||||||
} | null
|
|
||||||
|
|
||||||
interface Category extends Entity {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Brand extends Entity {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Features = 'wishlist' | 'customer'
|
|
@ -148,3 +148,54 @@ export interface RemoveCartItemBody {
|
|||||||
export interface RemoveCartItemHandlerBody extends Partial<RemoveCartItemBody> {
|
export interface RemoveCartItemHandlerBody extends Partial<RemoveCartItemBody> {
|
||||||
cartId?: string
|
cartId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporal types
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Entity {
|
||||||
|
id: string | number
|
||||||
|
[prop: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Product extends Entity {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
slug?: string
|
||||||
|
path?: string
|
||||||
|
images: ProductImage[]
|
||||||
|
variants: ProductVariant2[]
|
||||||
|
price: ProductPrice
|
||||||
|
options: ProductOption[]
|
||||||
|
sku?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductOption extends Entity {
|
||||||
|
displayName: string
|
||||||
|
values: ProductOptionValues[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductOptionValues {
|
||||||
|
label: string
|
||||||
|
hexColors?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductImage {
|
||||||
|
url: string
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductVariant2 {
|
||||||
|
id: string | number
|
||||||
|
options: ProductOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductPrice {
|
||||||
|
value: number
|
||||||
|
currencyCode: 'USD' | 'ARS' | string | undefined
|
||||||
|
retailPrice?: number
|
||||||
|
salePrice?: number
|
||||||
|
listPrice?: number
|
||||||
|
extendedSalePrice?: number
|
||||||
|
extendedListPrice?: number
|
||||||
|
}
|
||||||
|
@ -76,6 +76,29 @@ export type HookHandler<
|
|||||||
fetcher?: HookFetcherFn<Data, FetchInput>
|
fetcher?: HookFetcherFn<Data, FetchInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MutationHandler<
|
||||||
|
// Data obj returned by the hook and fetch operation
|
||||||
|
Data,
|
||||||
|
// Input expected by the hook
|
||||||
|
Input extends { [k: string]: unknown } = {},
|
||||||
|
// Input expected before doing a fetch operation
|
||||||
|
FetchInput extends HookFetchInput = {},
|
||||||
|
// Custom state added to the response object of SWR
|
||||||
|
State = {}
|
||||||
|
> = {
|
||||||
|
useHook?(context: {
|
||||||
|
input: Input & { swrOptions?: SwrOptions<Data, FetchInput> }
|
||||||
|
useCallback(
|
||||||
|
fn: (context?: {
|
||||||
|
input?: HookFetchInput | HookSwrInput
|
||||||
|
swrOptions?: SwrOptions<Data, FetchInput>
|
||||||
|
}) => Data
|
||||||
|
): ResponseState<Data>
|
||||||
|
}): ResponseState<Data> & State
|
||||||
|
fetchOptions: HookFetcherOptions
|
||||||
|
fetcher?: HookFetcherFn<Data, FetchInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
|
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
|
||||||
Data,
|
Data,
|
||||||
CommerceError,
|
CommerceError,
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
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 interface AddItemInput {
|
||||||
|
// includeProducts?: boolean
|
||||||
|
// }
|
||||||
|
export type AddItemInput<T extends CartItemBody> = T
|
||||||
|
|
||||||
const useAddItem = useAction
|
const useAddItem = useAction
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ export default function Slug({
|
|||||||
return router.isFallback ? (
|
return router.isFallback ? (
|
||||||
<h1>Loading...</h1> // TODO (BC) Add Skeleton Views
|
<h1>Loading...</h1> // TODO (BC) Add Skeleton Views
|
||||||
) : (
|
) : (
|
||||||
<ProductView product={product} />
|
<ProductView product={product as any} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ export default function Wishlist() {
|
|||||||
) : (
|
) : (
|
||||||
data &&
|
data &&
|
||||||
data.items?.map((item) => (
|
data.items?.map((item) => (
|
||||||
<WishlistCard key={item.id} item={item} />
|
<WishlistCard key={item.id} product={item as any} />
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user