diff --git a/README.md b/README.md index 5df149b06..a586199d2 100644 --- a/README.md +++ b/README.md @@ -1 +1,7 @@ # Next.js Commerce + +## Features + +## Todo + +## Contribute diff --git a/assets/base.css b/assets/base.css index 7d0df28bf..7be74ba3d 100644 --- a/assets/base.css +++ b/assets/base.css @@ -1,13 +1,13 @@ :root { - --primary: white; + --primary: #ffffff; --primary-2: #f1f3f5; - --secondary: black; + --secondary: #000000; --secondary-2: #111; --selection: var(--cyan); - --text-base: black; - --text-primary: black; + --text-base: #000000; + --text-primary: #000000; --text-secondary: white; --hover: rgba(0, 0, 0, 0.075); @@ -38,9 +38,9 @@ } [data-theme='dark'] { - --primary: black; + --primary: #000000; --primary-2: #111; - --secondary: white; + --secondary: #ffffff; --secondary-2: #f1f3f5; --hover: rgba(255, 255, 255, 0.075); --hover-1: rgba(255, 255, 255, 0.15); diff --git a/components/auth/LoginView.tsx b/components/auth/LoginView.tsx index b9cb4cb63..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/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-customer-id.ts b/lib/bigcommerce/api/operations/get-customer-id.ts new file mode 100644 index 000000000..6cabb3ff1 --- /dev/null +++ b/lib/bigcommerce/api/operations/get-customer-id.ts @@ -0,0 +1,34 @@ +import { GetCustomerIdQuery } from '@lib/bigcommerce/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/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/index.ts b/lib/bigcommerce/api/wishlist/index.ts index 3bcba106b..13cbf60f7 100644 --- a/lib/bigcommerce/api/wishlist/index.ts +++ b/lib/bigcommerce/api/wishlist/index.ts @@ -11,15 +11,16 @@ import removeItem from './handlers/remove-item' import updateWishlist from './handlers/update-wishlist' import removeWishlist from './handlers/remove-wishlist' import addWishlist from './handlers/add-wishlist' +import { definitions } from '../definitions/wishlist' type Body = Partial | undefined 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 } @@ -32,21 +33,11 @@ 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 + getWishlist: BigcommerceHandler addWishlist: BigcommerceHandler< Wishlist, { wishlistId: string } & Body @@ -57,7 +48,7 @@ export type WishlistHandlers = { > addItem: BigcommerceHandler< Wishlist, - { wishlistId: string } & Body + { customerToken?: string } & Body > removeItem: BigcommerceHandler< Wishlist, @@ -77,17 +68,21 @@ 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 }) } diff --git a/lib/bigcommerce/cart/use-add-item.tsx b/lib/bigcommerce/cart/use-add-item.tsx index 36fbec7d2..19caca508 100644 --- a/lib/bigcommerce/cart/use-add-item.tsx +++ b/lib/bigcommerce/cart/use-add-item.tsx @@ -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..b12362d5c 100644 --- a/lib/bigcommerce/cart/use-cart.tsx +++ b/lib/bigcommerce/cart/use-cart.tsx @@ -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/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/wishlist/use-add-item.tsx b/lib/bigcommerce/wishlist/use-add-item.tsx index 9bbbe009e..60ea09c7f 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 { CommerceError } from '@lib/commerce/utils/errors' +import useWishlistAddItem from '@lib/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,24 +15,33 @@ 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 }, diff --git a/lib/bigcommerce/wishlist/use-wishlist.tsx b/lib/bigcommerce/wishlist/use-wishlist.tsx index d60660c05..438901de0 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 { SwrOptions } from '@lib/commerce/utils/use-data' +import useCommerceWishlist from '@lib/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-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 9f374253b..64443f536 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -80,8 +80,8 @@ export default function Home({ key={node.path} product={node} // The first image is the largest one in the grid - imgWidth={i === 0 ? 1600 : 820} - imgHeight={i === 0 ? 1600 : 820} + imgWidth={i === 0 ? '65vw' : '30vw'} + imgHeight={i === 0 ? '45vw' : '22vw'} priority /> ))}