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 { 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<Props> = ({ className, children }) => {
}
const Layout: FC<Props> = (props) => (
<UIProvider>
<CoreLayout {...props} />
</UIProvider>
<CommerceProvider>
<CartProvider>
<UIProvider>
<CoreLayout {...props} />
</UIProvider>
</CartProvider>
</CommerceProvider>
)
export default Layout

View File

@ -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<Props> = ({ productData, className }) => {
const rootClassName = cn(s.root, className)
const colors: Colors[] = ['pink', 'black', 'white']
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 (
<div className={rootClassName}>
<div className={cn(s.root, className)}>
<div className="absolute">
<h1 className="px-8 py-2 bg-violet text-white font-bold text-3xl">
{productData.name}
@ -38,8 +55,8 @@ const ProductView: FC<Props> = ({ productData, className }) => {
<section className="pb-4">
<h2 className="uppercase font-medium">Color</h2>
<div className="flex flex-row py-4">
{colors.map((c) => (
<Swatch color={c} />
{COLORS.map((c) => (
<Swatch key={c} color={c} />
))}
</div>
</section>
@ -57,7 +74,9 @@ const ProductView: FC<Props> = ({ productData, className }) => {
<p>{productData.description}</p>
</section>
<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>
</div>
</div>

View File

@ -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<Cart> = 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)

View File

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

View File

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

View File

@ -30,7 +30,7 @@ export default async function fetchStoreApi<T>(
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

View File

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

View File

@ -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<Cart>, { item }: { item: any }) {
return fetch({ url: '/api/cart', method: 'POST', body: { item } })
export type { 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() {
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) {
const { data } = await res.json()
console.log('DATA', data)
return data
}
@ -44,12 +45,11 @@ export type BigcommerceConfig = Partial<CommerceConfig>
export type BigcommerceProps = {
children?: ReactNode
config: BigcommerceConfig
}
} & BigcommerceConfig
export function CommerceProvider({ children, config }: BigcommerceProps) {
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
return (
<CoreCommerceProvider config={{ ...config, ...bigcommerceConfig }}>
<CoreCommerceProvider config={{ ...bigcommerceConfig, ...config }}>
{children}
</CoreCommerceProvider>
)

View File

@ -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 ? (
<h1>Loading...</h1>
) : (
<ProductView productData={productData} />
<ProductView product={product} productData={productData} />
)
}