forked from crowetic/commerce
add to cart c:
This commit is contained in:
parent
6c378d98ea
commit
6fa1204e0b
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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} />
|
||||
)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user