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 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
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user