4
0
forked from crowetic/commerce

add to cart c:

This commit is contained in:
Luis Alvarez 2020-10-04 19:44:11 -05:00
parent 6c378d98ea
commit 6fa1204e0b
10 changed files with 107 additions and 42 deletions

View File

@ -1,6 +1,8 @@
import cn from 'classnames' import cn from 'classnames'
import { FC } from 'react' import { FC } from 'react'
import s from './Layout.module.css' import s from './Layout.module.css'
import { CommerceProvider } from '@lib/bigcommerce'
import { CartProvider } from '@lib/bigcommerce/cart'
import { Navbar, Featurebar } from '@components/core' import { Navbar, Featurebar } from '@components/core'
import { Container, Sidebar } from '@components/ui' import { Container, Sidebar } from '@components/ui'
import { CartSidebarView } from '@components/cart' import { CartSidebarView } from '@components/cart'
@ -35,9 +37,13 @@ const CoreLayout: FC<Props> = ({ className, children }) => {
} }
const Layout: FC<Props> = (props) => ( const Layout: FC<Props> = (props) => (
<UIProvider> <CommerceProvider>
<CoreLayout {...props} /> <CartProvider>
</UIProvider> <UIProvider>
<CoreLayout {...props} />
</UIProvider>
</CartProvider>
</CommerceProvider>
) )
export default Layout export default Layout

View File

@ -4,6 +4,9 @@ import s from './ProductView.module.css'
import { Button } from '@components/ui' import { Button } from '@components/ui'
import { Swatch } from '@components/product' import { Swatch } from '@components/product'
import { Colors } from '@components/ui/types' 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 { interface ProductData {
name: string name: string
images: any images: any
@ -12,17 +15,31 @@ interface ProductData {
colors?: any[] colors?: any[]
sizes?: any[] sizes?: any[]
} }
interface Props { interface Props {
className?: string className?: string
children?: any children?: any
productData: ProductData productData: ProductData
product: Product
} }
const ProductView: FC<Props> = ({ productData, className }) => { const COLORS: Colors[] = ['pink', 'black', 'white']
const rootClassName = cn(s.root, className)
const colors: Colors[] = ['pink', 'black', 'white'] const ProductView: FC<Props> = ({ 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 ( return (
<div className={rootClassName}> <div className={cn(s.root, className)}>
<div className="absolute"> <div className="absolute">
<h1 className="px-8 py-2 bg-violet text-white font-bold text-3xl"> <h1 className="px-8 py-2 bg-violet text-white font-bold text-3xl">
{productData.name} {productData.name}
@ -38,8 +55,8 @@ const ProductView: FC<Props> = ({ productData, className }) => {
<section className="pb-4"> <section className="pb-4">
<h2 className="uppercase font-medium">Color</h2> <h2 className="uppercase font-medium">Color</h2>
<div className="flex flex-row py-4"> <div className="flex flex-row py-4">
{colors.map((c) => ( {COLORS.map((c) => (
<Swatch color={c} /> <Swatch key={c} color={c} />
))} ))}
</div> </div>
</section> </section>
@ -57,7 +74,9 @@ const ProductView: FC<Props> = ({ productData, className }) => {
<p>{productData.description}</p> <p>{productData.description}</p>
</section> </section>
<section className="pb-4"> <section className="pb-4">
<Button className={s.button}>Add to Cart</Button> <Button type="button" className={s.button} onClick={addToCart}>
Add to Cart
</Button>
</section> </section>
</div> </div>
</div> </div>

View File

@ -5,11 +5,17 @@ import createApiHandler, {
} from './utils/create-api-handler' } from './utils/create-api-handler'
import { BigcommerceApiError } from './utils/errors' 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 METHODS = ['GET', 'POST', 'PUT', 'DELETE']
const cartApi: BigcommerceApiHandler = async (req, res, config) => { const cartApi: BigcommerceApiHandler<Cart> = async (req, res, config) => {
if (!isAllowedMethod(req, res, METHODS)) return if (!isAllowedMethod(req, res, METHODS)) return
const { cookies } = req const { cookies } = req
@ -27,7 +33,7 @@ const cartApi: BigcommerceApiHandler = async (req, res, config) => {
} catch (error) { } catch (error) {
if (error instanceof BigcommerceApiError && error.status === 404) { if (error instanceof BigcommerceApiError && error.status === 404) {
// Remove the cookie if it exists but the cart wasn't found // 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 { } else {
throw error throw error
} }
@ -38,10 +44,11 @@ const cartApi: BigcommerceApiHandler = async (req, res, config) => {
// Create or add a product to the cart // Create or add a product to the cart
if (req.method === 'POST') { if (req.method === 'POST') {
const { product } = req.body const item: Item | undefined = req.body?.item
if (!product) { if (!item) {
return res.status(400).json({ return res.status(400).json({
data: null,
errors: [{ message: 'Missing product' }], errors: [{ message: 'Missing product' }],
}) })
} }
@ -49,28 +56,32 @@ const cartApi: BigcommerceApiHandler = async (req, res, config) => {
const options = { const options = {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
line_items: [parseProduct(product)], line_items: [parseItem(item)],
}), }),
} }
const { data } = cartId const { data } = cartId
? await config.storeApiFetch(`/v3/carts/${cartId}/items`, options) ? await config.storeApiFetch(`/v3/carts/${cartId}/items`, options)
: await config.storeApiFetch('/v3/carts', options) : await config.storeApiFetch('/v3/carts', options)
console.log('API DATA', data)
// Create or update the cart cookie // Create or update the cart cookie
res.setHeader( res.setHeader(
'Set-Cookie', '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) { } catch (error) {
console.error(error)
const message = const message =
error instanceof BigcommerceApiError error instanceof BigcommerceApiError
? 'An unexpected error ocurred with the Bigcommerce API' ? 'An unexpected error ocurred with the Bigcommerce API'
: 'An unexpected error ocurred' : '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) return serialize(name, cartId || '', options)
} }
const parseProduct = (product: any) => ({ const parseItem = (item: Item) => ({
quantity: product.quantity, quantity: item.quantity || 1,
product_id: product.productId, product_id: item.productId,
variant_id: product.variantId, variant_id: item.variantId,
}) })
export default createApiHandler(cartApi) export default createApiHandler(cartApi)

View File

@ -33,11 +33,14 @@ export const getProductQuery = /* GraphQL */ `
${productInfoFragment} ${productInfoFragment}
` `
export interface GetProductResult<T> { export type Product = Extract<
product?: T extends GetProductQuery GetProductQuery['site']['route']['node'],
? Extract<T['site']['route']['node'], { __typename: 'Product' }> { __typename: 'Product' }
: unknown >
}
export type GetProductResult<
T extends { product?: any } = { product?: Product }
> = T
export type ProductVariables = Images & export type ProductVariables = Images &
({ path: string; slug?: never } | { path?: never; slug: string }) ({ path: string; slug?: never } | { path?: never; slug: string })
@ -45,7 +48,7 @@ export type ProductVariables = Images &
async function getProduct(opts: { async function getProduct(opts: {
variables: ProductVariables variables: ProductVariables
config?: BigcommerceConfig config?: BigcommerceConfig
}): Promise<GetProductResult<GetProductQuery>> }): Promise<GetProductResult>
async function getProduct<T, V = any>(opts: { async function getProduct<T, V = any>(opts: {
query: string query: string
@ -61,7 +64,7 @@ async function getProduct({
query?: string query?: string
variables: ProductVariables variables: ProductVariables
config?: BigcommerceConfig config?: BigcommerceConfig
}): Promise<GetProductResult<GetProductQuery>> { }): Promise<GetProductResult> {
config = getConfig(config) config = getConfig(config)
const variables: GetProductQueryVariables = { const variables: GetProductQueryVariables = {
...config.imageVariables, ...config.imageVariables,

View File

@ -1,12 +1,17 @@
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next' import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
import { BigcommerceConfig, getConfig } from '..' import { BigcommerceConfig, getConfig } from '..'
export type BigcommerceApiHandler = ( export type BigcommerceApiHandler<T = any> = (
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse<BigcommerceApiResponse<T>>,
config: BigcommerceConfig config: BigcommerceConfig
) => void | Promise<void> ) => void | Promise<void>
export type BigcommerceApiResponse<T> = {
data: T | null
errors?: { message: string }[]
}
export default function createApiHandler(handler: BigcommerceApiHandler) { export default function createApiHandler(handler: BigcommerceApiHandler) {
return function getApiHandler({ return function getApiHandler({
config, config,

View File

@ -30,7 +30,7 @@ export default async function fetchStoreApi<T>(
const contentType = res.headers.get('Content-Type') const contentType = res.headers.get('Content-Type')
if (contentType?.includes('application/json')) { if (!contentType?.includes('application/json')) {
throw new BigcommerceApiError( throw new BigcommerceApiError(
`Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`, `Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`,
res res

View File

@ -7,7 +7,11 @@ import {
export type Cart = {} export type Cart = {}
export const CartProvider: FC = ({ children }) => { export const CartProvider: FC = ({ children }) => {
return <CommerceCartProvider url="/api/cart">{children}</CommerceCartProvider> return (
<CommerceCartProvider url="/api/bigcommerce/cart">
{children}
</CommerceCartProvider>
)
} }
export function useCart() { export function useCart() {

View File

@ -1,11 +1,23 @@
import type { Fetcher } from '@lib/commerce' import type { Fetcher } from '@lib/commerce'
import { default as useCartAddItem } from '@lib/commerce/cart/use-add-item' import { default as useCartAddItem } from '@lib/commerce/cart/use-add-item'
import type { Item } from '../api/cart'
import { Cart } from '.' import { Cart } from '.'
async function fetcher(fetch: Fetcher<Cart>, { item }: { item: any }) { export type { Item }
return fetch({ url: '/api/cart', method: 'POST', body: { item } })
function fetcher(fetch: Fetcher<Cart>, { 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() { export default function useAddItem() {
return useCartAddItem<Cart, { item: any }>(fetcher) return useCartAddItem<Cart, { item: Item }>(fetcher)
} }

View File

@ -33,6 +33,7 @@ export const bigcommerceConfig: CommerceConfig = {
if (res.ok) { if (res.ok) {
const { data } = await res.json() const { data } = await res.json()
console.log('DATA', data)
return data return data
} }
@ -44,12 +45,11 @@ export type BigcommerceConfig = Partial<CommerceConfig>
export type BigcommerceProps = { export type BigcommerceProps = {
children?: ReactNode children?: ReactNode
config: BigcommerceConfig } & BigcommerceConfig
}
export function CommerceProvider({ children, config }: BigcommerceProps) { export function CommerceProvider({ children, ...config }: BigcommerceProps) {
return ( return (
<CoreCommerceProvider config={{ ...config, ...bigcommerceConfig }}> <CoreCommerceProvider config={{ ...bigcommerceConfig, ...config }}>
{children} {children}
</CoreCommerceProvider> </CoreCommerceProvider>
) )

View File

@ -9,6 +9,11 @@ export async function getStaticProps({
params, params,
}: GetStaticPropsContext<{ slug: string }>) { }: GetStaticPropsContext<{ slug: string }>) {
const { product } = await getProduct({ variables: { slug: params!.slug } }) const { product } = await getProduct({ variables: { slug: params!.slug } })
if (!product) {
throw new Error(`Product with slug '${params!.slug}' not found`)
}
const productData = { const productData = {
name: 'T-Shirt', name: 'T-Shirt',
description: ` description: `
@ -51,7 +56,7 @@ export default function Slug({
return router.isFallback ? ( return router.isFallback ? (
<h1>Loading...</h1> <h1>Loading...</h1>
) : ( ) : (
<ProductView productData={productData} /> <ProductView product={product} productData={productData} />
) )
} }