Merge branch 'master' of github.com:okbel/e-comm-example

This commit is contained in:
Belen Curcio 2020-10-26 21:07:37 -03:00
commit c4161a292f
52 changed files with 538 additions and 272 deletions

View File

@ -70,8 +70,8 @@ const LoginView: FC<Props> = () => {
</a>
</div>
)}
<Input placeholder="Email" onChange={setEmail} type="email" />
<Input placeholder="Password" onChange={setPassword} type="password" />
<Input type="email" placeholder="Email" onChange={setEmail} />
<Input type="password" placeholder="Password" onChange={setPassword} />
<Button
variant="slim"

View File

@ -69,8 +69,8 @@ const SignUpView: FC<Props> = () => {
)}
<Input placeholder="First Name" onChange={setFirstName} />
<Input placeholder="Last Name" onChange={setLastName} />
<Input placeholder="Email" onChange={setEmail} type="email" />
<Input placeholder="Password" onChange={setPassword} type="password" />
<Input type="email" placeholder="Email" onChange={setEmail} />
<Input type="password" placeholder="Password" onChange={setPassword} />
<span className="text-accents-8">
<span className="inline-block align-middle ">
<Info width="15" height="15" />

View File

@ -0,0 +1,23 @@
.root {
@apply py-12 flex flex-col w-full px-6;
@screen md {
@apply flex-row;
}
}
.asideWrapper {
@apply pr-3 w-full relative;
@screen md {
@apply w-48;
}
}
.aside {
@apply flex flex-row w-full justify-around mb-12;
@screen md {
@apply mb-0 block sticky top-32;
}
}

View File

@ -0,0 +1,66 @@
import { FC } from 'react'
import Link from 'next/link'
import { getCategoryPath, getDesignerPath } from '@utils/search'
import { Grid } from '@components/ui'
import { ProductCard } from '@components/product'
import s from './HomeAllProductsGrid.module.css'
interface Props {
categories?: any
brands?: any
newestProducts?: any
}
const Head: FC<Props> = ({ categories, brands, newestProducts }) => {
return (
<div className={s.root}>
<div className={s.asideWrapper}>
<div className={s.aside}>
<ul className="mb-10">
<li className="py-1 text-base font-bold tracking-wide">
<Link href={getCategoryPath('')}>
<a>All Categories</a>
</Link>
</li>
{categories.map((cat: any) => (
<li key={cat.path} className="py-1 text-accents-8">
<Link href={getCategoryPath(cat.path)}>
<a>{cat.name}</a>
</Link>
</li>
))}
</ul>
<ul className="">
<li className="py-1 text-base font-bold tracking-wide">
<Link href={getDesignerPath('')}>
<a>All Designers</a>
</Link>
</li>
{brands.flatMap(({ node }: any) => (
<li key={node.path} className="py-1 text-accents-8">
<Link href={getDesignerPath(node.path)}>
<a>{node.name}</a>
</Link>
</li>
))}
</ul>
</div>
</div>
<div className="flex-1">
<Grid layout="normal">
{newestProducts.map(({ node }: any) => (
<ProductCard
key={node.path}
product={node}
variant="simple"
imgWidth={480}
imgHeight={480}
/>
))}
</Grid>
</div>
</div>
)
}
export default Head

View File

@ -0,0 +1 @@
export { default } from './HomeAllProductsGrid'

View File

@ -119,7 +119,7 @@
}
.wishlistButton {
@apply w-10 h-10 flex ml-auto flex items-center justify-center bg-primary text-primary font-semibold text-xs leading-6 cursor-pointer;
@apply w-10 h-10 flex ml-auto flex items-center justify-center bg-primary text-primary font-semibold text-xs leading-6 cursor-pointer z-10;
}
.imageContainer {

View File

@ -1,15 +1,14 @@
import React, { FC, ReactNode, Component } from 'react'
import type { FC } from 'react'
import cn from 'classnames'
import Link from 'next/link'
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-all-products'
import usePrice from '@lib/bigcommerce/use-price'
import { Heart } from '@components/icons'
import { EnhancedImage } from '@components/core'
import s from './ProductCard.module.css'
import WishlistButton from '@components/wishlist/WishlistButton'
interface Props {
className?: string
children?: ReactNode[] | Component[] | any[]
product: ProductNode
variant?: 'slim' | 'simple'
imgWidth: number | string
@ -25,7 +24,7 @@ const ProductCard: FC<Props> = ({
imgHeight,
priority,
}) => {
const src = p.images.edges?.[0]?.node.urlOriginal!
const src = p.images.edges?.[0]?.node?.urlOriginal!
const { price } = usePrice({
amount: p.prices?.price?.value,
baseAmount: p.prices?.retailPrice?.value,
@ -53,7 +52,7 @@ const ProductCard: FC<Props> = ({
}
return (
<Link href={`product${p.path}`}>
<Link href={`/product${p.path}`}>
<a
className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}
>
@ -65,9 +64,11 @@ const ProductCard: FC<Props> = ({
</h3>
<span className={s.productPrice}>{price}</span>
</div>
<div className={s.wishlistButton}>
<Heart />
</div>
<WishlistButton
className={s.wishlistButton}
productId={p.entityId}
variant={p.variants.edges?.[0]!}
/>
</div>
<div className={cn(s.imageContainer)}>
<EnhancedImage

View File

@ -48,7 +48,8 @@
@apply hidden;
@screen sm {
@apply block absolute bottom-6 left-1/2 -translate-x-1/2 transform;
@apply block absolute bottom-6 left-1/2;
transform: translateX(-50%);
}
}

View File

@ -0,0 +1,74 @@
import React, { FC, useState } from 'react'
import cn from 'classnames'
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-all-products'
import useAddItem from '@lib/bigcommerce/wishlist/use-add-item'
import useRemoveItem from '@lib/bigcommerce/wishlist/use-remove-item'
import useWishlist from '@lib/bigcommerce/wishlist/use-wishlist'
import useCustomer from '@lib/bigcommerce/use-customer'
import { Heart } from '@components/icons'
import { useUI } from '@components/ui/context'
type Props = {
productId: number
variant: NonNullable<ProductNode['variants']['edges']>[0]
} & React.ButtonHTMLAttributes<HTMLButtonElement>
const WishlistButton: FC<Props> = ({
productId,
variant,
className,
...props
}) => {
const addItem = useAddItem()
const removeItem = useRemoveItem()
const { data } = useWishlist()
const { data: customer } = useCustomer()
const [loading, setLoading] = useState(false)
const { openModal, setModalView } = useUI()
const itemInWishlist = data?.items?.find(
(item) =>
item.product_id === productId &&
item.variant_id === variant?.node.entityId
)
const handleWishlistChange = async (e: any) => {
e.preventDefault()
if (loading) return
// A login is required before adding an item to the wishlist
if (!customer) {
setModalView('LOGIN_VIEW')
return openModal()
}
setLoading(true)
try {
if (itemInWishlist) {
await removeItem({ id: itemInWishlist.id! })
} else {
await addItem({
productId,
variantId: variant?.node.entityId!,
})
}
setLoading(false)
} catch (err) {
setLoading(false)
}
}
return (
<button
{...props}
className={cn({ 'opacity-50': loading }, className)}
onClick={handleWishlistChange}
>
<Heart fill={itemInWishlist ? 'var(--pink)' : 'none'} />
</button>
)
}
export default WishlistButton

View File

@ -0,0 +1 @@
export { default } from './WishlistButton'

View File

@ -1,4 +1,4 @@
import parseItem from '../../utils/parse-item'
import { parseCartItem } from '../../utils/parse-item'
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartHandlers } from '..'
@ -19,7 +19,7 @@ const addItem: CartHandlers['addItem'] = async ({
const options = {
method: 'POST',
body: JSON.stringify({
line_items: [parseItem(item)],
line_items: [parseCartItem(item)],
}),
}
const { data } = cartId

View File

@ -1,4 +1,4 @@
import parseItem from '../../utils/parse-item'
import { parseCartItem } from '../../utils/parse-item'
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartHandlers } from '..'
@ -20,7 +20,7 @@ const updateItem: CartHandlers['updateItem'] = async ({
{
method: 'PUT',
body: JSON.stringify({
line_item: parseItem(item),
line_item: parseCartItem(item),
}),
}
)

View File

@ -9,8 +9,6 @@ import addItem from './handlers/add-item'
import updateItem from './handlers/update-item'
import removeItem from './handlers/remove-item'
type Body<T> = Partial<T> | undefined
export type ItemBody = {
productId: number
variantId: number
@ -46,14 +44,14 @@ export type Cart = {
export type CartHandlers = {
getCart: BigcommerceHandler<Cart, { cartId?: string }>
addItem: BigcommerceHandler<Cart, { cartId?: string } & Body<AddItemBody>>
addItem: BigcommerceHandler<Cart, { cartId?: string } & Partial<AddItemBody>>
updateItem: BigcommerceHandler<
Cart,
{ cartId?: string } & Body<UpdateItemBody>
{ cartId?: string } & Partial<UpdateItemBody>
>
removeItem: BigcommerceHandler<
Cart,
{ cartId?: string } & Body<RemoveItemBody>
{ cartId?: string } & Partial<RemoveItemBody>
>
}

View File

@ -1,4 +1,4 @@
import type { GetLoggedInCustomerQuery } from '@lib/bigcommerce/schema'
import type { GetLoggedInCustomerQuery } from '../../../schema'
import type { CustomersHandlers } from '..'
export const getLoggedInCustomerQuery = /* GraphQL */ `

View File

@ -1,4 +1,4 @@
import { FetcherError } from '@lib/commerce/utils/errors'
import { FetcherError } from '../../../../commerce/utils/errors'
import login from '../../operations/login'
import type { LoginHandlers } from '../login'

View File

@ -1,5 +1,5 @@
import type { RequestInit } from '@vercel/fetch'
import type { CommerceAPIConfig } from 'lib/commerce/api'
import type { CommerceAPIConfig } from '../../commerce/api'
import fetchGraphqlApi from './utils/fetch-graphql-api'
import fetchStoreApi from './utils/fetch-store-api'

View File

@ -13,7 +13,7 @@ async function getAllPages(opts?: {
preview?: boolean
}): Promise<GetAllPagesResult>
async function getAllPages<T extends { pages: any[] }, V = any>(opts: {
async function getAllPages<T extends { pages: any[] }>(opts: {
url: string
config?: BigcommerceConfig
preview?: boolean

View File

@ -1,7 +1,7 @@
import type {
GetAllProductPathsQuery,
GetAllProductPathsQueryVariables,
} from 'lib/bigcommerce/schema'
} from '../../schema'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import filterEdges from '../utils/filter-edges'
import { BigcommerceConfig, getConfig } from '..'

View File

@ -1,7 +1,7 @@
import type {
GetAllProductsQuery,
GetAllProductsQueryVariables,
} from '@lib/bigcommerce/schema'
} from '../../schema'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import filterEdges from '../utils/filter-edges'
import setProductLocaleMeta from '../utils/set-product-locale-meta'

View File

@ -0,0 +1,34 @@
import { GetCustomerIdQuery } from '../../schema'
import { BigcommerceConfig, getConfig } from '..'
export const getCustomerIdQuery = /* GraphQL */ `
query getCustomerId {
customer {
entityId
}
}
`
async function getCustomerId({
customerToken,
config,
}: {
customerToken: string
config?: BigcommerceConfig
}): Promise<number | undefined> {
config = getConfig(config)
const { data } = await config.fetch<GetCustomerIdQuery>(
getCustomerIdQuery,
undefined,
{
headers: {
cookie: `${config.customerCookie}=${customerToken}`,
},
}
)
return data?.customer?.entityId
}
export default getCustomerId

View File

@ -0,0 +1,51 @@
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import { BigcommerceConfig, getConfig } from '..'
import { definitions } from '../definitions/wishlist'
export type Wishlist = definitions['wishlist_Full']
export type GetCustomerWishlistResult<
T extends { wishlist?: any } = { wishlist?: Wishlist }
> = T
export type GetCustomerWishlistVariables = {
customerId: number
}
async function getCustomerWishlist(opts: {
variables: GetCustomerWishlistVariables
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetCustomerWishlistResult>
async function getCustomerWishlist<
T extends { wishlist?: any },
V = any
>(opts: {
url: string
variables: V
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetCustomerWishlistResult<T>>
async function getCustomerWishlist({
config,
variables,
}: {
url?: string
variables: GetCustomerWishlistVariables
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetCustomerWishlistResult> {
config = getConfig(config)
const { data } = await config.storeApiFetch<
RecursivePartial<{ data: Wishlist[] }>
>(`/v3/wishlists?customer_id=${variables.customerId}`)
const wishlists = (data as RecursiveRequired<typeof data>) ?? []
const wishlist = wishlists[0]
return { wishlist }
}
export default getCustomerWishlist

View File

@ -1,7 +1,4 @@
import type {
GetProductQuery,
GetProductQueryVariables,
} from 'lib/bigcommerce/schema'
import type { GetProductQuery, GetProductQueryVariables } from '../../schema'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import setProductLocaleMeta from '../utils/set-product-locale-meta'
import { productInfoFragment } from '../fragments/product'

View File

@ -1,7 +1,4 @@
import type {
GetSiteInfoQuery,
GetSiteInfoQueryVariables,
} from 'lib/bigcommerce/schema'
import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../../schema'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import filterEdges from '../utils/filter-edges'
import { BigcommerceConfig, getConfig } from '..'

View File

@ -1,8 +1,5 @@
import type { ServerResponse } from 'http'
import type {
LoginMutation,
LoginMutationVariables,
} from 'lib/bigcommerce/schema'
import type { LoginMutation, LoginMutationVariables } from '../../schema'
import type { RecursivePartial } from '../utils/types'
import concatHeader from '../utils/concat-cookie'
import { BigcommerceConfig, getConfig } from '..'

View File

@ -1,7 +1,6 @@
import { FetcherError } from '@lib/commerce/utils/errors'
import type { GraphQLFetcher } from '@lib/commerce/api'
import { FetcherError } from '../../../commerce/utils/errors'
import type { GraphQLFetcher } from '../../../commerce/api'
import { getConfig } from '..'
import log from '@lib/logger'
import fetch from './fetch'
const fetchGraphqlApi: GraphQLFetcher = async (

View File

@ -41,7 +41,7 @@ export default async function fetchStoreApi<T>(
throw new BigcommerceApiError(msg, res, data)
}
if (!isJSON) {
if (res.status !== 204 && !isJSON) {
throw new BigcommerceApiError(
`Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`,
res

View File

@ -1,9 +1,13 @@
import type { ItemBody as WishlistItemBody } from '../wishlist'
import type { ItemBody } from '../cart'
const parseItem = (item: ItemBody) => ({
quantity: item.quantity,
export const parseWishlistItem = (item: WishlistItemBody) => ({
product_id: item.productId,
variant_id: item.variantId,
})
export default parseItem
export const parseCartItem = (item: ItemBody) => ({
quantity: item.quantity,
product_id: item.productId,
variant_id: item.variantId,
})

View File

@ -1,9 +1,12 @@
import type { WishlistHandlers } from '..'
import getCustomerId from '../../operations/get-customer-id'
import getCustomerWishlist from '../../operations/get-customer-wishlist'
import { parseWishlistItem } from '../../utils/parse-item'
// Return current wishlist info
// Returns the wishlist of the signed customer
const addItem: WishlistHandlers['addItem'] = async ({
res,
body: { wishlistId, item },
body: { customerToken, item },
config,
}) => {
if (!item) {
@ -13,16 +16,39 @@ const addItem: WishlistHandlers['addItem'] = async ({
})
}
const customerId =
customerToken && (await getCustomerId({ customerToken, config }))
if (!customerId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const { wishlist } = await getCustomerWishlist({
variables: { customerId },
config,
})
const options = {
method: 'POST',
body: JSON.stringify({
items: [item],
}),
body: JSON.stringify(
wishlist
? {
items: [parseWishlistItem(item)],
}
: {
name: 'Wishlist',
customer_id: customerId,
items: [parseWishlistItem(item)],
is_public: false,
}
),
}
const { data } = await config.storeApiFetch(
`/v3/wishlists/${wishlistId}/items`,
options
)
const { data } = wishlist
? await config.storeApiFetch(`/v3/wishlists/${wishlist.id}/items`, options)
: await config.storeApiFetch('/v3/wishlists', options)
res.status(200).json({ data })
}

View File

@ -1,17 +1,32 @@
import getCustomerId from '../../operations/get-customer-id'
import type { Wishlist, WishlistHandlers } from '..'
import getCustomerWishlist from '../../operations/get-customer-wishlist'
// Return wishlist info
const getWishlist: WishlistHandlers['getWishlist'] = async ({
res,
body: { wishlistId },
body: { customerToken },
config,
}) => {
let result: { data?: Wishlist } = {}
try {
result = await config.storeApiFetch(`/v3/wishlists/${wishlistId}`)
} catch (error) {
throw error
if (customerToken) {
const customerId =
customerToken && (await getCustomerId({ customerToken, config }))
if (!customerId) {
// If the customerToken is invalid, then this request is too
return res.status(404).json({
data: null,
errors: [{ message: 'Wishlist not found' }],
})
}
const { wishlist } = await getCustomerWishlist({
variables: { customerId },
config,
})
result = { data: wishlist }
}
res.status(200).json({ data: result.data ?? null })

View File

@ -1,20 +1,34 @@
import getCustomerId from '../../operations/get-customer-id'
import getCustomerWishlist, {
Wishlist,
} from '../../operations/get-customer-wishlist'
import type { WishlistHandlers } from '..'
// Return current wishlist info
const removeItem: WishlistHandlers['removeItem'] = async ({
res,
body: { wishlistId, itemId },
body: { customerToken, itemId },
config,
}) => {
if (!wishlistId || !itemId) {
const customerId =
customerToken && (await getCustomerId({ customerToken, config }))
const { wishlist } =
(customerId &&
(await getCustomerWishlist({
variables: { customerId },
config,
}))) ||
{}
if (!wishlist || !itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const result = await config.storeApiFetch<{ data: any } | null>(
`/v3/wishlists/${wishlistId}/items/${itemId}`,
const result = await config.storeApiFetch<{ data: Wishlist } | null>(
`/v3/wishlists/${wishlist.id}/items/${itemId}`,
{ method: 'DELETE' }
)
const data = result?.data ?? null

View File

@ -5,23 +5,18 @@ import createApiHandler, {
} from '../utils/create-api-handler'
import { BigcommerceApiError } from '../utils/errors'
import getWishlist from './handlers/get-wishlist'
import getAllWishlists from './handlers/get-all-wishlists'
import addItem from './handlers/add-item'
import removeItem from './handlers/remove-item'
import updateWishlist from './handlers/update-wishlist'
import removeWishlist from './handlers/remove-wishlist'
import addWishlist from './handlers/add-wishlist'
type Body<T> = Partial<T> | undefined
import { definitions } from '../definitions/wishlist'
export type ItemBody = {
product_id: number
variant_id: number
productId: number
variantId: number
}
export type AddItemBody = { wishlistId: string; item: ItemBody }
export type AddItemBody = { item: ItemBody }
export type RemoveItemBody = { wishlistId: string; itemId: string }
export type RemoveItemBody = { itemId: string }
export type WishlistBody = {
customer_id: number
@ -32,41 +27,21 @@ export type WishlistBody = {
export type AddWishlistBody = { wishlist: WishlistBody }
// TODO: this type should match:
// https://developer.bigcommerce.com/api-reference/store-management/wishlists/wishlists/wishlistsbyidget
export type Wishlist = {
id: string
customer_id: number
name: string
is_public: boolean
token: string
items: any[]
// TODO: add missing fields
}
export type Wishlist = definitions['wishlist_Full']
export type WishlistHandlers = {
getAllWishlists: BigcommerceHandler<Wishlist[], { customerId?: string }>
getWishlist: BigcommerceHandler<Wishlist, { wishlistId?: string }>
addWishlist: BigcommerceHandler<
Wishlist,
{ wishlistId: string } & Body<AddWishlistBody>
>
updateWishlist: BigcommerceHandler<
Wishlist,
{ wishlistId: string } & Body<AddWishlistBody>
>
getWishlist: BigcommerceHandler<Wishlist, { customerToken?: string }>
addItem: BigcommerceHandler<
Wishlist,
{ wishlistId: string } & Body<AddItemBody>
{ customerToken?: string } & Partial<AddItemBody>
>
removeItem: BigcommerceHandler<
Wishlist,
{ wishlistId: string } & Body<RemoveItemBody>
{ customerToken?: string } & Partial<RemoveItemBody>
>
removeWishlist: BigcommerceHandler<Wishlist, { wishlistId: string }>
}
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
const METHODS = ['GET', 'POST', 'DELETE']
// TODO: a complete implementation should have schema validation for `req.body`
const wishlistApi: BigcommerceApiHandler<Wishlist, WishlistHandlers> = async (
@ -77,57 +52,27 @@ const wishlistApi: BigcommerceApiHandler<Wishlist, WishlistHandlers> = async (
) => {
if (!isAllowedMethod(req, res, METHODS)) return
const { cookies } = req
const customerToken = cookies[config.customerCookie]
try {
const { wishlistId, itemId, customerId } = req.body
// Return current wishlist info
if (req.method === 'GET' && wishlistId) {
const body = { wishlistId: wishlistId as string }
if (req.method === 'GET') {
const body = { customerToken }
return await handlers['getWishlist']({ req, res, config, body })
}
// Add an item to the wishlist
if (req.method === 'POST' && wishlistId) {
const body = { ...req.body, wishlistId }
if (req.method === 'POST') {
const body = { ...req.body, customerToken }
return await handlers['addItem']({ req, res, config, body })
}
// Update a wishlist
if (req.method === 'PUT' && wishlistId) {
const body = { ...req.body, wishlistId }
return await handlers['updateWishlist']({ req, res, config, body })
}
// Remove an item from the wishlist
if (req.method === 'DELETE' && wishlistId && itemId) {
const body = {
wishlistId: wishlistId as string,
itemId: itemId as string,
}
if (req.method === 'DELETE') {
const body = { ...req.body, customerToken }
return await handlers['removeItem']({ req, res, config, body })
}
// Remove the wishlist
if (req.method === 'DELETE' && wishlistId && !itemId) {
const body = { wishlistId: wishlistId as string }
return await handlers['removeWishlist']({ req, res, config, body })
}
// Get all the wishlists
if (req.method === 'GET' && !wishlistId) {
const body = { customerId: customerId as string }
return await handlers['getAllWishlists']({
req,
res: res as any,
config,
body,
})
}
// Create a wishlist
if (req.method === 'POST' && !wishlistId) {
const { body } = req
return await handlers['addWishlist']({ req, res, config, body })
}
} catch (error) {
console.error(error)
@ -143,11 +88,7 @@ const wishlistApi: BigcommerceApiHandler<Wishlist, WishlistHandlers> = async (
export const handlers = {
getWishlist,
addItem,
updateWishlist,
removeItem,
removeWishlist,
getAllWishlists,
addWishlist,
}
export default createApiHandler(wishlistApi, handlers, {})

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react'
import type { HookFetcher } from '@lib/commerce/utils/types'
import { CommerceError } from '@lib/commerce/utils/errors'
import useCartAddItem from '@lib/commerce/cart/use-add-item'
import type { HookFetcher } from '../../commerce/utils/types'
import { CommerceError } from '../../commerce/utils/errors'
import useCartAddItem from '../../commerce/cart/use-add-item'
import type { ItemBody, AddItemBody } from '../api/cart'
import useCart, { Cart } from './use-cart'
@ -36,7 +36,7 @@ export const fetcher: HookFetcher<Cart, AddItemBody> = (
export function extendHook(customFetcher: typeof fetcher) {
const useAddItem = () => {
const { mutate } = useCart()
const fn = useCartAddItem<Cart, AddItemBody>(defaultOpts, customFetcher)
const fn = useCartAddItem(defaultOpts, customFetcher)
return useCallback(
async function addItem(input: AddItemInput) {

View File

@ -1,6 +1,6 @@
import type { HookFetcher } from '@lib/commerce/utils/types'
import type { SwrOptions } from '@lib/commerce/utils/use-data'
import useCommerceCart, { CartInput } from '@lib/commerce/cart/use-cart'
import type { HookFetcher } from '../../commerce/utils/types'
import type { SwrOptions } from '../../commerce/utils/use-data'
import useCommerceCart, { CartInput } from '../../commerce/cart/use-cart'
import type { Cart } from '../api/cart'
const defaultOpts = {
@ -23,23 +23,23 @@ export function extendHook(
swrOptions?: SwrOptions<Cart | null, CartInput>
) {
const useCart = () => {
const cart = useCommerceCart(defaultOpts, [], customFetcher, {
const response = useCommerceCart(defaultOpts, [], customFetcher, {
revalidateOnFocus: false,
...swrOptions,
})
// Uses a getter to only calculate the prop when required
// cart.data is also a getter and it's better to not trigger it early
Object.defineProperty(cart, 'isEmpty', {
// response.data is also a getter and it's better to not trigger it early
Object.defineProperty(response, 'isEmpty', {
get() {
return Object.values(cart.data?.line_items ?? {}).every(
return Object.values(response.data?.line_items ?? {}).every(
(items) => !items.length
)
},
set: (x) => x,
})
return cart
return response
}
useCart.extend = extendHook

View File

@ -1,6 +1,6 @@
import { useCallback } from 'react'
import { HookFetcher } from '@lib/commerce/utils/types'
import useCartRemoveItem from '@lib/commerce/cart/use-remove-item'
import { HookFetcher } from '../../commerce/utils/types'
import useCartRemoveItem from '../../commerce/cart/use-remove-item'
import type { RemoveItemBody } from '../api/cart'
import useCart, { Cart } from './use-cart'
@ -26,7 +26,7 @@ export const fetcher: HookFetcher<Cart | null, RemoveItemBody> = (
}
export function extendHook(customFetcher: typeof fetcher) {
const useRemoveItem = (item?: any) => {
const useRemoveItem = () => {
const { mutate } = useCart()
const fn = useCartRemoveItem<Cart | null, RemoveItemBody>(
defaultOpts,
@ -35,7 +35,7 @@ export function extendHook(customFetcher: typeof fetcher) {
return useCallback(
async function removeItem(input: RemoveItemInput) {
const data = await fn({ itemId: input.id ?? item?.id })
const data = await fn({ itemId: input.id })
await mutate(data, false)
return data
},

View File

@ -1,8 +1,8 @@
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
import type { HookFetcher } from '@lib/commerce/utils/types'
import { CommerceError } from '@lib/commerce/utils/errors'
import useCartUpdateItem from '@lib/commerce/cart/use-update-item'
import type { HookFetcher } from '../../commerce/utils/types'
import { CommerceError } from '../../commerce/utils/errors'
import useCartUpdateItem from '../../commerce/cart/use-update-item'
import type { ItemBody, UpdateItemBody } from '../api/cart'
import { fetcher as removeFetcher } from './use-remove-item'
import useCart, { Cart } from './use-cart'

View File

@ -3,8 +3,8 @@ import {
CommerceConfig,
CommerceProvider as CoreCommerceProvider,
useCommerce as useCoreCommerce,
} from 'lib/commerce'
import { FetcherError } from '@lib/commerce/utils/errors'
} from '../commerce'
import { FetcherError } from '../commerce/utils/errors'
async function getText(res: Response) {
try {

View File

@ -1,6 +1,6 @@
import type { HookFetcher } from '@lib/commerce/utils/types'
import type { SwrOptions } from '@lib/commerce/utils/use-data'
import useCommerceSearch from '@lib/commerce/products/use-search'
import type { HookFetcher } from '../../commerce/utils/types'
import type { SwrOptions } from '../../commerce/utils/use-data'
import useCommerceSearch from '../../commerce/products/use-search'
import type { SearchProductsData } from '../api/catalog/products'
const defaultOpts = {

View File

@ -1886,6 +1886,12 @@ export type GetAllProductsQuery = { __typename?: 'Query' } & {
}
}
export type GetCustomerIdQueryVariables = Exact<{ [key: string]: never }>
export type GetCustomerIdQuery = { __typename?: 'Query' } & {
customer?: Maybe<{ __typename?: 'Customer' } & Pick<Customer, 'entityId'>>
}
export type GetProductQueryVariables = Exact<{
hasLocale?: Maybe<Scalars['Boolean']>
locale?: Maybe<Scalars['String']>

View File

@ -1,6 +1,6 @@
import type { HookFetcher } from '@lib/commerce/utils/types'
import type { SwrOptions } from '@lib/commerce/utils/use-data'
import useCommerceCustomer from '@lib/commerce/use-customer'
import type { HookFetcher } from '../commerce/utils/types'
import type { SwrOptions } from '../commerce/utils/use-data'
import useCommerceCustomer from '../commerce/use-customer'
import type { Customer, CustomerData } from './api/customers'
const defaultOpts = {

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react'
import type { HookFetcher } from '@lib/commerce/utils/types'
import { CommerceError } from '@lib/commerce/utils/errors'
import useCommerceLogin from '@lib/commerce/use-login'
import type { HookFetcher } from '../commerce/utils/types'
import { CommerceError } from '../commerce/utils/errors'
import useCommerceLogin from '../commerce/use-login'
import type { LoginBody } from './api/customers/login'
import useCustomer from './use-customer'

View File

@ -1,6 +1,6 @@
import { useCallback } from 'react'
import type { HookFetcher } from '@lib/commerce/utils/types'
import useCommerceLogout from '@lib/commerce/use-logout'
import type { HookFetcher } from '../commerce/utils/types'
import useCommerceLogout from '../commerce/use-logout'
import useCustomer from './use-customer'
const defaultOpts = {

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react'
import type { HookFetcher } from '@lib/commerce/utils/types'
import { CommerceError } from '@lib/commerce/utils/errors'
import useCommerceSignup from '@lib/commerce/use-signup'
import type { HookFetcher } from '../commerce/utils/types'
import { CommerceError } from '../commerce/utils/errors'
import useCommerceSignup from '../commerce/use-signup'
import type { SignupBody } from './api/customers/signup'
import useCustomer from './use-customer'

View File

@ -1,7 +1,9 @@
import { useCallback } from 'react'
import { HookFetcher } from '@lib/commerce/utils/types'
import useAction from '@lib/commerce/utils/use-action'
import { HookFetcher } from '../../commerce/utils/types'
import { CommerceError } from '../../commerce/utils/errors'
import useWishlistAddItem from '../../commerce/wishlist/use-add-item'
import type { ItemBody, AddItemBody } from '../api/wishlist'
import useCustomer from '../use-customer'
import useWishlist, { Wishlist } from './use-wishlist'
const defaultOpts = {
@ -13,28 +15,37 @@ export type AddItemInput = ItemBody
export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
options,
{ wishlistId, item },
{ item },
fetch
) => {
// TODO: add validations before doing the fetch
return fetch({
...defaultOpts,
...options,
body: { wishlistId, item },
body: { item },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useAddItem = (wishlistId: string) => {
const { mutate } = useWishlist(wishlistId)
const fn = useAction<Wishlist, AddItemBody>(defaultOpts, customFetcher)
const useAddItem = () => {
const { data: customer } = useCustomer()
const { mutate } = useWishlist()
const fn = useWishlistAddItem(defaultOpts, customFetcher)
return useCallback(
async function addItem(input: AddItemInput) {
const data = await fn({ wishlistId, item: input })
if (!customer) {
// A signed customer is required in order to have a wishlist
throw new CommerceError({
message: 'Signed customer not found',
})
}
const data = await fn({ item: input })
await mutate(data, false)
return data
},
[fn, mutate]
[fn, mutate, customer]
)
}

View File

@ -1,45 +1,55 @@
import { useCallback } from 'react'
import { HookFetcher } from '@lib/commerce/utils/types'
import useAction from '@lib/commerce/utils/use-action'
import { HookFetcher } from '../../commerce/utils/types'
import { CommerceError } from '../../commerce/utils/errors'
import useWishlistRemoveItem from '../../commerce/wishlist/use-remove-item'
import type { RemoveItemBody } from '../api/wishlist'
import useCustomer from '../use-customer'
import useWishlist, { Wishlist } from './use-wishlist'
const defaultOpts = {
url: '/api/bigcommerce/wishlists',
url: '/api/bigcommerce/wishlist',
method: 'DELETE',
}
export type RemoveItemInput = {
id: string
id: string | number
}
export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = (
options,
{ wishlistId, itemId },
{ itemId },
fetch
) => {
return fetch({
...defaultOpts,
...options,
body: { wishlistId, itemId },
body: { itemId },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useRemoveItem = (wishlistId: string, item?: any) => {
const { mutate } = useWishlist(wishlistId)
const fn = useAction<Wishlist | null, RemoveItemBody>(
const useRemoveItem = () => {
const { data: customer } = useCustomer()
const { mutate } = useWishlist()
const fn = useWishlistRemoveItem<Wishlist | null, RemoveItemBody>(
defaultOpts,
customFetcher
)
return useCallback(
async function removeItem(input: RemoveItemInput) {
const data = await fn({ wishlistId, itemId: input.id ?? item?.id })
if (!customer) {
// A signed customer is required in order to have a wishlist
throw new CommerceError({
message: 'Signed customer not found',
})
}
const data = await fn({ itemId: String(input.id) })
await mutate(data, false)
return data
},
[fn, mutate]
[fn, mutate, customer]
)
}

View File

@ -3,9 +3,9 @@ import useRemoveItem from './use-remove-item'
// This hook is probably not going to be used, but it's here
// to show how a commerce should be structuring it
export default function useWishlistActions(wishlistId: string) {
const addItem = useAddItem(wishlistId)
const removeItem = useRemoveItem(wishlistId)
export default function useWishlistActions() {
const addItem = useAddItem()
const removeItem = useRemoveItem()
return { addItem, removeItem }
}

View File

@ -1,36 +1,48 @@
import { HookFetcher } from '@lib/commerce/utils/types'
import useData from '@lib/commerce/utils/use-data'
import { HookFetcher } from '../../commerce/utils/types'
import { SwrOptions } from '../../commerce/utils/use-data'
import useCommerceWishlist from '../../commerce/wishlist/use-wishlist'
import type { Wishlist } from '../api/wishlist'
import useCustomer from '../use-customer'
const defaultOpts = {
url: '/api/bigcommerce/wishlists',
url: '/api/bigcommerce/wishlist',
method: 'GET',
}
export type { Wishlist }
export type WishlistInput = {
wishlistId: string | undefined
}
export const fetcher: HookFetcher<Wishlist | null, WishlistInput> = (
export const fetcher: HookFetcher<Wishlist | null, { customerId?: number }> = (
options,
{ wishlistId },
{ customerId },
fetch
) => {
return fetch({
...defaultOpts,
...options,
body: { wishlistId },
})
return customerId ? fetch({ ...defaultOpts, ...options }) : null
}
export function extendHook(customFetcher: typeof fetcher) {
const useWishlists = (wishlistId: string) => {
const fetchFn: typeof fetcher = (options, input, fetch) => {
return customFetcher(options, input, fetch)
}
const response = useData(defaultOpts, [['wishlistId', wishlistId]], fetchFn)
export function extendHook(
customFetcher: typeof fetcher,
swrOptions?: SwrOptions<Wishlist | null, { customerId?: number }>
) {
const useWishlists = () => {
const { data: customer } = useCustomer()
const response = useCommerceWishlist(
defaultOpts,
[['customerId', customer?.entityId]],
customFetcher,
{
revalidateOnFocus: false,
...swrOptions,
}
)
// Uses a getter to only calculate the prop when required
// response.data is also a getter and it's better to not trigger it early
Object.defineProperty(response, 'isEmpty', {
get() {
return (response.data?.items?.length || 0) > 0
},
set: (x) => x,
})
return response
}

View File

@ -4,7 +4,7 @@ import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
import useData, { SwrOptions } from '../utils/use-data'
import { useCommerce } from '..'
export type CartResponse<C> = responseInterface<C, Error> & {
export type CartResponse<Result> = responseInterface<Result, Error> & {
isEmpty: boolean
}

View File

@ -0,0 +1,5 @@
import useAction from '../utils/use-action'
const useAddItem = useAction
export default useAddItem

View File

@ -0,0 +1,5 @@
import useAction from '../utils/use-action'
const useRemoveItem = useAction
export default useRemoveItem

View File

@ -0,0 +1,17 @@
import type { responseInterface } from 'swr'
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
import useData, { SwrOptions } from '../utils/use-data'
export type WishlistResponse<Result> = responseInterface<Result, Error> & {
isEmpty: boolean
}
export default function useWishlist<Result, Input = null>(
options: HookFetcherOptions,
input: HookInput,
fetcherFn: HookFetcher<Result, Input>,
swrOptions?: SwrOptions<Result, Input>
) {
const response = useData(options, input, fetcherFn, swrOptions)
return Object.assign(response, { isEmpty: true }) as WishlistResponse<Result>
}

View File

@ -0,0 +1,3 @@
import wishlistApi from '@lib/bigcommerce/api/wishlist'
export default wishlistApi()

View File

@ -5,11 +5,10 @@ import getAllProducts from '@lib/bigcommerce/api/operations/get-all-products'
import getSiteInfo from '@lib/bigcommerce/api/operations/get-site-info'
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
import rangeMap from '@lib/range-map'
import { getCategoryPath, getDesignerPath } from '@utils/search'
import { Layout } from '@components/core'
import { Grid, Marquee, Hero } from '@components/ui'
import { ProductCard } from '@components/product'
import Link from 'next/link'
import HomeAllProductsGrid from '@components/core/HomeAllProductsGrid'
export async function getStaticProps({
preview,
@ -129,53 +128,11 @@ export default function Home({
/>
))}
</Marquee>
<div className="py-12 flex flex-row w-full px-6">
<div className="pr-3 w-48 relative">
<div className="sticky top-32">
<ul className="mb-10">
<li className="py-1 text-base font-bold tracking-wide">
<Link href={getCategoryPath('')}>
<a>All Categories</a>
</Link>
</li>
{categories.map((cat) => (
<li key={cat.path} className="py-1 text-accents-8">
<Link href={getCategoryPath(cat.path)}>
<a>{cat.name}</a>
</Link>
</li>
))}
</ul>
<ul className="">
<li className="py-1 text-base font-bold tracking-wide">
<Link href={getDesignerPath('')}>
<a>All Designers</a>
</Link>
</li>
{brands.flatMap(({ node }) => (
<li key={node.path} className="py-1 text-accents-8">
<Link href={getDesignerPath(node.path)}>
<a>{node.name}</a>
</Link>
</li>
))}
</ul>
</div>
</div>
<div className="flex-1">
<Grid layout="normal">
{newestProducts.map(({ node }) => (
<ProductCard
key={node.path}
product={node}
variant="simple"
imgWidth={480}
imgHeight={480}
/>
))}
</Grid>
</div>
</div>
<HomeAllProductsGrid
categories={categories}
brands={brands}
newestProducts={newestProducts}
/>
</div>
)
}