diff --git a/components/auth/LoginView.tsx b/components/auth/LoginView.tsx index e2973fb39..f41a128f5 100644 --- a/components/auth/LoginView.tsx +++ b/components/auth/LoginView.tsx @@ -70,8 +70,8 @@ const LoginView: FC = () => { )} - - + + + ) +} + +export default WishlistButton diff --git a/components/wishlist/WishlistButton/index.ts b/components/wishlist/WishlistButton/index.ts new file mode 100644 index 000000000..66e88074b --- /dev/null +++ b/components/wishlist/WishlistButton/index.ts @@ -0,0 +1 @@ +export { default } from './WishlistButton' diff --git a/lib/bigcommerce/api/cart/handlers/add-item.ts b/lib/bigcommerce/api/cart/handlers/add-item.ts index f5475fb08..c37abeb63 100644 --- a/lib/bigcommerce/api/cart/handlers/add-item.ts +++ b/lib/bigcommerce/api/cart/handlers/add-item.ts @@ -1,4 +1,4 @@ -import parseItem from '../../utils/parse-item' +import { parseCartItem } from '../../utils/parse-item' import getCartCookie from '../../utils/get-cart-cookie' import type { CartHandlers } from '..' @@ -19,7 +19,7 @@ const addItem: CartHandlers['addItem'] = async ({ const options = { method: 'POST', body: JSON.stringify({ - line_items: [parseItem(item)], + line_items: [parseCartItem(item)], }), } const { data } = cartId diff --git a/lib/bigcommerce/api/cart/handlers/update-item.ts b/lib/bigcommerce/api/cart/handlers/update-item.ts index 0a9fcaca7..c64c111df 100644 --- a/lib/bigcommerce/api/cart/handlers/update-item.ts +++ b/lib/bigcommerce/api/cart/handlers/update-item.ts @@ -1,4 +1,4 @@ -import parseItem from '../../utils/parse-item' +import { parseCartItem } from '../../utils/parse-item' import getCartCookie from '../../utils/get-cart-cookie' import type { CartHandlers } from '..' @@ -20,7 +20,7 @@ const updateItem: CartHandlers['updateItem'] = async ({ { method: 'PUT', body: JSON.stringify({ - line_item: parseItem(item), + line_item: parseCartItem(item), }), } ) diff --git a/lib/bigcommerce/api/cart/index.ts b/lib/bigcommerce/api/cart/index.ts index 4f6062674..c8a4913d6 100644 --- a/lib/bigcommerce/api/cart/index.ts +++ b/lib/bigcommerce/api/cart/index.ts @@ -9,8 +9,6 @@ import addItem from './handlers/add-item' import updateItem from './handlers/update-item' import removeItem from './handlers/remove-item' -type Body = Partial | undefined - export type ItemBody = { productId: number variantId: number @@ -46,14 +44,14 @@ export type Cart = { export type CartHandlers = { getCart: BigcommerceHandler - addItem: BigcommerceHandler> + addItem: BigcommerceHandler> updateItem: BigcommerceHandler< Cart, - { cartId?: string } & Body + { cartId?: string } & Partial > removeItem: BigcommerceHandler< Cart, - { cartId?: string } & Body + { cartId?: string } & Partial > } diff --git a/lib/bigcommerce/api/customers/handlers/get-logged-in-customer.ts b/lib/bigcommerce/api/customers/handlers/get-logged-in-customer.ts index b86a8a01a..698235dda 100644 --- a/lib/bigcommerce/api/customers/handlers/get-logged-in-customer.ts +++ b/lib/bigcommerce/api/customers/handlers/get-logged-in-customer.ts @@ -1,4 +1,4 @@ -import type { GetLoggedInCustomerQuery } from '@lib/bigcommerce/schema' +import type { GetLoggedInCustomerQuery } from '../../../schema' import type { CustomersHandlers } from '..' export const getLoggedInCustomerQuery = /* GraphQL */ ` diff --git a/lib/bigcommerce/api/customers/handlers/login.ts b/lib/bigcommerce/api/customers/handlers/login.ts index e744acc31..d280a88d8 100644 --- a/lib/bigcommerce/api/customers/handlers/login.ts +++ b/lib/bigcommerce/api/customers/handlers/login.ts @@ -1,4 +1,4 @@ -import { FetcherError } from '@lib/commerce/utils/errors' +import { FetcherError } from '../../../../commerce/utils/errors' import login from '../../operations/login' import type { LoginHandlers } from '../login' diff --git a/lib/bigcommerce/api/index.ts b/lib/bigcommerce/api/index.ts index 78118b432..78215d4d6 100644 --- a/lib/bigcommerce/api/index.ts +++ b/lib/bigcommerce/api/index.ts @@ -1,5 +1,5 @@ import type { RequestInit } from '@vercel/fetch' -import type { CommerceAPIConfig } from 'lib/commerce/api' +import type { CommerceAPIConfig } from '../../commerce/api' import fetchGraphqlApi from './utils/fetch-graphql-api' import fetchStoreApi from './utils/fetch-store-api' diff --git a/lib/bigcommerce/api/operations/get-all-pages.ts b/lib/bigcommerce/api/operations/get-all-pages.ts index 5aa5bde2d..9fe83f2a1 100644 --- a/lib/bigcommerce/api/operations/get-all-pages.ts +++ b/lib/bigcommerce/api/operations/get-all-pages.ts @@ -13,7 +13,7 @@ async function getAllPages(opts?: { preview?: boolean }): Promise -async function getAllPages(opts: { +async function getAllPages(opts: { url: string config?: BigcommerceConfig preview?: boolean diff --git a/lib/bigcommerce/api/operations/get-all-product-paths.ts b/lib/bigcommerce/api/operations/get-all-product-paths.ts index 9aea42126..71522be35 100644 --- a/lib/bigcommerce/api/operations/get-all-product-paths.ts +++ b/lib/bigcommerce/api/operations/get-all-product-paths.ts @@ -1,7 +1,7 @@ import type { GetAllProductPathsQuery, GetAllProductPathsQueryVariables, -} from 'lib/bigcommerce/schema' +} from '../../schema' import type { RecursivePartial, RecursiveRequired } from '../utils/types' import filterEdges from '../utils/filter-edges' import { BigcommerceConfig, getConfig } from '..' diff --git a/lib/bigcommerce/api/operations/get-all-products.ts b/lib/bigcommerce/api/operations/get-all-products.ts index dbd439d9e..f67d62831 100644 --- a/lib/bigcommerce/api/operations/get-all-products.ts +++ b/lib/bigcommerce/api/operations/get-all-products.ts @@ -1,7 +1,7 @@ import type { GetAllProductsQuery, GetAllProductsQueryVariables, -} from '@lib/bigcommerce/schema' +} from '../../schema' import type { RecursivePartial, RecursiveRequired } from '../utils/types' import filterEdges from '../utils/filter-edges' import setProductLocaleMeta from '../utils/set-product-locale-meta' diff --git a/lib/bigcommerce/api/operations/get-customer-id.ts b/lib/bigcommerce/api/operations/get-customer-id.ts new file mode 100644 index 000000000..7fe84093e --- /dev/null +++ b/lib/bigcommerce/api/operations/get-customer-id.ts @@ -0,0 +1,34 @@ +import { GetCustomerIdQuery } from '../../schema' +import { BigcommerceConfig, getConfig } from '..' + +export const getCustomerIdQuery = /* GraphQL */ ` + query getCustomerId { + customer { + entityId + } + } +` + +async function getCustomerId({ + customerToken, + config, +}: { + customerToken: string + config?: BigcommerceConfig +}): Promise { + config = getConfig(config) + + const { data } = await config.fetch( + getCustomerIdQuery, + undefined, + { + headers: { + cookie: `${config.customerCookie}=${customerToken}`, + }, + } + ) + + return data?.customer?.entityId +} + +export default getCustomerId diff --git a/lib/bigcommerce/api/operations/get-customer-wishlist.ts b/lib/bigcommerce/api/operations/get-customer-wishlist.ts new file mode 100644 index 000000000..093b136f1 --- /dev/null +++ b/lib/bigcommerce/api/operations/get-customer-wishlist.ts @@ -0,0 +1,51 @@ +import type { RecursivePartial, RecursiveRequired } from '../utils/types' +import { BigcommerceConfig, getConfig } from '..' +import { definitions } from '../definitions/wishlist' + +export type Wishlist = definitions['wishlist_Full'] + +export type GetCustomerWishlistResult< + T extends { wishlist?: any } = { wishlist?: Wishlist } +> = T + +export type GetCustomerWishlistVariables = { + customerId: number +} + +async function getCustomerWishlist(opts: { + variables: GetCustomerWishlistVariables + config?: BigcommerceConfig + preview?: boolean +}): Promise + +async function getCustomerWishlist< + T extends { wishlist?: any }, + V = any +>(opts: { + url: string + variables: V + config?: BigcommerceConfig + preview?: boolean +}): Promise> + +async function getCustomerWishlist({ + config, + variables, +}: { + url?: string + variables: GetCustomerWishlistVariables + config?: BigcommerceConfig + preview?: boolean +}): Promise { + config = getConfig(config) + + const { data } = await config.storeApiFetch< + RecursivePartial<{ data: Wishlist[] }> + >(`/v3/wishlists?customer_id=${variables.customerId}`) + const wishlists = (data as RecursiveRequired) ?? [] + const wishlist = wishlists[0] + + return { wishlist } +} + +export default getCustomerWishlist diff --git a/lib/bigcommerce/api/operations/get-product.ts b/lib/bigcommerce/api/operations/get-product.ts index 229d402bc..5071f68ec 100644 --- a/lib/bigcommerce/api/operations/get-product.ts +++ b/lib/bigcommerce/api/operations/get-product.ts @@ -1,7 +1,4 @@ -import type { - GetProductQuery, - GetProductQueryVariables, -} from 'lib/bigcommerce/schema' +import type { GetProductQuery, GetProductQueryVariables } from '../../schema' import type { RecursivePartial, RecursiveRequired } from '../utils/types' import setProductLocaleMeta from '../utils/set-product-locale-meta' import { productInfoFragment } from '../fragments/product' diff --git a/lib/bigcommerce/api/operations/get-site-info.ts b/lib/bigcommerce/api/operations/get-site-info.ts index cc9063b07..0a6e84f0f 100644 --- a/lib/bigcommerce/api/operations/get-site-info.ts +++ b/lib/bigcommerce/api/operations/get-site-info.ts @@ -1,7 +1,4 @@ -import type { - GetSiteInfoQuery, - GetSiteInfoQueryVariables, -} from 'lib/bigcommerce/schema' +import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../../schema' import type { RecursivePartial, RecursiveRequired } from '../utils/types' import filterEdges from '../utils/filter-edges' import { BigcommerceConfig, getConfig } from '..' diff --git a/lib/bigcommerce/api/operations/login.ts b/lib/bigcommerce/api/operations/login.ts index 04faba0fe..e16502af0 100644 --- a/lib/bigcommerce/api/operations/login.ts +++ b/lib/bigcommerce/api/operations/login.ts @@ -1,8 +1,5 @@ import type { ServerResponse } from 'http' -import type { - LoginMutation, - LoginMutationVariables, -} from 'lib/bigcommerce/schema' +import type { LoginMutation, LoginMutationVariables } from '../../schema' import type { RecursivePartial } from '../utils/types' import concatHeader from '../utils/concat-cookie' import { BigcommerceConfig, getConfig } from '..' diff --git a/lib/bigcommerce/api/utils/fetch-graphql-api.ts b/lib/bigcommerce/api/utils/fetch-graphql-api.ts index 049a01e33..02df1337c 100644 --- a/lib/bigcommerce/api/utils/fetch-graphql-api.ts +++ b/lib/bigcommerce/api/utils/fetch-graphql-api.ts @@ -1,7 +1,6 @@ -import { FetcherError } from '@lib/commerce/utils/errors' -import type { GraphQLFetcher } from '@lib/commerce/api' +import { FetcherError } from '../../../commerce/utils/errors' +import type { GraphQLFetcher } from '../../../commerce/api' import { getConfig } from '..' -import log from '@lib/logger' import fetch from './fetch' const fetchGraphqlApi: GraphQLFetcher = async ( diff --git a/lib/bigcommerce/api/utils/fetch-store-api.ts b/lib/bigcommerce/api/utils/fetch-store-api.ts index 22b3e5a99..7e59b9f06 100644 --- a/lib/bigcommerce/api/utils/fetch-store-api.ts +++ b/lib/bigcommerce/api/utils/fetch-store-api.ts @@ -41,7 +41,7 @@ export default async function fetchStoreApi( throw new BigcommerceApiError(msg, res, data) } - if (!isJSON) { + if (res.status !== 204 && !isJSON) { throw new BigcommerceApiError( `Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`, res diff --git a/lib/bigcommerce/api/utils/parse-item.ts b/lib/bigcommerce/api/utils/parse-item.ts index 02c27bea8..2a2c87dde 100644 --- a/lib/bigcommerce/api/utils/parse-item.ts +++ b/lib/bigcommerce/api/utils/parse-item.ts @@ -1,9 +1,13 @@ +import type { ItemBody as WishlistItemBody } from '../wishlist' import type { ItemBody } from '../cart' -const parseItem = (item: ItemBody) => ({ - quantity: item.quantity, +export const parseWishlistItem = (item: WishlistItemBody) => ({ product_id: item.productId, variant_id: item.variantId, }) -export default parseItem +export const parseCartItem = (item: ItemBody) => ({ + quantity: item.quantity, + product_id: item.productId, + variant_id: item.variantId, +}) diff --git a/lib/bigcommerce/api/wishlist/handlers/add-item.ts b/lib/bigcommerce/api/wishlist/handlers/add-item.ts index d6678031f..a02ef4434 100644 --- a/lib/bigcommerce/api/wishlist/handlers/add-item.ts +++ b/lib/bigcommerce/api/wishlist/handlers/add-item.ts @@ -1,9 +1,12 @@ import type { WishlistHandlers } from '..' +import getCustomerId from '../../operations/get-customer-id' +import getCustomerWishlist from '../../operations/get-customer-wishlist' +import { parseWishlistItem } from '../../utils/parse-item' -// Return current wishlist info +// Returns the wishlist of the signed customer const addItem: WishlistHandlers['addItem'] = async ({ res, - body: { wishlistId, item }, + body: { customerToken, item }, config, }) => { if (!item) { @@ -13,16 +16,39 @@ const addItem: WishlistHandlers['addItem'] = async ({ }) } + const customerId = + customerToken && (await getCustomerId({ customerToken, config })) + + if (!customerId) { + return res.status(400).json({ + data: null, + errors: [{ message: 'Invalid request' }], + }) + } + + const { wishlist } = await getCustomerWishlist({ + variables: { customerId }, + config, + }) const options = { method: 'POST', - body: JSON.stringify({ - items: [item], - }), + body: JSON.stringify( + wishlist + ? { + items: [parseWishlistItem(item)], + } + : { + name: 'Wishlist', + customer_id: customerId, + items: [parseWishlistItem(item)], + is_public: false, + } + ), } - const { data } = await config.storeApiFetch( - `/v3/wishlists/${wishlistId}/items`, - options - ) + + const { data } = wishlist + ? await config.storeApiFetch(`/v3/wishlists/${wishlist.id}/items`, options) + : await config.storeApiFetch('/v3/wishlists', options) res.status(200).json({ data }) } diff --git a/lib/bigcommerce/api/wishlist/handlers/get-wishlist.ts b/lib/bigcommerce/api/wishlist/handlers/get-wishlist.ts index 5d0aa19c5..a77cf4467 100644 --- a/lib/bigcommerce/api/wishlist/handlers/get-wishlist.ts +++ b/lib/bigcommerce/api/wishlist/handlers/get-wishlist.ts @@ -1,17 +1,32 @@ +import getCustomerId from '../../operations/get-customer-id' import type { Wishlist, WishlistHandlers } from '..' +import getCustomerWishlist from '../../operations/get-customer-wishlist' // Return wishlist info const getWishlist: WishlistHandlers['getWishlist'] = async ({ res, - body: { wishlistId }, + body: { customerToken }, config, }) => { let result: { data?: Wishlist } = {} - try { - result = await config.storeApiFetch(`/v3/wishlists/${wishlistId}`) - } catch (error) { - throw error + if (customerToken) { + const customerId = + customerToken && (await getCustomerId({ customerToken, config })) + + if (!customerId) { + // If the customerToken is invalid, then this request is too + return res.status(404).json({ + data: null, + errors: [{ message: 'Wishlist not found' }], + }) + } + + const { wishlist } = await getCustomerWishlist({ + variables: { customerId }, + config, + }) + result = { data: wishlist } } res.status(200).json({ data: result.data ?? null }) diff --git a/lib/bigcommerce/api/wishlist/handlers/remove-item.ts b/lib/bigcommerce/api/wishlist/handlers/remove-item.ts index 9be970f11..29b6eff60 100644 --- a/lib/bigcommerce/api/wishlist/handlers/remove-item.ts +++ b/lib/bigcommerce/api/wishlist/handlers/remove-item.ts @@ -1,20 +1,34 @@ +import getCustomerId from '../../operations/get-customer-id' +import getCustomerWishlist, { + Wishlist, +} from '../../operations/get-customer-wishlist' import type { WishlistHandlers } from '..' // Return current wishlist info const removeItem: WishlistHandlers['removeItem'] = async ({ res, - body: { wishlistId, itemId }, + body: { customerToken, itemId }, config, }) => { - if (!wishlistId || !itemId) { + const customerId = + customerToken && (await getCustomerId({ customerToken, config })) + const { wishlist } = + (customerId && + (await getCustomerWishlist({ + variables: { customerId }, + config, + }))) || + {} + + if (!wishlist || !itemId) { return res.status(400).json({ data: null, errors: [{ message: 'Invalid request' }], }) } - const result = await config.storeApiFetch<{ data: any } | null>( - `/v3/wishlists/${wishlistId}/items/${itemId}`, + const result = await config.storeApiFetch<{ data: Wishlist } | null>( + `/v3/wishlists/${wishlist.id}/items/${itemId}`, { method: 'DELETE' } ) const data = result?.data ?? null diff --git a/lib/bigcommerce/api/wishlist/index.ts b/lib/bigcommerce/api/wishlist/index.ts index 3bcba106b..79f0a9c1a 100644 --- a/lib/bigcommerce/api/wishlist/index.ts +++ b/lib/bigcommerce/api/wishlist/index.ts @@ -5,23 +5,18 @@ import createApiHandler, { } from '../utils/create-api-handler' import { BigcommerceApiError } from '../utils/errors' import getWishlist from './handlers/get-wishlist' -import getAllWishlists from './handlers/get-all-wishlists' import addItem from './handlers/add-item' import removeItem from './handlers/remove-item' -import updateWishlist from './handlers/update-wishlist' -import removeWishlist from './handlers/remove-wishlist' -import addWishlist from './handlers/add-wishlist' - -type Body = Partial | undefined +import { definitions } from '../definitions/wishlist' export type ItemBody = { - product_id: number - variant_id: number + productId: number + variantId: number } -export type AddItemBody = { wishlistId: string; item: ItemBody } +export type AddItemBody = { item: ItemBody } -export type RemoveItemBody = { wishlistId: string; itemId: string } +export type RemoveItemBody = { itemId: string } export type WishlistBody = { customer_id: number @@ -32,41 +27,21 @@ export type WishlistBody = { export type AddWishlistBody = { wishlist: WishlistBody } -// TODO: this type should match: -// https://developer.bigcommerce.com/api-reference/store-management/wishlists/wishlists/wishlistsbyidget -export type Wishlist = { - id: string - customer_id: number - name: string - is_public: boolean - token: string - items: any[] - // TODO: add missing fields -} +export type Wishlist = definitions['wishlist_Full'] export type WishlistHandlers = { - getAllWishlists: BigcommerceHandler - getWishlist: BigcommerceHandler - addWishlist: BigcommerceHandler< - Wishlist, - { wishlistId: string } & Body - > - updateWishlist: BigcommerceHandler< - Wishlist, - { wishlistId: string } & Body - > + getWishlist: BigcommerceHandler addItem: BigcommerceHandler< Wishlist, - { wishlistId: string } & Body + { customerToken?: string } & Partial > removeItem: BigcommerceHandler< Wishlist, - { wishlistId: string } & Body + { customerToken?: string } & Partial > - removeWishlist: BigcommerceHandler } -const METHODS = ['GET', 'POST', 'PUT', 'DELETE'] +const METHODS = ['GET', 'POST', 'DELETE'] // TODO: a complete implementation should have schema validation for `req.body` const wishlistApi: BigcommerceApiHandler = async ( @@ -77,57 +52,27 @@ const wishlistApi: BigcommerceApiHandler = async ( ) => { if (!isAllowedMethod(req, res, METHODS)) return + const { cookies } = req + const customerToken = cookies[config.customerCookie] + try { - const { wishlistId, itemId, customerId } = req.body // Return current wishlist info - if (req.method === 'GET' && wishlistId) { - const body = { wishlistId: wishlistId as string } + if (req.method === 'GET') { + const body = { customerToken } return await handlers['getWishlist']({ req, res, config, body }) } // Add an item to the wishlist - if (req.method === 'POST' && wishlistId) { - const body = { ...req.body, wishlistId } + if (req.method === 'POST') { + const body = { ...req.body, customerToken } return await handlers['addItem']({ req, res, config, body }) } - // Update a wishlist - if (req.method === 'PUT' && wishlistId) { - const body = { ...req.body, wishlistId } - return await handlers['updateWishlist']({ req, res, config, body }) - } - // Remove an item from the wishlist - if (req.method === 'DELETE' && wishlistId && itemId) { - const body = { - wishlistId: wishlistId as string, - itemId: itemId as string, - } + if (req.method === 'DELETE') { + const body = { ...req.body, customerToken } return await handlers['removeItem']({ req, res, config, body }) } - - // Remove the wishlist - if (req.method === 'DELETE' && wishlistId && !itemId) { - const body = { wishlistId: wishlistId as string } - return await handlers['removeWishlist']({ req, res, config, body }) - } - - // Get all the wishlists - if (req.method === 'GET' && !wishlistId) { - const body = { customerId: customerId as string } - return await handlers['getAllWishlists']({ - req, - res: res as any, - config, - body, - }) - } - - // Create a wishlist - if (req.method === 'POST' && !wishlistId) { - const { body } = req - return await handlers['addWishlist']({ req, res, config, body }) - } } catch (error) { console.error(error) @@ -143,11 +88,7 @@ const wishlistApi: BigcommerceApiHandler = async ( export const handlers = { getWishlist, addItem, - updateWishlist, removeItem, - removeWishlist, - getAllWishlists, - addWishlist, } export default createApiHandler(wishlistApi, handlers, {}) diff --git a/lib/bigcommerce/cart/use-add-item.tsx b/lib/bigcommerce/cart/use-add-item.tsx index 36fbec7d2..75e8ec3e6 100644 --- a/lib/bigcommerce/cart/use-add-item.tsx +++ b/lib/bigcommerce/cart/use-add-item.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react' -import type { HookFetcher } from '@lib/commerce/utils/types' -import { CommerceError } from '@lib/commerce/utils/errors' -import useCartAddItem from '@lib/commerce/cart/use-add-item' +import type { HookFetcher } from '../../commerce/utils/types' +import { CommerceError } from '../../commerce/utils/errors' +import useCartAddItem from '../../commerce/cart/use-add-item' import type { ItemBody, AddItemBody } from '../api/cart' import useCart, { Cart } from './use-cart' @@ -36,7 +36,7 @@ export const fetcher: HookFetcher = ( export function extendHook(customFetcher: typeof fetcher) { const useAddItem = () => { const { mutate } = useCart() - const fn = useCartAddItem(defaultOpts, customFetcher) + const fn = useCartAddItem(defaultOpts, customFetcher) return useCallback( async function addItem(input: AddItemInput) { diff --git a/lib/bigcommerce/cart/use-cart.tsx b/lib/bigcommerce/cart/use-cart.tsx index ce0800ca3..a80d3e091 100644 --- a/lib/bigcommerce/cart/use-cart.tsx +++ b/lib/bigcommerce/cart/use-cart.tsx @@ -1,6 +1,6 @@ -import type { HookFetcher } from '@lib/commerce/utils/types' -import type { SwrOptions } from '@lib/commerce/utils/use-data' -import useCommerceCart, { CartInput } from '@lib/commerce/cart/use-cart' +import type { HookFetcher } from '../../commerce/utils/types' +import type { SwrOptions } from '../../commerce/utils/use-data' +import useCommerceCart, { CartInput } from '../../commerce/cart/use-cart' import type { Cart } from '../api/cart' const defaultOpts = { @@ -23,23 +23,23 @@ export function extendHook( swrOptions?: SwrOptions ) { const useCart = () => { - const cart = useCommerceCart(defaultOpts, [], customFetcher, { + const response = useCommerceCart(defaultOpts, [], customFetcher, { revalidateOnFocus: false, ...swrOptions, }) // Uses a getter to only calculate the prop when required - // cart.data is also a getter and it's better to not trigger it early - Object.defineProperty(cart, 'isEmpty', { + // response.data is also a getter and it's better to not trigger it early + Object.defineProperty(response, 'isEmpty', { get() { - return Object.values(cart.data?.line_items ?? {}).every( + return Object.values(response.data?.line_items ?? {}).every( (items) => !items.length ) }, set: (x) => x, }) - return cart + return response } useCart.extend = extendHook diff --git a/lib/bigcommerce/cart/use-remove-item.tsx b/lib/bigcommerce/cart/use-remove-item.tsx index c07aa718e..319b722d3 100644 --- a/lib/bigcommerce/cart/use-remove-item.tsx +++ b/lib/bigcommerce/cart/use-remove-item.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react' -import { HookFetcher } from '@lib/commerce/utils/types' -import useCartRemoveItem from '@lib/commerce/cart/use-remove-item' +import { HookFetcher } from '../../commerce/utils/types' +import useCartRemoveItem from '../../commerce/cart/use-remove-item' import type { RemoveItemBody } from '../api/cart' import useCart, { Cart } from './use-cart' @@ -26,7 +26,7 @@ export const fetcher: HookFetcher = ( } export function extendHook(customFetcher: typeof fetcher) { - const useRemoveItem = (item?: any) => { + const useRemoveItem = () => { const { mutate } = useCart() const fn = useCartRemoveItem( defaultOpts, @@ -35,7 +35,7 @@ export function extendHook(customFetcher: typeof fetcher) { return useCallback( async function removeItem(input: RemoveItemInput) { - const data = await fn({ itemId: input.id ?? item?.id }) + const data = await fn({ itemId: input.id }) await mutate(data, false) return data }, diff --git a/lib/bigcommerce/cart/use-update-item.tsx b/lib/bigcommerce/cart/use-update-item.tsx index 875c241f8..e335677d6 100644 --- a/lib/bigcommerce/cart/use-update-item.tsx +++ b/lib/bigcommerce/cart/use-update-item.tsx @@ -1,8 +1,8 @@ import { useCallback } from 'react' import debounce from 'lodash.debounce' -import type { HookFetcher } from '@lib/commerce/utils/types' -import { CommerceError } from '@lib/commerce/utils/errors' -import useCartUpdateItem from '@lib/commerce/cart/use-update-item' +import type { HookFetcher } from '../../commerce/utils/types' +import { CommerceError } from '../../commerce/utils/errors' +import useCartUpdateItem from '../../commerce/cart/use-update-item' import type { ItemBody, UpdateItemBody } from '../api/cart' import { fetcher as removeFetcher } from './use-remove-item' import useCart, { Cart } from './use-cart' diff --git a/lib/bigcommerce/index.tsx b/lib/bigcommerce/index.tsx index f02d7d5ab..96faef82b 100644 --- a/lib/bigcommerce/index.tsx +++ b/lib/bigcommerce/index.tsx @@ -3,8 +3,8 @@ import { CommerceConfig, CommerceProvider as CoreCommerceProvider, useCommerce as useCoreCommerce, -} from 'lib/commerce' -import { FetcherError } from '@lib/commerce/utils/errors' +} from '../commerce' +import { FetcherError } from '../commerce/utils/errors' async function getText(res: Response) { try { diff --git a/lib/bigcommerce/products/use-search.tsx b/lib/bigcommerce/products/use-search.tsx index e4b1e9b91..f90051887 100644 --- a/lib/bigcommerce/products/use-search.tsx +++ b/lib/bigcommerce/products/use-search.tsx @@ -1,6 +1,6 @@ -import type { HookFetcher } from '@lib/commerce/utils/types' -import type { SwrOptions } from '@lib/commerce/utils/use-data' -import useCommerceSearch from '@lib/commerce/products/use-search' +import type { HookFetcher } from '../../commerce/utils/types' +import type { SwrOptions } from '../../commerce/utils/use-data' +import useCommerceSearch from '../../commerce/products/use-search' import type { SearchProductsData } from '../api/catalog/products' const defaultOpts = { diff --git a/lib/bigcommerce/schema.d.ts b/lib/bigcommerce/schema.d.ts index 69cff1ea6..aaafbe312 100644 --- a/lib/bigcommerce/schema.d.ts +++ b/lib/bigcommerce/schema.d.ts @@ -1886,6 +1886,12 @@ export type GetAllProductsQuery = { __typename?: 'Query' } & { } } +export type GetCustomerIdQueryVariables = Exact<{ [key: string]: never }> + +export type GetCustomerIdQuery = { __typename?: 'Query' } & { + customer?: Maybe<{ __typename?: 'Customer' } & Pick> +} + export type GetProductQueryVariables = Exact<{ hasLocale?: Maybe locale?: Maybe diff --git a/lib/bigcommerce/use-customer.tsx b/lib/bigcommerce/use-customer.tsx index 173f1db9c..8f99d5cde 100644 --- a/lib/bigcommerce/use-customer.tsx +++ b/lib/bigcommerce/use-customer.tsx @@ -1,6 +1,6 @@ -import type { HookFetcher } from '@lib/commerce/utils/types' -import type { SwrOptions } from '@lib/commerce/utils/use-data' -import useCommerceCustomer from '@lib/commerce/use-customer' +import type { HookFetcher } from '../commerce/utils/types' +import type { SwrOptions } from '../commerce/utils/use-data' +import useCommerceCustomer from '../commerce/use-customer' import type { Customer, CustomerData } from './api/customers' const defaultOpts = { diff --git a/lib/bigcommerce/use-login.tsx b/lib/bigcommerce/use-login.tsx index fa91e54b9..b74a0f976 100644 --- a/lib/bigcommerce/use-login.tsx +++ b/lib/bigcommerce/use-login.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react' -import type { HookFetcher } from '@lib/commerce/utils/types' -import { CommerceError } from '@lib/commerce/utils/errors' -import useCommerceLogin from '@lib/commerce/use-login' +import type { HookFetcher } from '../commerce/utils/types' +import { CommerceError } from '../commerce/utils/errors' +import useCommerceLogin from '../commerce/use-login' import type { LoginBody } from './api/customers/login' import useCustomer from './use-customer' diff --git a/lib/bigcommerce/use-logout.tsx b/lib/bigcommerce/use-logout.tsx index d12afb4d6..fda8e4c69 100644 --- a/lib/bigcommerce/use-logout.tsx +++ b/lib/bigcommerce/use-logout.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react' -import type { HookFetcher } from '@lib/commerce/utils/types' -import useCommerceLogout from '@lib/commerce/use-logout' +import type { HookFetcher } from '../commerce/utils/types' +import useCommerceLogout from '../commerce/use-logout' import useCustomer from './use-customer' const defaultOpts = { diff --git a/lib/bigcommerce/use-signup.tsx b/lib/bigcommerce/use-signup.tsx index 4e98e7705..9ff52a57a 100644 --- a/lib/bigcommerce/use-signup.tsx +++ b/lib/bigcommerce/use-signup.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react' -import type { HookFetcher } from '@lib/commerce/utils/types' -import { CommerceError } from '@lib/commerce/utils/errors' -import useCommerceSignup from '@lib/commerce/use-signup' +import type { HookFetcher } from '../commerce/utils/types' +import { CommerceError } from '../commerce/utils/errors' +import useCommerceSignup from '../commerce/use-signup' import type { SignupBody } from './api/customers/signup' import useCustomer from './use-customer' diff --git a/lib/bigcommerce/wishlist/use-add-item.tsx b/lib/bigcommerce/wishlist/use-add-item.tsx index 9bbbe009e..c5f608579 100644 --- a/lib/bigcommerce/wishlist/use-add-item.tsx +++ b/lib/bigcommerce/wishlist/use-add-item.tsx @@ -1,7 +1,9 @@ import { useCallback } from 'react' -import { HookFetcher } from '@lib/commerce/utils/types' -import useAction from '@lib/commerce/utils/use-action' +import { HookFetcher } from '../../commerce/utils/types' +import { CommerceError } from '../../commerce/utils/errors' +import useWishlistAddItem from '../../commerce/wishlist/use-add-item' import type { ItemBody, AddItemBody } from '../api/wishlist' +import useCustomer from '../use-customer' import useWishlist, { Wishlist } from './use-wishlist' const defaultOpts = { @@ -13,28 +15,37 @@ export type AddItemInput = ItemBody export const fetcher: HookFetcher = ( options, - { wishlistId, item }, + { item }, fetch ) => { + // TODO: add validations before doing the fetch return fetch({ ...defaultOpts, ...options, - body: { wishlistId, item }, + body: { item }, }) } export function extendHook(customFetcher: typeof fetcher) { - const useAddItem = (wishlistId: string) => { - const { mutate } = useWishlist(wishlistId) - const fn = useAction(defaultOpts, customFetcher) + const useAddItem = () => { + const { data: customer } = useCustomer() + const { mutate } = useWishlist() + const fn = useWishlistAddItem(defaultOpts, customFetcher) return useCallback( async function addItem(input: AddItemInput) { - const data = await fn({ wishlistId, item: input }) + if (!customer) { + // A signed customer is required in order to have a wishlist + throw new CommerceError({ + message: 'Signed customer not found', + }) + } + + const data = await fn({ item: input }) await mutate(data, false) return data }, - [fn, mutate] + [fn, mutate, customer] ) } diff --git a/lib/bigcommerce/wishlist/use-remove-item.tsx b/lib/bigcommerce/wishlist/use-remove-item.tsx index 05f9744cb..f8a4ddba9 100644 --- a/lib/bigcommerce/wishlist/use-remove-item.tsx +++ b/lib/bigcommerce/wishlist/use-remove-item.tsx @@ -1,45 +1,55 @@ import { useCallback } from 'react' -import { HookFetcher } from '@lib/commerce/utils/types' -import useAction from '@lib/commerce/utils/use-action' +import { HookFetcher } from '../../commerce/utils/types' +import { CommerceError } from '../../commerce/utils/errors' +import useWishlistRemoveItem from '../../commerce/wishlist/use-remove-item' import type { RemoveItemBody } from '../api/wishlist' +import useCustomer from '../use-customer' import useWishlist, { Wishlist } from './use-wishlist' const defaultOpts = { - url: '/api/bigcommerce/wishlists', + url: '/api/bigcommerce/wishlist', method: 'DELETE', } export type RemoveItemInput = { - id: string + id: string | number } export const fetcher: HookFetcher = ( options, - { wishlistId, itemId }, + { itemId }, fetch ) => { return fetch({ ...defaultOpts, ...options, - body: { wishlistId, itemId }, + body: { itemId }, }) } export function extendHook(customFetcher: typeof fetcher) { - const useRemoveItem = (wishlistId: string, item?: any) => { - const { mutate } = useWishlist(wishlistId) - const fn = useAction( + const useRemoveItem = () => { + const { data: customer } = useCustomer() + const { mutate } = useWishlist() + const fn = useWishlistRemoveItem( defaultOpts, customFetcher ) return useCallback( async function removeItem(input: RemoveItemInput) { - const data = await fn({ wishlistId, itemId: input.id ?? item?.id }) + if (!customer) { + // A signed customer is required in order to have a wishlist + throw new CommerceError({ + message: 'Signed customer not found', + }) + } + + const data = await fn({ itemId: String(input.id) }) await mutate(data, false) return data }, - [fn, mutate] + [fn, mutate, customer] ) } diff --git a/lib/bigcommerce/wishlist/use-wishlist-actions.tsx b/lib/bigcommerce/wishlist/use-wishlist-actions.tsx index 7301994b0..711d00516 100644 --- a/lib/bigcommerce/wishlist/use-wishlist-actions.tsx +++ b/lib/bigcommerce/wishlist/use-wishlist-actions.tsx @@ -3,9 +3,9 @@ import useRemoveItem from './use-remove-item' // This hook is probably not going to be used, but it's here // to show how a commerce should be structuring it -export default function useWishlistActions(wishlistId: string) { - const addItem = useAddItem(wishlistId) - const removeItem = useRemoveItem(wishlistId) +export default function useWishlistActions() { + const addItem = useAddItem() + const removeItem = useRemoveItem() return { addItem, removeItem } } diff --git a/lib/bigcommerce/wishlist/use-wishlist.tsx b/lib/bigcommerce/wishlist/use-wishlist.tsx index d60660c05..ac2f9d496 100644 --- a/lib/bigcommerce/wishlist/use-wishlist.tsx +++ b/lib/bigcommerce/wishlist/use-wishlist.tsx @@ -1,36 +1,48 @@ -import { HookFetcher } from '@lib/commerce/utils/types' -import useData from '@lib/commerce/utils/use-data' +import { HookFetcher } from '../../commerce/utils/types' +import { SwrOptions } from '../../commerce/utils/use-data' +import useCommerceWishlist from '../../commerce/wishlist/use-wishlist' import type { Wishlist } from '../api/wishlist' +import useCustomer from '../use-customer' const defaultOpts = { - url: '/api/bigcommerce/wishlists', + url: '/api/bigcommerce/wishlist', method: 'GET', } export type { Wishlist } -export type WishlistInput = { - wishlistId: string | undefined -} - -export const fetcher: HookFetcher = ( +export const fetcher: HookFetcher = ( options, - { wishlistId }, + { customerId }, fetch ) => { - return fetch({ - ...defaultOpts, - ...options, - body: { wishlistId }, - }) + return customerId ? fetch({ ...defaultOpts, ...options }) : null } -export function extendHook(customFetcher: typeof fetcher) { - const useWishlists = (wishlistId: string) => { - const fetchFn: typeof fetcher = (options, input, fetch) => { - return customFetcher(options, input, fetch) - } - const response = useData(defaultOpts, [['wishlistId', wishlistId]], fetchFn) +export function extendHook( + customFetcher: typeof fetcher, + swrOptions?: SwrOptions +) { + const useWishlists = () => { + const { data: customer } = useCustomer() + const response = useCommerceWishlist( + defaultOpts, + [['customerId', customer?.entityId]], + customFetcher, + { + revalidateOnFocus: false, + ...swrOptions, + } + ) + + // Uses a getter to only calculate the prop when required + // response.data is also a getter and it's better to not trigger it early + Object.defineProperty(response, 'isEmpty', { + get() { + return (response.data?.items?.length || 0) > 0 + }, + set: (x) => x, + }) return response } diff --git a/lib/commerce/cart/use-cart.tsx b/lib/commerce/cart/use-cart.tsx index 6649b20d0..8aefc3e68 100644 --- a/lib/commerce/cart/use-cart.tsx +++ b/lib/commerce/cart/use-cart.tsx @@ -4,7 +4,7 @@ import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types' import useData, { SwrOptions } from '../utils/use-data' import { useCommerce } from '..' -export type CartResponse = responseInterface & { +export type CartResponse = responseInterface & { isEmpty: boolean } diff --git a/lib/commerce/wishlist/use-add-item.tsx b/lib/commerce/wishlist/use-add-item.tsx new file mode 100644 index 000000000..f6c069f2b --- /dev/null +++ b/lib/commerce/wishlist/use-add-item.tsx @@ -0,0 +1,5 @@ +import useAction from '../utils/use-action' + +const useAddItem = useAction + +export default useAddItem diff --git a/lib/commerce/wishlist/use-remove-item.tsx b/lib/commerce/wishlist/use-remove-item.tsx new file mode 100644 index 000000000..dfa60c363 --- /dev/null +++ b/lib/commerce/wishlist/use-remove-item.tsx @@ -0,0 +1,5 @@ +import useAction from '../utils/use-action' + +const useRemoveItem = useAction + +export default useRemoveItem diff --git a/lib/commerce/wishlist/use-wishlist.tsx b/lib/commerce/wishlist/use-wishlist.tsx new file mode 100644 index 000000000..7b2981412 --- /dev/null +++ b/lib/commerce/wishlist/use-wishlist.tsx @@ -0,0 +1,17 @@ +import type { responseInterface } from 'swr' +import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types' +import useData, { SwrOptions } from '../utils/use-data' + +export type WishlistResponse = responseInterface & { + isEmpty: boolean +} + +export default function useWishlist( + options: HookFetcherOptions, + input: HookInput, + fetcherFn: HookFetcher, + swrOptions?: SwrOptions +) { + const response = useData(options, input, fetcherFn, swrOptions) + return Object.assign(response, { isEmpty: true }) as WishlistResponse +} diff --git a/pages/api/bigcommerce/wishlist.ts b/pages/api/bigcommerce/wishlist.ts new file mode 100644 index 000000000..cfc3e00d0 --- /dev/null +++ b/pages/api/bigcommerce/wishlist.ts @@ -0,0 +1,3 @@ +import wishlistApi from '@lib/bigcommerce/api/wishlist' + +export default wishlistApi() diff --git a/pages/index.tsx b/pages/index.tsx index eb83a6520..8f02e3c01 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -5,11 +5,10 @@ import getAllProducts from '@lib/bigcommerce/api/operations/get-all-products' import getSiteInfo from '@lib/bigcommerce/api/operations/get-site-info' import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages' import rangeMap from '@lib/range-map' -import { getCategoryPath, getDesignerPath } from '@utils/search' import { Layout } from '@components/core' import { Grid, Marquee, Hero } from '@components/ui' import { ProductCard } from '@components/product' -import Link from 'next/link' +import HomeAllProductsGrid from '@components/core/HomeAllProductsGrid' export async function getStaticProps({ preview, @@ -129,53 +128,11 @@ export default function Home({ /> ))} -
-
-
- - -
-
-
- - {newestProducts.map(({ node }) => ( - - ))} - -
-
+ ) }