From 6fa1204e0b123821b4c7ee2d5266e9d16a699d94 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Sun, 4 Oct 2020 19:44:11 -0500 Subject: [PATCH] add to cart c: --- components/core/Layout/Layout.tsx | 12 ++++-- .../product/ProductView/ProductView.tsx | 33 +++++++++++++---- lib/bigcommerce/api/cart.ts | 37 ++++++++++++------- lib/bigcommerce/api/operations/get-product.ts | 17 +++++---- .../api/utils/create-api-handler.ts | 9 ++++- lib/bigcommerce/api/utils/fetch-store-api.ts | 2 +- lib/bigcommerce/cart/index.tsx | 6 ++- lib/bigcommerce/cart/use-add-item.tsx | 18 +++++++-- lib/bigcommerce/index.tsx | 8 ++-- pages/product/[slug].tsx | 7 +++- 10 files changed, 107 insertions(+), 42 deletions(-) diff --git a/components/core/Layout/Layout.tsx b/components/core/Layout/Layout.tsx index 05cbd504f..4c5861842 100644 --- a/components/core/Layout/Layout.tsx +++ b/components/core/Layout/Layout.tsx @@ -1,6 +1,8 @@ import cn from 'classnames' import { FC } from 'react' import s from './Layout.module.css' +import { CommerceProvider } from '@lib/bigcommerce' +import { CartProvider } from '@lib/bigcommerce/cart' import { Navbar, Featurebar } from '@components/core' import { Container, Sidebar } from '@components/ui' import { CartSidebarView } from '@components/cart' @@ -35,9 +37,13 @@ const CoreLayout: FC = ({ className, children }) => { } const Layout: FC = (props) => ( - - - + + + + + + + ) export default Layout diff --git a/components/product/ProductView/ProductView.tsx b/components/product/ProductView/ProductView.tsx index d8e59fba1..db4c93baf 100644 --- a/components/product/ProductView/ProductView.tsx +++ b/components/product/ProductView/ProductView.tsx @@ -4,6 +4,9 @@ import s from './ProductView.module.css' import { Button } from '@components/ui' import { Swatch } from '@components/product' import { Colors } from '@components/ui/types' +import type { Product } from '@lib/bigcommerce/api/operations/get-product' +import useAddItem from '@lib/bigcommerce/cart/use-add-item' + interface ProductData { name: string images: any @@ -12,17 +15,31 @@ interface ProductData { colors?: any[] sizes?: any[] } + interface Props { className?: string children?: any productData: ProductData + product: Product } -const ProductView: FC = ({ productData, className }) => { - const rootClassName = cn(s.root, className) - const colors: Colors[] = ['pink', 'black', 'white'] +const COLORS: Colors[] = ['pink', 'black', 'white'] + +const ProductView: FC = ({ product, productData, className }) => { + const addItem = useAddItem() + const addToCart = () => { + addItem({ + item: { + productId: product.entityId, + variantId: product.variants.edges?.[0]?.node.entityId!, + }, + }) + } + + console.log('PRODUCT', product) + return ( -
+

{productData.name} @@ -38,8 +55,8 @@ const ProductView: FC = ({ productData, className }) => {

Color

- {colors.map((c) => ( - + {COLORS.map((c) => ( + ))}
@@ -57,7 +74,9 @@ const ProductView: FC = ({ productData, className }) => {

{productData.description}

- +

diff --git a/lib/bigcommerce/api/cart.ts b/lib/bigcommerce/api/cart.ts index 447713051..1f92205de 100644 --- a/lib/bigcommerce/api/cart.ts +++ b/lib/bigcommerce/api/cart.ts @@ -5,11 +5,17 @@ import createApiHandler, { } from './utils/create-api-handler' import { BigcommerceApiError } from './utils/errors' -type Cart = any +export type Item = { + productId: number + variantId: number + quantity?: number +} + +export type Cart = any const METHODS = ['GET', 'POST', 'PUT', 'DELETE'] -const cartApi: BigcommerceApiHandler = async (req, res, config) => { +const cartApi: BigcommerceApiHandler = async (req, res, config) => { if (!isAllowedMethod(req, res, METHODS)) return const { cookies } = req @@ -27,7 +33,7 @@ const cartApi: BigcommerceApiHandler = async (req, res, config) => { } catch (error) { if (error instanceof BigcommerceApiError && error.status === 404) { // Remove the cookie if it exists but the cart wasn't found - res.setHeader('Set-Cookie', getCartCookie(name)) + res.setHeader('Set-Cookie', getCartCookie(config.cartCookie)) } else { throw error } @@ -38,10 +44,11 @@ const cartApi: BigcommerceApiHandler = async (req, res, config) => { // Create or add a product to the cart if (req.method === 'POST') { - const { product } = req.body + const item: Item | undefined = req.body?.item - if (!product) { + if (!item) { return res.status(400).json({ + data: null, errors: [{ message: 'Missing product' }], }) } @@ -49,28 +56,32 @@ const cartApi: BigcommerceApiHandler = async (req, res, config) => { const options = { method: 'POST', body: JSON.stringify({ - line_items: [parseProduct(product)], + line_items: [parseItem(item)], }), } const { data } = cartId ? await config.storeApiFetch(`/v3/carts/${cartId}/items`, options) : await config.storeApiFetch('/v3/carts', options) + console.log('API DATA', data) + // Create or update the cart cookie res.setHeader( 'Set-Cookie', - getCartCookie(name, data.id, config.cartCookieMaxAge) + getCartCookie(config.cartCookie, data.id, config.cartCookieMaxAge) ) - return res.status(200).json({ done: { data } }) + return res.status(200).json({ data }) } } catch (error) { + console.error(error) + const message = error instanceof BigcommerceApiError ? 'An unexpected error ocurred with the Bigcommerce API' : 'An unexpected error ocurred' - res.status(500).json({ errors: [{ message }] }) + res.status(500).json({ data: null, errors: [{ message }] }) } } @@ -90,10 +101,10 @@ function getCartCookie(name: string, cartId?: string, maxAge?: number) { return serialize(name, cartId || '', options) } -const parseProduct = (product: any) => ({ - quantity: product.quantity, - product_id: product.productId, - variant_id: product.variantId, +const parseItem = (item: Item) => ({ + quantity: item.quantity || 1, + product_id: item.productId, + variant_id: item.variantId, }) export default createApiHandler(cartApi) diff --git a/lib/bigcommerce/api/operations/get-product.ts b/lib/bigcommerce/api/operations/get-product.ts index d2e90b57f..89acc7aa7 100644 --- a/lib/bigcommerce/api/operations/get-product.ts +++ b/lib/bigcommerce/api/operations/get-product.ts @@ -33,11 +33,14 @@ export const getProductQuery = /* GraphQL */ ` ${productInfoFragment} ` -export interface GetProductResult { - product?: T extends GetProductQuery - ? Extract - : unknown -} +export type Product = Extract< + GetProductQuery['site']['route']['node'], + { __typename: 'Product' } +> + +export type GetProductResult< + T extends { product?: any } = { product?: Product } +> = T export type ProductVariables = Images & ({ path: string; slug?: never } | { path?: never; slug: string }) @@ -45,7 +48,7 @@ export type ProductVariables = Images & async function getProduct(opts: { variables: ProductVariables config?: BigcommerceConfig -}): Promise> +}): Promise async function getProduct(opts: { query: string @@ -61,7 +64,7 @@ async function getProduct({ query?: string variables: ProductVariables config?: BigcommerceConfig -}): Promise> { +}): Promise { config = getConfig(config) const variables: GetProductQueryVariables = { ...config.imageVariables, diff --git a/lib/bigcommerce/api/utils/create-api-handler.ts b/lib/bigcommerce/api/utils/create-api-handler.ts index 144f13068..68bf92d38 100644 --- a/lib/bigcommerce/api/utils/create-api-handler.ts +++ b/lib/bigcommerce/api/utils/create-api-handler.ts @@ -1,12 +1,17 @@ import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next' import { BigcommerceConfig, getConfig } from '..' -export type BigcommerceApiHandler = ( +export type BigcommerceApiHandler = ( req: NextApiRequest, - res: NextApiResponse, + res: NextApiResponse>, config: BigcommerceConfig ) => void | Promise +export type BigcommerceApiResponse = { + data: T | null + errors?: { message: string }[] +} + export default function createApiHandler(handler: BigcommerceApiHandler) { return function getApiHandler({ config, diff --git a/lib/bigcommerce/api/utils/fetch-store-api.ts b/lib/bigcommerce/api/utils/fetch-store-api.ts index a3dae1e34..687c53c0e 100644 --- a/lib/bigcommerce/api/utils/fetch-store-api.ts +++ b/lib/bigcommerce/api/utils/fetch-store-api.ts @@ -30,7 +30,7 @@ export default async function fetchStoreApi( const contentType = res.headers.get('Content-Type') - if (contentType?.includes('application/json')) { + if (!contentType?.includes('application/json')) { throw new BigcommerceApiError( `Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`, res diff --git a/lib/bigcommerce/cart/index.tsx b/lib/bigcommerce/cart/index.tsx index 691d38f3b..b778c86e1 100644 --- a/lib/bigcommerce/cart/index.tsx +++ b/lib/bigcommerce/cart/index.tsx @@ -7,7 +7,11 @@ import { export type Cart = {} export const CartProvider: FC = ({ children }) => { - return {children} + return ( + + {children} + + ) } export function useCart() { diff --git a/lib/bigcommerce/cart/use-add-item.tsx b/lib/bigcommerce/cart/use-add-item.tsx index 182702a0b..becf2ad05 100644 --- a/lib/bigcommerce/cart/use-add-item.tsx +++ b/lib/bigcommerce/cart/use-add-item.tsx @@ -1,11 +1,23 @@ import type { Fetcher } from '@lib/commerce' import { default as useCartAddItem } from '@lib/commerce/cart/use-add-item' +import type { Item } from '../api/cart' import { Cart } from '.' -async function fetcher(fetch: Fetcher, { item }: { item: any }) { - return fetch({ url: '/api/cart', method: 'POST', body: { item } }) +export type { Item } + +function fetcher(fetch: Fetcher, { item }: { item: Item }) { + if ( + item.quantity && + (!Number.isInteger(item.quantity) || item.quantity! < 1) + ) { + throw new Error( + 'The item quantity has to be a valid integer greater than 0' + ) + } + + return fetch({ url: '/api/bigcommerce/cart', method: 'POST', body: { item } }) } export default function useAddItem() { - return useCartAddItem(fetcher) + return useCartAddItem(fetcher) } diff --git a/lib/bigcommerce/index.tsx b/lib/bigcommerce/index.tsx index 5b4162d01..7086812ec 100644 --- a/lib/bigcommerce/index.tsx +++ b/lib/bigcommerce/index.tsx @@ -33,6 +33,7 @@ export const bigcommerceConfig: CommerceConfig = { if (res.ok) { const { data } = await res.json() + console.log('DATA', data) return data } @@ -44,12 +45,11 @@ export type BigcommerceConfig = Partial export type BigcommerceProps = { children?: ReactNode - config: BigcommerceConfig -} +} & BigcommerceConfig -export function CommerceProvider({ children, config }: BigcommerceProps) { +export function CommerceProvider({ children, ...config }: BigcommerceProps) { return ( - + {children} ) diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index 4e6af3308..2b408b49f 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -9,6 +9,11 @@ export async function getStaticProps({ params, }: GetStaticPropsContext<{ slug: string }>) { const { product } = await getProduct({ variables: { slug: params!.slug } }) + + if (!product) { + throw new Error(`Product with slug '${params!.slug}' not found`) + } + const productData = { name: 'T-Shirt', description: ` @@ -51,7 +56,7 @@ export default function Slug({ return router.isFallback ? (

Loading...

) : ( - + ) }