forked from crowetic/commerce
Merge branch 'master' into arzafran/responsive-bottom-grid
This commit is contained in:
commit
0c9271861f
@ -1 +1,7 @@
|
|||||||
# Next.js Commerce
|
# Next.js Commerce
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
## Todo
|
||||||
|
|
||||||
|
## Contribute
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
:root {
|
:root {
|
||||||
--primary: white;
|
--primary: #ffffff;
|
||||||
--primary-2: #f1f3f5;
|
--primary-2: #f1f3f5;
|
||||||
--secondary: black;
|
--secondary: #000000;
|
||||||
--secondary-2: #111;
|
--secondary-2: #111;
|
||||||
|
|
||||||
--selection: var(--cyan);
|
--selection: var(--cyan);
|
||||||
|
|
||||||
--text-base: black;
|
--text-base: #000000;
|
||||||
--text-primary: black;
|
--text-primary: #000000;
|
||||||
--text-secondary: white;
|
--text-secondary: white;
|
||||||
|
|
||||||
--hover: rgba(0, 0, 0, 0.075);
|
--hover: rgba(0, 0, 0, 0.075);
|
||||||
@ -38,9 +38,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='dark'] {
|
[data-theme='dark'] {
|
||||||
--primary: black;
|
--primary: #000000;
|
||||||
--primary-2: #111;
|
--primary-2: #111;
|
||||||
--secondary: white;
|
--secondary: #ffffff;
|
||||||
--secondary-2: #f1f3f5;
|
--secondary-2: #f1f3f5;
|
||||||
--hover: rgba(255, 255, 255, 0.075);
|
--hover: rgba(255, 255, 255, 0.075);
|
||||||
--hover-1: rgba(255, 255, 255, 0.15);
|
--hover-1: rgba(255, 255, 255, 0.15);
|
||||||
|
@ -70,8 +70,8 @@ const LoginView: FC<Props> = () => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Input placeholder="Email" onChange={setEmail} />
|
<Input type="email" placeholder="Email" onChange={setEmail} />
|
||||||
<Input placeholder="Password" onChange={setPassword} />
|
<Input type="password" placeholder="Password" onChange={setPassword} />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="slim"
|
variant="slim"
|
||||||
|
@ -59,7 +59,7 @@ const SignUpView: FC<Props> = () => {
|
|||||||
}, [handleValidation])
|
}, [handleValidation])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-96 flex flex-col justify-between p-3">
|
<div className="w-80 flex flex-col justify-between p-3">
|
||||||
<div className="flex justify-center pb-12 ">
|
<div className="flex justify-center pb-12 ">
|
||||||
<Logo width="64px" height="64px" />
|
<Logo width="64px" height="64px" />
|
||||||
</div>
|
</div>
|
||||||
@ -69,8 +69,8 @@ const SignUpView: FC<Props> = () => {
|
|||||||
)}
|
)}
|
||||||
<Input placeholder="First Name" onChange={setFirstName} />
|
<Input placeholder="First Name" onChange={setFirstName} />
|
||||||
<Input placeholder="Last Name" onChange={setLastName} />
|
<Input placeholder="Last Name" onChange={setLastName} />
|
||||||
<Input placeholder="Email" onChange={setEmail} />
|
<Input type="email" placeholder="Email" onChange={setEmail} />
|
||||||
<Input placeholder="Password" onChange={setPassword} />
|
<Input type="password" placeholder="Password" onChange={setPassword} />
|
||||||
<span className="text-accents-8">
|
<span className="text-accents-8">
|
||||||
<span className="inline-block align-middle ">
|
<span className="inline-block align-middle ">
|
||||||
<Info width="15" height="15" />
|
<Info width="15" height="15" />
|
||||||
|
@ -1,2 +1,15 @@
|
|||||||
.root {
|
.root {
|
||||||
|
@apply h-full flex flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root.empty {
|
||||||
|
@apply bg-secondary text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root.success {
|
||||||
|
@apply bg-green text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root.error {
|
||||||
|
@apply bg-red text-white;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { useUI } from '@components/ui/context'
|
|||||||
import useCart from '@lib/bigcommerce/cart/use-cart'
|
import useCart from '@lib/bigcommerce/cart/use-cart'
|
||||||
import usePrice from '@lib/bigcommerce/use-price'
|
import usePrice from '@lib/bigcommerce/use-price'
|
||||||
import CartItem from '../CartItem'
|
import CartItem from '../CartItem'
|
||||||
|
import s from './CartSidebarView.module.css'
|
||||||
|
|
||||||
const CartSidebarView: FC = () => {
|
const CartSidebarView: FC = () => {
|
||||||
const { data, isEmpty } = useCart()
|
const { data, isEmpty } = useCart()
|
||||||
@ -32,10 +33,10 @@ const CartSidebarView: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('h-full flex flex-col', {
|
className={cn(s.root, {
|
||||||
'bg-secondary text-secondary': isEmpty,
|
[s.empty]: error,
|
||||||
'bg-red text-white': error,
|
[s.empty]: success,
|
||||||
'bg-green text-white': success,
|
[s.empty]: isEmpty,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<header className="px-4 pt-6 pb-4 sm:px-6">
|
<header className="px-4 pt-6 pb-4 sm:px-6">
|
||||||
|
@ -6,7 +6,7 @@ import s from './Featurebar.module.css'
|
|||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description?: string
|
||||||
hide?: boolean
|
hide?: boolean
|
||||||
action?: React.ReactNode
|
action?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
@ -77,8 +77,7 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
|
|||||||
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
|
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
|
||||||
</Modal>
|
</Modal>
|
||||||
<Featurebar
|
<Featurebar
|
||||||
title="This site uses cookies to improve your experience."
|
title="This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy."
|
||||||
description="By clicking, you agree to our Privacy Policy."
|
|
||||||
hide={acceptedCookies}
|
hide={acceptedCookies}
|
||||||
action={
|
action={
|
||||||
<Button className="mx-5" onClick={() => setAcceptedCookies(true)}>
|
<Button className="mx-5" onClick={() => setAcceptedCookies(true)}>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
@apply mr-6 cursor-pointer relative transition ease-in-out duration-100 text-primary flex items-center outline-none text-primary;
|
@apply mr-6 cursor-pointer relative transition ease-in-out duration-100 flex items-center outline-none text-primary;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply text-accents-6 transition scale-110 duration-100;
|
@apply text-accents-6 transition scale-110 duration-100;
|
||||||
|
@ -83,6 +83,7 @@
|
|||||||
.simple {
|
.simple {
|
||||||
& .squareBg {
|
& .squareBg {
|
||||||
@apply bg-accents-0 !important;
|
@apply bg-accents-0 !important;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg width='48' height='46' viewBox='0 0 48 46' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cline opacity='0.05' x1='9.41421' y1='8' x2='21' y2='19.5858' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cline opacity='0.05' x1='1' y1='-1' x2='17.3848' y2='-1' transform='matrix(-0.707107 0.707107 0.707107 0.707107 40 8)' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cline opacity='0.05' x1='1' y1='-1' x2='17.3848' y2='-1' transform='matrix(0.707107 -0.707107 -0.707107 -0.707107 8 38)' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cline opacity='0.05' x1='38.5858' y1='38' x2='27' y2='26.4142' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
|
||||||
}
|
}
|
||||||
|
|
||||||
& .productTitle {
|
& .productTitle {
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
import React, { FC, ReactNode, Component } from 'react'
|
import type { FC } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-all-products'
|
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-all-products'
|
||||||
import usePrice from '@lib/bigcommerce/use-price'
|
import usePrice from '@lib/bigcommerce/use-price'
|
||||||
import { Heart } from '@components/icons'
|
|
||||||
import { EnhancedImage } from '@components/core'
|
import { EnhancedImage } from '@components/core'
|
||||||
import s from './ProductCard.module.css'
|
import s from './ProductCard.module.css'
|
||||||
|
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
children?: ReactNode[] | Component[] | any[]
|
|
||||||
product: ProductNode
|
product: ProductNode
|
||||||
variant?: 'slim' | 'simple'
|
variant?: 'slim' | 'simple'
|
||||||
imgWidth: number
|
imgWidth: number | string
|
||||||
imgHeight: number
|
imgHeight: number | string
|
||||||
priority?: boolean
|
priority?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,7 +24,7 @@ const ProductCard: FC<Props> = ({
|
|||||||
imgHeight,
|
imgHeight,
|
||||||
priority,
|
priority,
|
||||||
}) => {
|
}) => {
|
||||||
const src = p.images.edges?.[0]?.node.urlOriginal!
|
const src = p.images.edges?.[0]?.node?.urlOriginal!
|
||||||
const { price } = usePrice({
|
const { price } = usePrice({
|
||||||
amount: p.prices?.price?.value,
|
amount: p.prices?.price?.value,
|
||||||
baseAmount: p.prices?.retailPrice?.value,
|
baseAmount: p.prices?.retailPrice?.value,
|
||||||
@ -65,9 +64,11 @@ const ProductCard: FC<Props> = ({
|
|||||||
</h3>
|
</h3>
|
||||||
<span className={s.productPrice}>{price}</span>
|
<span className={s.productPrice}>{price}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={s.wishlistButton}>
|
<WishlistButton
|
||||||
<Heart />
|
className={s.wishlistButton}
|
||||||
</div>
|
productId={p.entityId}
|
||||||
|
variant={p.variants.edges?.[0]!}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(s.imageContainer)}>
|
<div className={cn(s.imageContainer)}>
|
||||||
<EnhancedImage
|
<EnhancedImage
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply text-accents-1 cursor-pointer inline-flex px-10 rounded-sm leading-6 bg-secondary transition ease-in-out duration-150 shadow-sm font-semibold text-center justify-center uppercase py-4 uppercase text-center border border-transparent items-center;
|
@apply bg-secondary text-accents-1 cursor-pointer inline-flex px-10 rounded-sm leading-6 transition ease-in-out duration-150 shadow-sm font-semibold text-center justify-center uppercase py-4 border border-transparent items-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root:hover {
|
.root:hover {
|
||||||
|
@ -62,7 +62,7 @@ const Sidebar: FC<Props> = ({ className, children, open = false, onClose }) => {
|
|||||||
leaveFrom="translate-x-0"
|
leaveFrom="translate-x-0"
|
||||||
leaveTo="translate-x-full"
|
leaveTo="translate-x-full"
|
||||||
>
|
>
|
||||||
<div className="h-full w-screen max-w-md">
|
<div className="h-full md:w-screen md:max-w-md">
|
||||||
<div className="h-full flex flex-col text-base bg-accents-1 shadow-xl overflow-y-auto">
|
<div className="h-full flex flex-col text-base bg-accents-1 shadow-xl overflow-y-auto">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
65
components/wishlist/WishlistButton/WishlistButton.tsx
Normal file
65
components/wishlist/WishlistButton/WishlistButton.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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 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 { data } = useWishlist()
|
||||||
|
const { data: customer } = useCustomer()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const { openModal, setModalView } = useUI()
|
||||||
|
const isInWishlist = data?.items?.some(
|
||||||
|
(item) =>
|
||||||
|
item.product_id === productId &&
|
||||||
|
item.variant_id === variant?.node.entityId
|
||||||
|
)
|
||||||
|
|
||||||
|
const addToWishlist = async (e: any) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
// A login is required before adding an item to the wishlist
|
||||||
|
if (!customer) {
|
||||||
|
setModalView('LOGIN_VIEW')
|
||||||
|
return openModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addItem({
|
||||||
|
productId,
|
||||||
|
variantId: variant?.node.entityId!,
|
||||||
|
})
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
} catch (err) {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
className={cn({ 'opacity-50': loading }, className)}
|
||||||
|
onClick={addToWishlist}
|
||||||
|
>
|
||||||
|
<Heart fill={isInWishlist ? 'white' : 'none'} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WishlistButton
|
1
components/wishlist/WishlistButton/index.ts
Normal file
1
components/wishlist/WishlistButton/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './WishlistButton'
|
@ -1,4 +1,4 @@
|
|||||||
import parseItem from '../../utils/parse-item'
|
import { parseCartItem } from '../../utils/parse-item'
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
import type { CartHandlers } from '..'
|
import type { CartHandlers } from '..'
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ const addItem: CartHandlers['addItem'] = async ({
|
|||||||
const options = {
|
const options = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
line_items: [parseItem(item)],
|
line_items: [parseCartItem(item)],
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
const { data } = cartId
|
const { data } = cartId
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import parseItem from '../../utils/parse-item'
|
import { parseCartItem } from '../../utils/parse-item'
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
import type { CartHandlers } from '..'
|
import type { CartHandlers } from '..'
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ const updateItem: CartHandlers['updateItem'] = async ({
|
|||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
line_item: parseItem(item),
|
line_item: parseCartItem(item),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -13,7 +13,7 @@ async function getAllPages(opts?: {
|
|||||||
preview?: boolean
|
preview?: boolean
|
||||||
}): Promise<GetAllPagesResult>
|
}): Promise<GetAllPagesResult>
|
||||||
|
|
||||||
async function getAllPages<T extends { pages: any[] }, V = any>(opts: {
|
async function getAllPages<T extends { pages: any[] }>(opts: {
|
||||||
url: string
|
url: string
|
||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
preview?: boolean
|
preview?: boolean
|
||||||
|
34
lib/bigcommerce/api/operations/get-customer-id.ts
Normal file
34
lib/bigcommerce/api/operations/get-customer-id.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { GetCustomerIdQuery } from '@lib/bigcommerce/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
|
51
lib/bigcommerce/api/operations/get-customer-wishlist.ts
Normal file
51
lib/bigcommerce/api/operations/get-customer-wishlist.ts
Normal 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
|
@ -1,9 +1,13 @@
|
|||||||
|
import type { ItemBody as WishlistItemBody } from '../wishlist'
|
||||||
import type { ItemBody } from '../cart'
|
import type { ItemBody } from '../cart'
|
||||||
|
|
||||||
const parseItem = (item: ItemBody) => ({
|
export const parseWishlistItem = (item: WishlistItemBody) => ({
|
||||||
quantity: item.quantity,
|
|
||||||
product_id: item.productId,
|
product_id: item.productId,
|
||||||
variant_id: item.variantId,
|
variant_id: item.variantId,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default parseItem
|
export const parseCartItem = (item: ItemBody) => ({
|
||||||
|
quantity: item.quantity,
|
||||||
|
product_id: item.productId,
|
||||||
|
variant_id: item.variantId,
|
||||||
|
})
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import type { WishlistHandlers } from '..'
|
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 ({
|
const addItem: WishlistHandlers['addItem'] = async ({
|
||||||
res,
|
res,
|
||||||
body: { wishlistId, item },
|
body: { customerToken, item },
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
if (!item) {
|
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 = {
|
const options = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(
|
||||||
items: [item],
|
wishlist
|
||||||
}),
|
? {
|
||||||
|
items: [parseWishlistItem(item)],
|
||||||
}
|
}
|
||||||
const { data } = await config.storeApiFetch(
|
: {
|
||||||
`/v3/wishlists/${wishlistId}/items`,
|
name: 'Wishlist',
|
||||||
options
|
customer_id: customerId,
|
||||||
)
|
items: [parseWishlistItem(item)],
|
||||||
|
is_public: false,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = wishlist
|
||||||
|
? await config.storeApiFetch(`/v3/wishlists/${wishlist.id}/items`, options)
|
||||||
|
: await config.storeApiFetch('/v3/wishlists', options)
|
||||||
|
|
||||||
res.status(200).json({ data })
|
res.status(200).json({ data })
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,32 @@
|
|||||||
|
import getCustomerId from '../../operations/get-customer-id'
|
||||||
import type { Wishlist, WishlistHandlers } from '..'
|
import type { Wishlist, WishlistHandlers } from '..'
|
||||||
|
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
||||||
|
|
||||||
// Return wishlist info
|
// Return wishlist info
|
||||||
const getWishlist: WishlistHandlers['getWishlist'] = async ({
|
const getWishlist: WishlistHandlers['getWishlist'] = async ({
|
||||||
res,
|
res,
|
||||||
body: { wishlistId },
|
body: { customerToken },
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
let result: { data?: Wishlist } = {}
|
let result: { data?: Wishlist } = {}
|
||||||
|
|
||||||
try {
|
if (customerToken) {
|
||||||
result = await config.storeApiFetch(`/v3/wishlists/${wishlistId}`)
|
const customerId =
|
||||||
} catch (error) {
|
customerToken && (await getCustomerId({ customerToken, config }))
|
||||||
throw error
|
|
||||||
|
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 })
|
res.status(200).json({ data: result.data ?? null })
|
||||||
|
@ -11,15 +11,16 @@ import removeItem from './handlers/remove-item'
|
|||||||
import updateWishlist from './handlers/update-wishlist'
|
import updateWishlist from './handlers/update-wishlist'
|
||||||
import removeWishlist from './handlers/remove-wishlist'
|
import removeWishlist from './handlers/remove-wishlist'
|
||||||
import addWishlist from './handlers/add-wishlist'
|
import addWishlist from './handlers/add-wishlist'
|
||||||
|
import { definitions } from '../definitions/wishlist'
|
||||||
|
|
||||||
type Body<T> = Partial<T> | undefined
|
type Body<T> = Partial<T> | undefined
|
||||||
|
|
||||||
export type ItemBody = {
|
export type ItemBody = {
|
||||||
product_id: number
|
productId: number
|
||||||
variant_id: 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 = { wishlistId: string; itemId: string }
|
||||||
|
|
||||||
@ -32,21 +33,11 @@ export type WishlistBody = {
|
|||||||
|
|
||||||
export type AddWishlistBody = { wishlist: WishlistBody }
|
export type AddWishlistBody = { wishlist: WishlistBody }
|
||||||
|
|
||||||
// TODO: this type should match:
|
export type Wishlist = definitions['wishlist_Full']
|
||||||
// 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 WishlistHandlers = {
|
export type WishlistHandlers = {
|
||||||
getAllWishlists: BigcommerceHandler<Wishlist[], { customerId?: string }>
|
getAllWishlists: BigcommerceHandler<Wishlist[], { customerId?: string }>
|
||||||
getWishlist: BigcommerceHandler<Wishlist, { wishlistId?: string }>
|
getWishlist: BigcommerceHandler<Wishlist, { customerToken?: string }>
|
||||||
addWishlist: BigcommerceHandler<
|
addWishlist: BigcommerceHandler<
|
||||||
Wishlist,
|
Wishlist,
|
||||||
{ wishlistId: string } & Body<AddWishlistBody>
|
{ wishlistId: string } & Body<AddWishlistBody>
|
||||||
@ -57,7 +48,7 @@ export type WishlistHandlers = {
|
|||||||
>
|
>
|
||||||
addItem: BigcommerceHandler<
|
addItem: BigcommerceHandler<
|
||||||
Wishlist,
|
Wishlist,
|
||||||
{ wishlistId: string } & Body<AddItemBody>
|
{ customerToken?: string } & Body<AddItemBody>
|
||||||
>
|
>
|
||||||
removeItem: BigcommerceHandler<
|
removeItem: BigcommerceHandler<
|
||||||
Wishlist,
|
Wishlist,
|
||||||
@ -77,17 +68,21 @@ const wishlistApi: BigcommerceApiHandler<Wishlist, WishlistHandlers> = async (
|
|||||||
) => {
|
) => {
|
||||||
if (!isAllowedMethod(req, res, METHODS)) return
|
if (!isAllowedMethod(req, res, METHODS)) return
|
||||||
|
|
||||||
|
const { cookies } = req
|
||||||
|
const customerToken = cookies[config.customerCookie]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { wishlistId, itemId, customerId } = req.body
|
const { wishlistId, itemId, customerId } = req.body
|
||||||
|
|
||||||
// Return current wishlist info
|
// Return current wishlist info
|
||||||
if (req.method === 'GET' && wishlistId) {
|
if (req.method === 'GET') {
|
||||||
const body = { wishlistId: wishlistId as string }
|
const body = { customerToken }
|
||||||
return await handlers['getWishlist']({ req, res, config, body })
|
return await handlers['getWishlist']({ req, res, config, body })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add an item to the wishlist
|
// Add an item to the wishlist
|
||||||
if (req.method === 'POST' && wishlistId) {
|
if (req.method === 'POST') {
|
||||||
const body = { ...req.body, wishlistId }
|
const body = { ...req.body, customerToken }
|
||||||
return await handlers['addItem']({ req, res, config, body })
|
return await handlers['addItem']({ req, res, config, body })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ export const fetcher: HookFetcher<Cart, AddItemBody> = (
|
|||||||
export function extendHook(customFetcher: typeof fetcher) {
|
export function extendHook(customFetcher: typeof fetcher) {
|
||||||
const useAddItem = () => {
|
const useAddItem = () => {
|
||||||
const { mutate } = useCart()
|
const { mutate } = useCart()
|
||||||
const fn = useCartAddItem<Cart, AddItemBody>(defaultOpts, customFetcher)
|
const fn = useCartAddItem(defaultOpts, customFetcher)
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function addItem(input: AddItemInput) {
|
async function addItem(input: AddItemInput) {
|
||||||
|
@ -23,23 +23,23 @@ export function extendHook(
|
|||||||
swrOptions?: SwrOptions<Cart | null, CartInput>
|
swrOptions?: SwrOptions<Cart | null, CartInput>
|
||||||
) {
|
) {
|
||||||
const useCart = () => {
|
const useCart = () => {
|
||||||
const cart = useCommerceCart(defaultOpts, [], customFetcher, {
|
const response = useCommerceCart(defaultOpts, [], customFetcher, {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
...swrOptions,
|
...swrOptions,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Uses a getter to only calculate the prop when required
|
// 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
|
// response.data is also a getter and it's better to not trigger it early
|
||||||
Object.defineProperty(cart, 'isEmpty', {
|
Object.defineProperty(response, 'isEmpty', {
|
||||||
get() {
|
get() {
|
||||||
return Object.values(cart.data?.line_items ?? {}).every(
|
return Object.values(response.data?.line_items ?? {}).every(
|
||||||
(items) => !items.length
|
(items) => !items.length
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
set: (x) => x,
|
set: (x) => x,
|
||||||
})
|
})
|
||||||
|
|
||||||
return cart
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
useCart.extend = extendHook
|
useCart.extend = extendHook
|
||||||
|
6
lib/bigcommerce/schema.d.ts
vendored
6
lib/bigcommerce/schema.d.ts
vendored
@ -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<{
|
export type GetProductQueryVariables = Exact<{
|
||||||
hasLocale?: Maybe<Scalars['Boolean']>
|
hasLocale?: Maybe<Scalars['Boolean']>
|
||||||
locale?: Maybe<Scalars['String']>
|
locale?: Maybe<Scalars['String']>
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { HookFetcher } from '@lib/commerce/utils/types'
|
import { HookFetcher } from '@lib/commerce/utils/types'
|
||||||
import useAction from '@lib/commerce/utils/use-action'
|
import { CommerceError } from '@lib/commerce/utils/errors'
|
||||||
|
import useWishlistAddItem from '@lib/commerce/wishlist/use-add-item'
|
||||||
import type { ItemBody, AddItemBody } from '../api/wishlist'
|
import type { ItemBody, AddItemBody } from '../api/wishlist'
|
||||||
|
import useCustomer from '../use-customer'
|
||||||
import useWishlist, { Wishlist } from './use-wishlist'
|
import useWishlist, { Wishlist } from './use-wishlist'
|
||||||
|
|
||||||
const defaultOpts = {
|
const defaultOpts = {
|
||||||
@ -13,24 +15,33 @@ export type AddItemInput = ItemBody
|
|||||||
|
|
||||||
export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
|
export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
|
||||||
options,
|
options,
|
||||||
{ wishlistId, item },
|
{ item },
|
||||||
fetch
|
fetch
|
||||||
) => {
|
) => {
|
||||||
|
// TODO: add validations before doing the fetch
|
||||||
return fetch({
|
return fetch({
|
||||||
...defaultOpts,
|
...defaultOpts,
|
||||||
...options,
|
...options,
|
||||||
body: { wishlistId, item },
|
body: { item },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
export function extendHook(customFetcher: typeof fetcher) {
|
||||||
const useAddItem = (wishlistId: string) => {
|
const useAddItem = () => {
|
||||||
const { mutate } = useWishlist(wishlistId)
|
const { data: customer } = useCustomer()
|
||||||
const fn = useAction<Wishlist, AddItemBody>(defaultOpts, customFetcher)
|
const { mutate } = useWishlist()
|
||||||
|
const fn = useWishlistAddItem(defaultOpts, customFetcher)
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function addItem(input: AddItemInput) {
|
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)
|
await mutate(data, false)
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
|
@ -1,36 +1,48 @@
|
|||||||
import { HookFetcher } from '@lib/commerce/utils/types'
|
import { HookFetcher } from '@lib/commerce/utils/types'
|
||||||
import useData from '@lib/commerce/utils/use-data'
|
import { SwrOptions } from '@lib/commerce/utils/use-data'
|
||||||
|
import useCommerceWishlist from '@lib/commerce/wishlist/use-wishlist'
|
||||||
import type { Wishlist } from '../api/wishlist'
|
import type { Wishlist } from '../api/wishlist'
|
||||||
|
import useCustomer from '../use-customer'
|
||||||
|
|
||||||
const defaultOpts = {
|
const defaultOpts = {
|
||||||
url: '/api/bigcommerce/wishlists',
|
url: '/api/bigcommerce/wishlist',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { Wishlist }
|
export type { Wishlist }
|
||||||
|
|
||||||
export type WishlistInput = {
|
export const fetcher: HookFetcher<Wishlist | null, { customerId?: number }> = (
|
||||||
wishlistId: string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Wishlist | null, WishlistInput> = (
|
|
||||||
options,
|
options,
|
||||||
{ wishlistId },
|
{ customerId },
|
||||||
fetch
|
fetch
|
||||||
) => {
|
) => {
|
||||||
return fetch({
|
return customerId ? fetch({ ...defaultOpts, ...options }) : null
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
|
||||||
body: { wishlistId },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
export function extendHook(
|
||||||
const useWishlists = (wishlistId: string) => {
|
customFetcher: typeof fetcher,
|
||||||
const fetchFn: typeof fetcher = (options, input, fetch) => {
|
swrOptions?: SwrOptions<Wishlist | null, { customerId?: number }>
|
||||||
return customFetcher(options, input, fetch)
|
) {
|
||||||
|
const useWishlists = () => {
|
||||||
|
const { data: customer } = useCustomer()
|
||||||
|
const response = useCommerceWishlist(
|
||||||
|
defaultOpts,
|
||||||
|
[['customerId', customer?.entityId]],
|
||||||
|
customFetcher,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
...swrOptions,
|
||||||
}
|
}
|
||||||
const response = useData(defaultOpts, [['wishlistId', wishlistId]], fetchFn)
|
)
|
||||||
|
|
||||||
|
// 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
|
return response
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
|
|||||||
import useData, { SwrOptions } from '../utils/use-data'
|
import useData, { SwrOptions } from '../utils/use-data'
|
||||||
import { useCommerce } from '..'
|
import { useCommerce } from '..'
|
||||||
|
|
||||||
export type CartResponse<C> = responseInterface<C, Error> & {
|
export type CartResponse<Result> = responseInterface<Result, Error> & {
|
||||||
isEmpty: boolean
|
isEmpty: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
lib/commerce/wishlist/use-add-item.tsx
Normal file
5
lib/commerce/wishlist/use-add-item.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import useAction from '../utils/use-action'
|
||||||
|
|
||||||
|
const useAddItem = useAction
|
||||||
|
|
||||||
|
export default useAddItem
|
17
lib/commerce/wishlist/use-wishlist.tsx
Normal file
17
lib/commerce/wishlist/use-wishlist.tsx
Normal 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>
|
||||||
|
}
|
3
pages/api/bigcommerce/wishlist.ts
Normal file
3
pages/api/bigcommerce/wishlist.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import wishlistApi from '@lib/bigcommerce/api/wishlist'
|
||||||
|
|
||||||
|
export default wishlistApi()
|
@ -80,8 +80,8 @@ export default function Home({
|
|||||||
key={node.path}
|
key={node.path}
|
||||||
product={node}
|
product={node}
|
||||||
// The first image is the largest one in the grid
|
// The first image is the largest one in the grid
|
||||||
imgWidth={i === 0 ? 1600 : 820}
|
imgWidth={i === 0 ? '65vw' : '30vw'}
|
||||||
imgHeight={i === 0 ? 1600 : 820}
|
imgHeight={i === 0 ? '45vw' : '22vw'}
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user