diff --git a/components/cart/CartItem/CartItem.tsx b/components/cart/CartItem/CartItem.tsx index 87fb86d0b..bb57c3f25 100644 --- a/components/cart/CartItem/CartItem.tsx +++ b/components/cart/CartItem/CartItem.tsx @@ -75,6 +75,8 @@ const CartItem = ({ setRemoving(false) } } + // TODO: Add a type for this + const options = (item as any).options useEffect(() => { // Reset the quantity state if the item quantity changes @@ -95,8 +97,8 @@ const CartItem = ({ className={s.productImage} width={150} height={150} - src={item.variant.image.url} - alt={item.variant.image.altText} + src={item.variant.image!.url} + alt={item.variant.image!.altText} unoptimized /> @@ -109,15 +111,15 @@ const CartItem = ({ {item.name} - {item.options && item.options.length > 0 ? ( + {options && options.length > 0 ? (
- {item.options.map((option: ItemOption, i: number) => ( + {options.map((option: ItemOption, i: number) => ( {option.value} - {i === item.options.length - 1 ? '' : ', '} + {i === options.length - 1 ? '' : ', '} ))}
diff --git a/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx b/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx index c7f07bf48..677757b7b 100644 --- a/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx +++ b/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx @@ -1,5 +1,6 @@ import { FC } from 'react' import Link from 'next/link' +import type { Product } from '@commerce/types' import { Grid } from '@components/ui' import { ProductCard } from '@components/product' import s from './HomeAllProductsGrid.module.css' diff --git a/components/product/ProductCard/ProductCard.tsx b/components/product/ProductCard/ProductCard.tsx index 8d4402271..5784e4b1a 100644 --- a/components/product/ProductCard/ProductCard.tsx +++ b/components/product/ProductCard/ProductCard.tsx @@ -1,6 +1,7 @@ import { FC } from 'react' import cn from 'classnames' import Link from 'next/link' +import type { Product } from '@commerce/types' import s from './ProductCard.module.css' import Image, { ImageProps } from 'next/image' // import WishlistButton from '@components/wishlist/WishlistButton' diff --git a/components/product/ProductView/ProductView.tsx b/components/product/ProductView/ProductView.tsx index 65fa4d93f..61beda7fe 100644 --- a/components/product/ProductView/ProductView.tsx +++ b/components/product/ProductView/ProductView.tsx @@ -8,6 +8,7 @@ import { useUI } from '@components/ui' import { Swatch, ProductSlider } from '@components/product' import { Button, Container, Text } from '@components/ui' +import type { Product } from '@commerce/types' import usePrice from '@framework/product/use-price' import { useAddItem } from '@framework/cart' @@ -41,8 +42,8 @@ const ProductView: FC = ({ product }) => { setLoading(true) try { await addItem({ - productId: product.id, - variantId: variant ? variant.id : product.variants[0].id, + productId: String(product.id), + variantId: String(variant ? variant.id : product.variants[0].id), }) openSidebar() setLoading(false) diff --git a/components/product/helpers.ts b/components/product/helpers.ts index ae0c43530..029476c92 100644 --- a/components/product/helpers.ts +++ b/components/product/helpers.ts @@ -1,3 +1,5 @@ +import type { Product } from '@commerce/types' + export type SelectedOptions = { size: string | null color: string | null diff --git a/components/wishlist/WishlistButton/WishlistButton.tsx b/components/wishlist/WishlistButton/WishlistButton.tsx index dced18a89..0c4c20194 100644 --- a/components/wishlist/WishlistButton/WishlistButton.tsx +++ b/components/wishlist/WishlistButton/WishlistButton.tsx @@ -3,6 +3,7 @@ import cn from 'classnames' import { Heart } from '@components/icons' import { useUI } from '@components/ui' +import type { Product, ProductVariant } from '@commerce/types' import useCustomer from '@framework/customer/use-customer' import useAddItem from '@framework/wishlist/use-add-item' import useRemoveItem from '@framework/wishlist/use-remove-item' diff --git a/components/wishlist/WishlistCard/WishlistCard.tsx b/components/wishlist/WishlistCard/WishlistCard.tsx index d1a9403b3..38663ab68 100644 --- a/components/wishlist/WishlistCard/WishlistCard.tsx +++ b/components/wishlist/WishlistCard/WishlistCard.tsx @@ -7,6 +7,7 @@ import { Trash } from '@components/icons' import { Button, Text } from '@components/ui' import { useUI } from '@components/ui/context' +import type { Product } from '@commerce/types' import usePrice from '@framework/product/use-price' import useAddItem from '@framework/cart/use-add-item' import useRemoveItem from '@framework/wishlist/use-remove-item' @@ -42,8 +43,8 @@ const WishlistCard: FC = ({ product }) => { setLoading(true) try { await addItem({ - productId: product.id, - variantId: product.variants[0].id, + productId: String(product.id), + variantId: String(product.variants[0].id), }) openSidebar() setLoading(false) diff --git a/framework/bigcommerce/api/catalog/handlers/get-products.ts b/framework/bigcommerce/api/catalog/handlers/get-products.ts index 894dc5cf3..bedd773b0 100644 --- a/framework/bigcommerce/api/catalog/handlers/get-products.ts +++ b/framework/bigcommerce/api/catalog/handlers/get-products.ts @@ -1,4 +1,4 @@ -import { Product } from 'framework/types' +import { Product } from '@commerce/types' import getAllProducts, { ProductEdge } from '../../../product/get-all-products' import type { ProductsHandlers } from '../products' @@ -60,7 +60,7 @@ const getProducts: ProductsHandlers['getProducts'] = async ({ const productsById = graphqlData.products.reduce<{ [k: number]: Product }>((prods, p) => { - prods[p.id] = p + prods[Number(p.id)] = p return prods }, {}) diff --git a/framework/bigcommerce/api/catalog/products.ts b/framework/bigcommerce/api/catalog/products.ts index d13dcd3c3..d52b2de7e 100644 --- a/framework/bigcommerce/api/catalog/products.ts +++ b/framework/bigcommerce/api/catalog/products.ts @@ -1,3 +1,4 @@ +import type { Product } from '@commerce/types' import isAllowedMethod from '../utils/is-allowed-method' import createApiHandler, { BigcommerceApiHandler, @@ -5,7 +6,6 @@ import createApiHandler, { } from '../utils/create-api-handler' import { BigcommerceApiError } from '../utils/errors' import getProducts from './handlers/get-products' -import { Product } from 'framework/types' export type SearchProductsData = { products: Product[] diff --git a/framework/bigcommerce/api/wishlist/index.ts b/framework/bigcommerce/api/wishlist/index.ts index e892d2e78..7c700689c 100644 --- a/framework/bigcommerce/api/wishlist/index.ts +++ b/framework/bigcommerce/api/wishlist/index.ts @@ -11,6 +11,7 @@ import type { import getWishlist from './handlers/get-wishlist' import addItem from './handlers/add-item' import removeItem from './handlers/remove-item' +import type { Product, ProductVariant, Customer } from '@commerce/types' export type { Wishlist, WishlistItem } @@ -24,7 +25,7 @@ export type AddItemBody = { item: ItemBody } export type RemoveItemBody = { itemId: Product['id'] } export type WishlistBody = { - customer_id: Customer['id'] + customer_id: Customer['entityId'] is_public: number name: string items: any[] diff --git a/framework/bigcommerce/cart/use-cart.tsx b/framework/bigcommerce/cart/use-cart.tsx index 4f8a5cbcd..b5cc0cccf 100644 --- a/framework/bigcommerce/cart/use-cart.tsx +++ b/framework/bigcommerce/cart/use-cart.tsx @@ -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 '..' export default useCart as UseCart + +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] + ) + }, +} diff --git a/framework/bigcommerce/customer/get-customer-wishlist.ts b/framework/bigcommerce/customer/get-customer-wishlist.ts index a3c7413cc..e854ff933 100644 --- a/framework/bigcommerce/customer/get-customer-wishlist.ts +++ b/framework/bigcommerce/customer/get-customer-wishlist.ts @@ -68,7 +68,7 @@ async function getCustomerWishlist({ const productsById = graphqlData.products.reduce<{ [k: number]: ProductEdge }>((prods, p) => { - prods[p.node.entityId] = p + prods[Number(p.node.entityId)] = p as any return prods }, {}) // Populate the wishlist items with the graphql products diff --git a/framework/bigcommerce/customer/use-customer.tsx b/framework/bigcommerce/customer/use-customer.tsx index 95adb6fb3..3929002f7 100644 --- a/framework/bigcommerce/customer/use-customer.tsx +++ b/framework/bigcommerce/customer/use-customer.tsx @@ -1,4 +1,25 @@ +import { HookHandler } from '@commerce/utils/types' import useCustomer, { UseCustomer } from '@commerce/customer/use-customer' +import type { Customer, CustomerData } from '../api/customers' import type { BigcommerceProvider } from '..' export default useCustomer as UseCustomer + +export const handler: HookHandler = { + fetchOptions: { + url: '/api/bigcommerce/customers', + method: 'GET', + }, + async fetcher({ options, fetch }) { + const data = await fetch(options) + return data?.customer ?? null + }, + useHook({ input, useData }) { + return useData({ + swrOptions: { + revalidateOnFocus: false, + ...input.swrOptions, + }, + }) + }, +} diff --git a/framework/bigcommerce/fetcher.ts b/framework/bigcommerce/fetcher.ts new file mode 100644 index 000000000..f8ca0c578 --- /dev/null +++ b/framework/bigcommerce/fetcher.ts @@ -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 diff --git a/framework/bigcommerce/lib/normalize.ts b/framework/bigcommerce/lib/normalize.ts index 89aed2c38..cc7606099 100644 --- a/framework/bigcommerce/lib/normalize.ts +++ b/framework/bigcommerce/lib/normalize.ts @@ -1,3 +1,4 @@ +import type { Product } from '@commerce/types' import type { Cart, BigcommerceCart, LineItem } from '../types' import update from './immutability' diff --git a/framework/bigcommerce/product/get-all-products.ts b/framework/bigcommerce/product/get-all-products.ts index b7d728c4a..4c563bc62 100644 --- a/framework/bigcommerce/product/get-all-products.ts +++ b/framework/bigcommerce/product/get-all-products.ts @@ -2,6 +2,7 @@ import type { GetAllProductsQuery, GetAllProductsQueryVariables, } from '../schema' +import type { Product } from '@commerce/types' import type { RecursivePartial, RecursiveRequired } from '../api/utils/types' import filterEdges from '../api/utils/filter-edges' import setProductLocaleMeta from '../api/utils/set-product-locale-meta' @@ -94,6 +95,7 @@ async function getAllProducts({ variables?: ProductVariables config?: BigcommerceConfig preview?: boolean + // TODO: fix the product type here } = {}): Promise<{ products: Product[] | any[] }> { config = getConfig(config) diff --git a/framework/bigcommerce/product/get-product.ts b/framework/bigcommerce/product/get-product.ts index 3624a9cca..7d77eb194 100644 --- a/framework/bigcommerce/product/get-product.ts +++ b/framework/bigcommerce/product/get-product.ts @@ -3,6 +3,7 @@ import setProductLocaleMeta from '../api/utils/set-product-locale-meta' import { productInfoFragment } from '../api/fragments/product' import { BigcommerceConfig, getConfig } from '../api' import { normalizeProduct } from '@framework/lib/normalize' +import type { Product } from '@commerce/types' export const getProductQuery = /* GraphQL */ ` query getProduct( diff --git a/framework/bigcommerce/product/use-search.tsx b/framework/bigcommerce/product/use-search.tsx index 52db6a72d..393a8c0b9 100644 --- a/framework/bigcommerce/product/use-search.tsx +++ b/framework/bigcommerce/product/use-search.tsx @@ -1,4 +1,54 @@ +import { HookHandler } from '@commerce/utils/types' import useSearch, { UseSearch } from '@commerce/products/use-search' +import type { SearchProductsData } from '../api/catalog/products' import type { BigcommerceProvider } from '..' export default useSearch as UseSearch + +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, + }, + }) + }, +} diff --git a/framework/bigcommerce/provider.ts b/framework/bigcommerce/provider.ts new file mode 100644 index 000000000..ee5630813 --- /dev/null +++ b/framework/bigcommerce/provider.ts @@ -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 diff --git a/framework/bigcommerce/provider.tsx b/framework/bigcommerce/provider.tsx deleted file mode 100644 index 6fd02e304..000000000 --- a/framework/bigcommerce/provider.tsx +++ /dev/null @@ -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 = { - fetchOptions: { - url: '/api/bigcommerce/customers', - method: 'GET', - }, - async fetcher({ options, fetch }) { - const data = await fetch(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 diff --git a/framework/bigcommerce/wishlist/use-add-item.tsx b/framework/bigcommerce/wishlist/use-add-item.tsx index 6e7d9de41..eb961951a 100644 --- a/framework/bigcommerce/wishlist/use-add-item.tsx +++ b/framework/bigcommerce/wishlist/use-add-item.tsx @@ -1,19 +1,23 @@ import { useCallback } from 'react' import { HookFetcher } from '@commerce/utils/types' 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 useCustomer from '../customer/use-customer' -import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist' +import useWishlist from './use-wishlist' +import type { BigcommerceProvider } from '..' const defaultOpts = { url: '/api/bigcommerce/wishlist', method: 'POST', } -export type AddItemInput = ItemBody +// export type AddItemInput = ItemBody -export const fetcher: HookFetcher = ( +export const fetcher: HookFetcher = ( options, { item }, fetch @@ -27,13 +31,13 @@ export const fetcher: HookFetcher = ( } export function extendHook(customFetcher: typeof fetcher) { - const useAddItem = (opts?: UseWishlistOptions) => { + const useAddItem = (opts?: UseWishlistInput) => { const { data: customer } = useCustomer() const { revalidate } = useWishlist(opts) const fn = useWishlistAddItem(defaultOpts, customFetcher) return useCallback( - async function addItem(input: AddItemInput) { + async function addItem(input: AddItemInput) { if (!customer) { // A signed customer is required in order to have a wishlist throw new CommerceError({ diff --git a/framework/bigcommerce/wishlist/use-remove-item.tsx b/framework/bigcommerce/wishlist/use-remove-item.tsx index 86614a21a..d00b3e78b 100644 --- a/framework/bigcommerce/wishlist/use-remove-item.tsx +++ b/framework/bigcommerce/wishlist/use-remove-item.tsx @@ -4,7 +4,7 @@ import { CommerceError } from '@commerce/utils/errors' import useWishlistRemoveItem from '@commerce/wishlist/use-remove-item' import type { RemoveItemBody } from '../api/wishlist' import useCustomer from '../customer/use-customer' -import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist' +import useWishlist from './use-wishlist' const defaultOpts = { url: '/api/bigcommerce/wishlist', @@ -15,7 +15,7 @@ export type RemoveItemInput = { id: string | number } -export const fetcher: HookFetcher = ( +export const fetcher: HookFetcher = ( options, { itemId }, fetch @@ -28,10 +28,10 @@ export const fetcher: HookFetcher = ( } export function extendHook(customFetcher: typeof fetcher) { - const useRemoveItem = (opts?: UseWishlistOptions) => { + const useRemoveItem = (opts?: any) => { const { data: customer } = useCustomer() const { revalidate } = useWishlist(opts) - const fn = useWishlistRemoveItem( + const fn = useWishlistRemoveItem( defaultOpts, customFetcher ) diff --git a/framework/bigcommerce/wishlist/use-wishlist.tsx b/framework/bigcommerce/wishlist/use-wishlist.tsx index dfa3d9dbc..a93f0f6a4 100644 --- a/framework/bigcommerce/wishlist/use-wishlist.tsx +++ b/framework/bigcommerce/wishlist/use-wishlist.tsx @@ -1,4 +1,59 @@ +import { useMemo } from 'react' +import { HookHandler } from '@commerce/utils/types' import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist' +import type { Wishlist } from '../api/wishlist' +import useCustomer from '../customer/use-customer' import type { BigcommerceProvider } from '..' export default useWishlist as UseWishlist + +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] + ) + }, +} diff --git a/framework/commerce/types.d.ts b/framework/commerce/types.d.ts deleted file mode 100644 index 9e69ec25d..000000000 --- a/framework/commerce/types.d.ts +++ /dev/null @@ -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[] -} - -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' diff --git a/framework/commerce/types.ts b/framework/commerce/types.ts index 41aedb228..1f8390535 100644 --- a/framework/commerce/types.ts +++ b/framework/commerce/types.ts @@ -148,3 +148,54 @@ export interface RemoveCartItemBody { export interface RemoveCartItemHandlerBody extends Partial { 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 +} diff --git a/framework/commerce/utils/types.ts b/framework/commerce/utils/types.ts index 98e4ebbae..a06aa0477 100644 --- a/framework/commerce/utils/types.ts +++ b/framework/commerce/utils/types.ts @@ -76,6 +76,29 @@ export type HookHandler< fetcher?: HookFetcherFn } +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 } + useCallback( + fn: (context?: { + input?: HookFetchInput | HookSwrInput + swrOptions?: SwrOptions + }) => Data + ): ResponseState + }): ResponseState & State + fetchOptions: HookFetcherOptions + fetcher?: HookFetcherFn +} + export type SwrOptions = ConfigInterface< Data, CommerceError, diff --git a/framework/commerce/wishlist/use-add-item.tsx b/framework/commerce/wishlist/use-add-item.tsx index f6c069f2b..d9b513694 100644 --- a/framework/commerce/wishlist/use-add-item.tsx +++ b/framework/commerce/wishlist/use-add-item.tsx @@ -1,4 +1,11 @@ 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 const useAddItem = useAction diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index 3d2971eed..83aeaa54c 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -61,7 +61,7 @@ export default function Slug({ return router.isFallback ? (

Loading...

// TODO (BC) Add Skeleton Views ) : ( - + ) } diff --git a/pages/wishlist.tsx b/pages/wishlist.tsx index a3f25d0e7..6de798411 100644 --- a/pages/wishlist.tsx +++ b/pages/wishlist.tsx @@ -44,7 +44,7 @@ export default function Wishlist() { ) : ( data && data.items?.map((item) => ( - + )) )}