Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic

This commit is contained in:
cond0r 2021-02-22 09:14:22 +02:00
commit 005fe9d6c9
63 changed files with 900 additions and 884 deletions

118
README.md
View File

@ -42,57 +42,6 @@ Additionally, we need to ensure feature parity (not all providers have e.g. wish
People actively working on this project: @okbel & @lfades. People actively working on this project: @okbel & @lfades.
## Troubleshoot
<details>
<summary>I already own a BigCommerce store. What should I do?</summary>
<br>
First thing you do is: <b>set your environment variables</b>
<br>
<br>
.env.local
```sh
BIGCOMMERCE_STOREFRONT_API_URL=<>
BIGCOMMERCE_STOREFRONT_API_TOKEN=<>
BIGCOMMERCE_STORE_API_URL=<>
BIGCOMMERCE_STORE_API_TOKEN=<>
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
```
If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials.
1. Install Vercel CLI: `npm i -g vercel`
2. Link local instance with Vercel and Github accounts (creates .vercel file): `vercel link`
3. Download your environment variables: `vercel env pull .env.local`
Next, you're free to customize the starter. More updates coming soon. Stay tuned.
</details>
<details>
<summary>BigCommerce shows a Coming Soon page and requests a Preview Code</summary>
<br>
After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard.
<br>
<br>
BigCommerce team has been notified and they plan to add more detailed about this subject.
</details>
## Contribute
Our commitment to Open Source can be found [here](https://vercel.com/oss).
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
2. Create a new branch `git checkout -b MY_BRANCH_NAME`
3. Install yarn: `npm install -g yarn`
4. Install the dependencies: `yarn`
5. Duplicate `.env.template` and rename it to `.env.local`.
6. Add proper store values to `.env.local`.
7. Run `yarn dev` to build and watch for code changes
8. The development branch is `canary` (this is the branch pull requests should be made against).
On a release, `canary` branch is rebased into `master`.
## Framework ## Framework
Framework is where the data comes from. It contains mostly hooks and functions. Framework is where the data comes from. It contains mostly hooks and functions.
@ -132,3 +81,70 @@ import { useUI } from '@components/ui'
import { useCustomer } from '@framework/customer' import { useCustomer } from '@framework/customer'
import { useAddItem, useWishlist, useRemoveItem } from '@framework/wishlist' import { useAddItem, useWishlist, useRemoveItem } from '@framework/wishlist'
``` ```
## Config
### Features
In order to make the UI entirely functional, we need to specify which features certain providers do not **provide**.
**Disabling wishlist:**
```
{
"features": {
"wishlist": false
}
}
```
## Contribute
Our commitment to Open Source can be found [here](https://vercel.com/oss).
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
2. Create a new branch `git checkout -b MY_BRANCH_NAME`
3. Install yarn: `npm install -g yarn`
4. Install the dependencies: `yarn`
5. Duplicate `.env.template` and rename it to `.env.local`.
6. Add proper store values to `.env.local`.
7. Run `yarn dev` to build and watch for code changes
8. The development branch is `canary` (this is the branch pull requests should be made against).
On a release, `canary` branch is rebased into `master`.
## Troubleshoot
<details>
<summary>I already own a BigCommerce store. What should I do?</summary>
<br>
First thing you do is: <b>set your environment variables</b>
<br>
<br>
.env.local
```sh
BIGCOMMERCE_STOREFRONT_API_URL=<>
BIGCOMMERCE_STOREFRONT_API_TOKEN=<>
BIGCOMMERCE_STORE_API_URL=<>
BIGCOMMERCE_STORE_API_TOKEN=<>
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
```
If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials.
1. Install Vercel CLI: `npm i -g vercel`
2. Link local instance with Vercel and Github accounts (creates .vercel file): `vercel link`
3. Download your environment variables: `vercel env pull .env.local`
Next, you're free to customize the starter. More updates coming soon. Stay tuned.
</details>
<details>
<summary>BigCommerce shows a Coming Soon page and requests a Preview Code</summary>
<br>
After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard.
<br>
<br>
BigCommerce team has been notified and they plan to add more detailed about this subject.
</details>

View File

@ -33,7 +33,7 @@ const CartItem = ({
currencyCode, currencyCode,
}) })
const updateItem = useUpdateItem(item) const updateItem = useUpdateItem({ item })
const removeItem = useRemoveItem() const removeItem = useRemoveItem()
const [quantity, setQuantity] = useState(item.quantity) const [quantity, setQuantity] = useState(item.quantity)
const [removing, setRemoving] = useState(false) const [removing, setRemoving] = useState(false)

View File

@ -9,7 +9,7 @@ import usePrice from '@framework/product/use-price'
import CartItem from '../CartItem' import CartItem from '../CartItem'
import s from './CartSidebarView.module.css' import s from './CartSidebarView.module.css'
const CartSidebarView: FC = () => { const CartSidebarView: FC<{ wishlist?: boolean }> = ({ wishlist }) => {
const { closeSidebar } = useUI() const { closeSidebar } = useUI()
const { data, isLoading, isEmpty } = useCart() const { data, isLoading, isEmpty } = useCart()
@ -48,7 +48,7 @@ const CartSidebarView: FC = () => {
</button> </button>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<UserNav className="" /> <UserNav wishlist={wishlist} />
</div> </div>
</div> </div>
</header> </header>

View File

@ -5,14 +5,21 @@ import { Grid } from '@components/ui'
import { ProductCard } from '@components/product' import { ProductCard } from '@components/product'
import s from './HomeAllProductsGrid.module.css' import s from './HomeAllProductsGrid.module.css'
import { getCategoryPath, getDesignerPath } from '@lib/search' import { getCategoryPath, getDesignerPath } from '@lib/search'
import wishlist from '@framework/api/wishlist'
interface Props { interface Props {
categories?: any categories?: any
brands?: any brands?: any
products?: Product[] products?: Product[]
wishlist?: boolean
} }
const Head: FC<Props> = ({ categories, brands, products = [] }) => { const HomeAllProductsGrid: FC<Props> = ({
categories,
brands,
products = [],
wishlist = false,
}) => {
return ( return (
<div className={s.root}> <div className={s.root}>
<div className={s.asideWrapper}> <div className={s.asideWrapper}>
@ -44,6 +51,7 @@ const Head: FC<Props> = ({ categories, brands, products = [] }) => {
width: 480, width: 480,
height: 480, height: 480,
}} }}
wishlist={wishlist}
/> />
))} ))}
</Grid> </Grid>
@ -52,4 +60,4 @@ const Head: FC<Props> = ({ categories, brands, products = [] }) => {
) )
} }
export default Head export default HomeAllProductsGrid

View File

@ -41,10 +41,14 @@ const FeatureBar = dynamic(
interface Props { interface Props {
pageProps: { pageProps: {
pages?: Page[] pages?: Page[]
commerceFeatures: Record<string, boolean>
} }
} }
const Layout: FC<Props> = ({ children, pageProps }) => { const Layout: FC<Props> = ({
children,
pageProps: { commerceFeatures, ...pageProps },
}) => {
const { const {
displaySidebar, displaySidebar,
displayModal, displayModal,
@ -54,11 +58,11 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
} = useUI() } = useUI()
const { acceptedCookies, onAcceptCookies } = useAcceptCookies() const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
const { locale = 'en-US' } = useRouter() const { locale = 'en-US' } = useRouter()
const isWishlistEnabled = commerceFeatures.wishlist
return ( return (
<CommerceProvider locale={locale}> <CommerceProvider locale={locale}>
<div className={cn(s.root)}> <div className={cn(s.root)}>
<Navbar /> <Navbar wishlist={isWishlistEnabled} />
<main className="fit">{children}</main> <main className="fit">{children}</main>
<Footer pages={pageProps.pages} /> <Footer pages={pageProps.pages} />
@ -69,7 +73,7 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
</Modal> </Modal>
<Sidebar open={displaySidebar} onClose={closeSidebar}> <Sidebar open={displaySidebar} onClose={closeSidebar}>
<CartSidebarView /> <CartSidebarView wishlist={isWishlistEnabled} />
</Sidebar> </Sidebar>
<FeatureBar <FeatureBar

View File

@ -5,7 +5,7 @@ import { Searchbar, UserNav } from '@components/common'
import NavbarRoot from './NavbarRoot' import NavbarRoot from './NavbarRoot'
import s from './Navbar.module.css' import s from './Navbar.module.css'
const Navbar: FC = () => ( const Navbar: FC<{ wishlist?: boolean }> = ({ wishlist }) => (
<NavbarRoot> <NavbarRoot>
<Container> <Container>
<div className="relative flex flex-row justify-between py-4 align-center md:py-6"> <div className="relative flex flex-row justify-between py-4 align-center md:py-6">
@ -36,7 +36,7 @@ const Navbar: FC = () => (
</div> </div>
<div className="flex justify-end flex-1 space-x-8"> <div className="flex justify-end flex-1 space-x-8">
<UserNav /> <UserNav wishlist={wishlist} />
</div> </div>
</div> </div>

View File

@ -12,11 +12,12 @@ import { Avatar } from '@components/common'
interface Props { interface Props {
className?: string className?: string
wishlist?: boolean
} }
const countItem = (count: number, item: LineItem) => count + item.quantity const countItem = (count: number, item: LineItem) => count + item.quantity
const UserNav: FC<Props> = ({ className }) => { const UserNav: FC<Props> = ({ className, wishlist = false }) => {
const { data } = useCart() const { data } = useCart()
const { data: customer } = useCustomer() const { data: customer } = useCustomer()
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI() const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
@ -30,13 +31,15 @@ const UserNav: FC<Props> = ({ className }) => {
<Bag /> <Bag />
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>} {itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
</li> </li>
<li className={s.item}> {wishlist && (
<Link href="/wishlist"> <li className={s.item}>
<a onClick={closeSidebarIfPresent} aria-label="Wishlist"> <Link href="/wishlist">
<Heart /> <a onClick={closeSidebarIfPresent} aria-label="Wishlist">
</a> <Heart />
</Link> </a>
</li> </Link>
</li>
)}
<li className={s.item}> <li className={s.item}>
{customer ? ( {customer ? (
<DropdownMenu /> <DropdownMenu />

View File

@ -4,13 +4,14 @@ import Link from 'next/link'
import type { Product } from '@commerce/types' import type { Product } from '@commerce/types'
import s from './ProductCard.module.css' import s from './ProductCard.module.css'
import Image, { ImageProps } from 'next/image' import Image, { ImageProps } from 'next/image'
// import WishlistButton from '@components/wishlist/WishlistButton' import WishlistButton from '@components/wishlist/WishlistButton'
interface Props { interface Props {
className?: string className?: string
product: Product product: Product
variant?: 'slim' | 'simple' variant?: 'slim' | 'simple'
imgProps?: Omit<ImageProps, 'src'> imgProps?: Omit<ImageProps, 'src'>
wishlist?: boolean
} }
const placeholderImg = '/product-img-placeholder.svg' const placeholderImg = '/product-img-placeholder.svg'
@ -20,6 +21,7 @@ const ProductCard: FC<Props> = ({
product, product,
variant, variant,
imgProps, imgProps,
wishlist = false,
...props ...props
}) => ( }) => (
<Link href={`/product/${product.slug}`} {...props}> <Link href={`/product/${product.slug}`} {...props}>
@ -57,11 +59,13 @@ const ProductCard: FC<Props> = ({
{product.price.currencyCode} {product.price.currencyCode}
</span> </span>
</div> </div>
{/* <WishlistButton {wishlist && (
<WishlistButton
className={s.wishlistButton} className={s.wishlistButton}
productId={product.id} productId={product.id}
variant={product.variants[0]} variant={product.variants[0]}
/> */} />
)}
</div> </div>
<div className={s.imageContainer}> <div className={s.imageContainer}>
{product?.images && ( {product?.images && (

View File

@ -50,10 +50,12 @@ const ProductSlider: FC = ({ children }) => {
) )
return () => { return () => {
sliderContainerRef.current!.removeEventListener( if (sliderContainerRef.current) {
'touchstart', sliderContainerRef.current!.removeEventListener(
preventNavigation 'touchstart',
) preventNavigation
)
}
} }
}, []) }, [])

View File

@ -13,15 +13,16 @@ import usePrice from '@framework/product/use-price'
import { useAddItem } from '@framework/cart' import { useAddItem } from '@framework/cart'
import { getVariant, SelectedOptions } from '../helpers' import { getVariant, SelectedOptions } from '../helpers'
// import WishlistButton from '@components/wishlist/WishlistButton' import WishlistButton from '@components/wishlist/WishlistButton'
interface Props { interface Props {
className?: string className?: string
children?: any children?: any
product: Product product: Product
wishlist?: boolean
} }
const ProductView: FC<Props> = ({ product }) => { const ProductView: FC<Props> = ({ product, wishlist = false }) => {
const addItem = useAddItem() const addItem = useAddItem()
const { price } = usePrice({ const { price } = usePrice({
amount: product.price.value, amount: product.price.value,
@ -151,11 +152,13 @@ const ProductView: FC<Props> = ({ product }) => {
</Button> </Button>
</div> </div>
</div> </div>
{/* <WishlistButton {wishlist && (
className={s.wishlistButton} <WishlistButton
productId={product.id} className={s.wishlistButton}
variant={product.variants[0]!} productId={product.id}
/> */} variant={product.variants[0]!}
/>
)}
</div> </div>
</Container> </Container>
) )

View File

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

View File

@ -22,7 +22,7 @@ const WishlistCard: FC<Props> = ({ product }) => {
baseAmount: product.prices?.retailPrice?.value, baseAmount: product.prices?.retailPrice?.value,
currencyCode: product.prices?.price?.currencyCode!, currencyCode: product.prices?.price?.currencyCode!,
}) })
const removeItem = useRemoveItem({ includeProducts: true }) const removeItem = useRemoveItem({ wishlist: { includeProducts: true } })
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [removing, setRemoving] = useState(false) const [removing, setRemoving] = useState(false)
const addItem = useAddItem() const addItem = useAddItem()

View File

@ -1 +1,2 @@
export { default as WishlistCard } from './WishlistCard' export { default as WishlistCard } from './WishlistCard'
export { default as WishlistButton } from './WishlistButton'

View File

@ -1,54 +1,40 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import type { HookFetcher } from '@commerce/utils/types' import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors' import { CommerceError } from '@commerce/utils/errors'
import useCommerceLogin from '@commerce/use-login' import useLogin, { UseLogin } from '@commerce/use-login'
import type { LoginBody } from '../api/customers/login' import type { LoginBody } from '../api/customers/login'
import useCustomer from '../customer/use-customer' import useCustomer from '../customer/use-customer'
const defaultOpts = { export default useLogin as UseLogin<typeof handler>
url: '/api/bigcommerce/customers/login',
method: 'POST',
}
export type LoginInput = LoginBody export const handler: MutationHook<null, {}, LoginBody> = {
fetchOptions: {
url: '/api/bigcommerce/customers/login',
method: 'POST',
},
async fetcher({ input: { email, password }, options, fetch }) {
if (!(email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to login',
})
}
export const fetcher: HookFetcher<null, LoginBody> = ( return fetch({
options, ...options,
{ email, password }, body: { email, password },
fetch
) => {
if (!(email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to login',
}) })
} },
useHook: ({ fetch }) => () => {
return fetch({
...defaultOpts,
...options,
body: { email, password },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useLogin = () => {
const { revalidate } = useCustomer() const { revalidate } = useCustomer()
const fn = useCommerceLogin<null, LoginInput>(defaultOpts, customFetcher)
return useCallback( return useCallback(
async function login(input: LoginInput) { async function login(input) {
const data = await fn(input) const data = await fetch({ input })
await revalidate() await revalidate()
return data return data
}, },
[fn] [fetch, revalidate]
) )
} },
useLogin.extend = extendHook
return useLogin
} }
export default extendHook(fetcher)

View File

@ -1,38 +1,25 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import type { HookFetcher } from '@commerce/utils/types' import type { MutationHook } from '@commerce/utils/types'
import useCommerceLogout from '@commerce/use-logout' import useLogout, { UseLogout } from '@commerce/use-logout'
import useCustomer from '../customer/use-customer' import useCustomer from '../customer/use-customer'
const defaultOpts = { export default useLogout as UseLogout<typeof handler>
url: '/api/bigcommerce/customers/logout',
method: 'GET',
}
export const fetcher: HookFetcher<null> = (options, _, fetch) => { export const handler: MutationHook<null> = {
return fetch({ fetchOptions: {
...defaultOpts, url: '/api/bigcommerce/customers/logout',
...options, method: 'GET',
}) },
} useHook: ({ fetch }) => () => {
export function extendHook(customFetcher: typeof fetcher) {
const useLogout = () => {
const { mutate } = useCustomer() const { mutate } = useCustomer()
const fn = useCommerceLogout<null>(defaultOpts, customFetcher)
return useCallback( return useCallback(
async function login() { async function logout() {
const data = await fn(null) const data = await fetch()
await mutate(null, false) await mutate(null, false)
return data return data
}, },
[fn] [fetch, mutate]
) )
} },
useLogout.extend = extendHook
return useLogout
} }
export default extendHook(fetcher)

View File

@ -1,55 +1,44 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import type { HookFetcher } from '@commerce/utils/types' import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors' import { CommerceError } from '@commerce/utils/errors'
import useCommerceSignup from '@commerce/use-signup' import useSignup, { UseSignup } from '@commerce/use-signup'
import type { SignupBody } from '../api/customers/signup'
import useCustomer from '../customer/use-customer' import useCustomer from '../customer/use-customer'
import customerCreateMutation from '@framework/utils/mutations/customer-create'
import { CustomerCreateInput } from '@framework/schema'
const defaultOpts = { export default useSignup as UseSignup<typeof handler>
query: customerCreateMutation,
}
export const fetcher: HookFetcher<null, CustomerCreateInput> = ( export const handler: MutationHook<null, {}, SignupBody, SignupBody> = {
options, fetchOptions: {
{ firstName, lastName, email, password }, url: '/api/bigcommerce/customers/signup',
fetch method: 'POST',
) => { },
if (!(firstName && lastName && email && password)) { async fetcher({
throw new CommerceError({ input: { firstName, lastName, email, password },
message: options,
'A first name, last name, email and password are required to signup', fetch,
}) {
if (!(firstName && lastName && email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to signup',
})
}
return fetch({
...options,
body: { firstName, lastName, email, password },
}) })
} },
useHook: ({ fetch }) => () => {
return fetch({
...defaultOpts,
...options,
variables: { firstName, lastName, email, password },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useSignup = () => {
const { revalidate } = useCustomer() const { revalidate } = useCustomer()
const fn = useCommerceSignup<null, CustomerCreateInput>(
defaultOpts,
customFetcher
)
return useCallback( return useCallback(
async function signup(input: CustomerCreateInput) { async function signup(input) {
const data = await fn(input) const data = await fetch({ input })
await revalidate() await revalidate()
return data return data
}, },
[fn] [fetch, revalidate]
) )
} },
useSignup.extend = extendHook
return useSignup
} }
export default extendHook(fetcher)

View File

@ -1,5 +1,4 @@
export { default as useCart } from './use-cart' export { default as useCart } from './use-cart'
export { default as useAddItem } from './use-add-item' export { default as useAddItem } from './use-add-item'
export { default as useRemoveItem } from './use-remove-item' export { default as useRemoveItem } from './use-remove-item'
export { default as useWishlistActions } from './use-cart-actions' export { default as useUpdateItem } from './use-update-item'
export { default as useUpdateItem } from './use-cart-actions'

View File

@ -1,29 +1,24 @@
import type { MutationHandler } from '@commerce/utils/types' import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors' import { CommerceError } from '@commerce/utils/errors'
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item' import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
import { normalizeCart } from '../lib/normalize' import { normalizeCart } from '../lib/normalize'
import type { import type {
AddCartItemBody,
Cart, Cart,
BigcommerceCart, BigcommerceCart,
CartItemBody, CartItemBody,
AddCartItemBody,
} from '../types' } from '../types'
import useCart from './use-cart' import useCart from './use-cart'
import { BigcommerceProvider } from '..'
const defaultOpts = { export default useAddItem as UseAddItem<typeof handler>
url: '/api/bigcommerce/cart',
method: 'POST',
}
export default useAddItem as UseAddItem<BigcommerceProvider, CartItemBody> export const handler: MutationHook<Cart, {}, CartItemBody> = {
export const handler: MutationHandler<Cart, {}, AddCartItemBody> = {
fetchOptions: { fetchOptions: {
url: '/api/bigcommerce/cart', url: '/api/bigcommerce/cart',
method: 'GET', method: 'POST',
}, },
async fetcher({ input: { item }, options, fetch }) { async fetcher({ input: item, options, fetch }) {
if ( if (
item.quantity && item.quantity &&
(!Number.isInteger(item.quantity) || item.quantity! < 1) (!Number.isInteger(item.quantity) || item.quantity! < 1)
@ -34,20 +29,22 @@ export const handler: MutationHandler<Cart, {}, AddCartItemBody> = {
} }
const data = await fetch<BigcommerceCart, AddCartItemBody>({ const data = await fetch<BigcommerceCart, AddCartItemBody>({
...defaultOpts,
...options, ...options,
body: { item }, body: { item },
}) })
return normalizeCart(data) return normalizeCart(data)
}, },
useHook() { useHook: ({ fetch }) => () => {
const { mutate } = useCart() const { mutate } = useCart()
return async function addItem({ input, fetch }) { return useCallback(
const data = await fetch({ input }) async function addItem(input) {
await mutate(data, false) const data = await fetch({ input })
return data await mutate(data, false)
} return data
},
[fetch, mutate]
)
}, },
} }

View File

@ -1,13 +0,0 @@
import useAddItem from './use-add-item'
import useRemoveItem from './use-remove-item'
import useUpdateItem from './use-update-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 useCartActions() {
const addItem = useAddItem()
const updateItem = useUpdateItem()
const removeItem = useRemoveItem()
return { addItem, updateItem, removeItem }
}

View File

@ -1,13 +1,12 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import { HookHandler } from '@commerce/utils/types' import { SWRHook } from '@commerce/utils/types'
import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart' import useCart, { UseCart, FetchCartInput } from '@commerce/cart/use-cart'
import { normalizeCart } from '../lib/normalize' import { normalizeCart } from '../lib/normalize'
import type { Cart } from '../types' import type { Cart } from '../types'
import type { BigcommerceProvider } from '..'
export default useCart as UseCart<BigcommerceProvider> export default useCart as UseCart<typeof handler>
export const handler: HookHandler< export const handler: SWRHook<
Cart | null, Cart | null,
{}, {},
FetchCartInput, FetchCartInput,
@ -21,9 +20,9 @@ export const handler: HookHandler<
const data = cartId ? await fetch(options) : null const data = cartId ? await fetch(options) : null
return data && normalizeCart(data) return data && normalizeCart(data)
}, },
useHook({ input, useData }) { useHook: ({ useData }) => (input) => {
const response = useData({ const response = useData({
swrOptions: { revalidateOnFocus: false, ...input.swrOptions }, swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
}) })
return useMemo( return useMemo(

View File

@ -1,8 +1,12 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import { HookFetcher } from '@commerce/utils/types' import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors' import { ValidationError } from '@commerce/utils/errors'
import useCartRemoveItem, { import useRemoveItem, {
RemoveItemInput as UseRemoveItemInput, RemoveItemInput as RemoveItemInputBase,
UseRemoveItem,
} from '@commerce/cart/use-remove-item' } from '@commerce/cart/use-remove-item'
import { normalizeCart } from '../lib/normalize' import { normalizeCart } from '../lib/normalize'
import type { import type {
@ -13,41 +17,41 @@ import type {
} from '../types' } from '../types'
import useCart from './use-cart' import useCart from './use-cart'
const defaultOpts = {
url: '/api/bigcommerce/cart',
method: 'DELETE',
}
export type RemoveItemFn<T = any> = T extends LineItem export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemInput<T>) => Promise<Cart | null> ? (input?: RemoveItemInput<T>) => Promise<Cart | null>
: (input: RemoveItemInput<T>) => Promise<Cart | null> : (input: RemoveItemInput<T>) => Promise<Cart | null>
export type RemoveItemInput<T = any> = T extends LineItem export type RemoveItemInput<T = any> = T extends LineItem
? Partial<UseRemoveItemInput> ? Partial<RemoveItemInputBase>
: UseRemoveItemInput : RemoveItemInputBase
export const fetcher: HookFetcher<Cart | null, RemoveCartItemBody> = async ( export default useRemoveItem as UseRemoveItem<typeof handler>
options,
{ itemId },
fetch
) => {
const data = await fetch<BigcommerceCart>({
...defaultOpts,
...options,
body: { itemId },
})
return normalizeCart(data)
}
export function extendHook(customFetcher: typeof fetcher) { export const handler = {
const useRemoveItem = <T extends LineItem | undefined = undefined>( fetchOptions: {
item?: T url: '/api/bigcommerce/cart',
method: 'DELETE',
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveCartItemBody>) {
const data = await fetch<BigcommerceCart>({
...options,
body: { itemId },
})
return normalizeCart(data)
},
useHook: ({
fetch,
}: MutationHookContext<Cart | null, RemoveCartItemBody>) => <
T extends LineItem | undefined = undefined
>(
ctx: { item?: T } = {}
) => { ) => {
const { item } = ctx
const { mutate } = useCart() const { mutate } = useCart()
const fn = useCartRemoveItem<Cart | null, RemoveCartItemBody>(
defaultOpts,
customFetcher
)
const removeItem: RemoveItemFn<LineItem> = async (input) => { const removeItem: RemoveItemFn<LineItem> = async (input) => {
const itemId = input?.id ?? item?.id const itemId = input?.id ?? item?.id
@ -57,17 +61,11 @@ export function extendHook(customFetcher: typeof fetcher) {
}) })
} }
const data = await fn({ itemId }) const data = await fetch({ input: { itemId } })
await mutate(data, false) await mutate(data, false)
return data return data
} }
return useCallback(removeItem as RemoveItemFn<T>, [fn, mutate]) return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
} },
useRemoveItem.extend = extendHook
return useRemoveItem
} }
export default extendHook(fetcher)

View File

@ -1,9 +1,13 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import type { HookFetcher } from '@commerce/utils/types' import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors' import { ValidationError } from '@commerce/utils/errors'
import useCartUpdateItem, { import useUpdateItem, {
UpdateItemInput as UseUpdateItemInput, UpdateItemInput as UpdateItemInputBase,
UseUpdateItem,
} from '@commerce/cart/use-update-item' } from '@commerce/cart/use-update-item'
import { normalizeCart } from '../lib/normalize' import { normalizeCart } from '../lib/normalize'
import type { import type {
@ -12,52 +16,59 @@ import type {
BigcommerceCart, BigcommerceCart,
LineItem, LineItem,
} from '../types' } from '../types'
import { fetcher as removeFetcher } from './use-remove-item' import { handler as removeItemHandler } from './use-remove-item'
import useCart from './use-cart' import useCart from './use-cart'
const defaultOpts = {
url: '/api/bigcommerce/cart',
method: 'PUT',
}
export type UpdateItemInput<T = any> = T extends LineItem export type UpdateItemInput<T = any> = T extends LineItem
? Partial<UseUpdateItemInput<LineItem>> ? Partial<UpdateItemInputBase<LineItem>>
: UseUpdateItemInput<LineItem> : UpdateItemInputBase<LineItem>
export const fetcher: HookFetcher<Cart | null, UpdateCartItemBody> = async ( export default useUpdateItem as UseUpdateItem<typeof handler>
options,
{ itemId, item }, export const handler = {
fetch fetchOptions: {
) => { url: '/api/bigcommerce/cart',
if (Number.isInteger(item.quantity)) { method: 'PUT',
// Also allow the update hook to remove an item if the quantity is lower than 1 },
if (item.quantity! < 1) { async fetcher({
return removeFetcher(null, { itemId }, fetch) input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateCartItemBody>) {
if (Number.isInteger(item.quantity)) {
// Also allow the update hook to remove an item if the quantity is lower than 1
if (item.quantity! < 1) {
return removeItemHandler.fetcher({
options: removeItemHandler.fetchOptions,
input: { itemId },
fetch,
})
}
} else if (item.quantity) {
throw new ValidationError({
message: 'The item quantity has to be a valid integer',
})
} }
} else if (item.quantity) {
throw new ValidationError({ const data = await fetch<BigcommerceCart, UpdateCartItemBody>({
message: 'The item quantity has to be a valid integer', ...options,
body: { itemId, item },
}) })
}
const data = await fetch<BigcommerceCart, UpdateCartItemBody>({ return normalizeCart(data)
...defaultOpts, },
...options, useHook: ({
body: { itemId, item }, fetch,
}) }: MutationHookContext<Cart | null, UpdateCartItemBody>) => <
T extends LineItem | undefined = undefined
return normalizeCart(data) >(
} ctx: {
item?: T
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) { wait?: number
const useUpdateItem = <T extends LineItem | undefined = undefined>( } = {}
item?: T
) => { ) => {
const { mutate } = useCart() const { item } = ctx
const fn = useCartUpdateItem<Cart | null, UpdateCartItemBody>( const { mutate } = useCart() as any
defaultOpts,
customFetcher
)
return useCallback( return useCallback(
debounce(async (input: UpdateItemInput<T>) => { debounce(async (input: UpdateItemInput<T>) => {
@ -71,20 +82,16 @@ function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
}) })
} }
const data = await fn({ const data = await fetch({
itemId, input: {
item: { productId, variantId, quantity: input.quantity }, itemId,
item: { productId, variantId, quantity: input.quantity },
},
}) })
await mutate(data, false) await mutate(data, false)
return data return data
}, cfg?.wait ?? 500), }, ctx.wait ?? 500),
[fn, mutate] [fetch, mutate]
) )
} },
useUpdateItem.extend = extendHook
return useUpdateItem
} }
export default extendHook(fetcher)

View File

@ -0,0 +1,5 @@
{
"features": {
"wishlist": false
}
}

View File

@ -1,11 +1,10 @@
import { HookHandler } from '@commerce/utils/types' import { SWRHook } from '@commerce/utils/types'
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer' import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
import type { Customer, CustomerData } from '../api/customers' import type { Customer, CustomerData } from '../api/customers'
import type { BigcommerceProvider } from '..'
export default useCustomer as UseCustomer<BigcommerceProvider> export default useCustomer as UseCustomer<typeof handler>
export const handler: HookHandler<Customer | null> = { export const handler: SWRHook<Customer | null> = {
fetchOptions: { fetchOptions: {
url: '/api/bigcommerce/customers', url: '/api/bigcommerce/customers',
method: 'GET', method: 'GET',
@ -14,11 +13,11 @@ export const handler: HookHandler<Customer | null> = {
const data = await fetch<CustomerData | null>(options) const data = await fetch<CustomerData | null>(options)
return data?.customer ?? null return data?.customer ?? null
}, },
useHook({ input, useData }) { useHook: ({ useData }) => (input) => {
return useData({ return useData({
swrOptions: { swrOptions: {
revalidateOnFocus: false, revalidateOnFocus: false,
...input.swrOptions, ...input?.swrOptions,
}, },
}) })
}, },

View File

@ -1,9 +1,8 @@
import { HookHandler } from '@commerce/utils/types' import { SWRHook } from '@commerce/utils/types'
import useSearch, { UseSearch } from '@commerce/products/use-search' import useSearch, { UseSearch } from '@commerce/product/use-search'
import type { SearchProductsData } from '../api/catalog/products' import type { SearchProductsData } from '../api/catalog/products'
import type { BigcommerceProvider } from '..'
export default useSearch as UseSearch<BigcommerceProvider> export default useSearch as UseSearch<typeof handler>
export type SearchProductsInput = { export type SearchProductsInput = {
search?: string search?: string
@ -12,7 +11,7 @@ export type SearchProductsInput = {
sort?: string sort?: string
} }
export const handler: HookHandler< export const handler: SWRHook<
SearchProductsData, SearchProductsData,
SearchProductsInput, SearchProductsInput,
SearchProductsInput SearchProductsInput
@ -37,7 +36,7 @@ export const handler: HookHandler<
method: options.method, method: options.method,
}) })
}, },
useHook({ input, useData }) { useHook: ({ useData }) => (input = {}) => {
return useData({ return useData({
input: [ input: [
['search', input.search], ['search', input.search],

View File

@ -1,18 +1,34 @@
import { handler as useCart } from './cart/use-cart' import { handler as useCart } from './cart/use-cart'
import { handler as useAddItem } from './cart/use-add-item' import { handler as useAddItem } from './cart/use-add-item'
import { handler as useUpdateItem } from './cart/use-update-item'
import { handler as useRemoveItem } from './cart/use-remove-item'
import { handler as useWishlist } from './wishlist/use-wishlist' import { handler as useWishlist } from './wishlist/use-wishlist'
import { handler as useWishlistAddItem } from './wishlist/use-add-item'
import { handler as useWishlistRemoveItem } from './wishlist/use-remove-item'
import { handler as useCustomer } from './customer/use-customer' import { handler as useCustomer } from './customer/use-customer'
import { handler as useSearch } from './product/use-search' import { handler as useSearch } from './product/use-search'
import { handler as useLogin } from './auth/use-login'
import { handler as useLogout } from './auth/use-logout'
import { handler as useSignup } from './auth/use-signup'
import fetcher from './fetcher' import fetcher from './fetcher'
export const bigcommerceProvider = { export const bigcommerceProvider = {
locale: 'en-us', locale: 'en-us',
cartCookie: 'bc_cartId', cartCookie: 'bc_cartId',
fetcher, fetcher,
cart: { useCart, useAddItem }, cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
wishlist: { useWishlist }, wishlist: {
useWishlist,
useAddItem: useWishlistAddItem,
useRemoveItem: useWishlistRemoveItem,
},
customer: { useCustomer }, customer: { useCustomer },
products: { useSearch }, products: { useSearch },
auth: { useLogin, useLogout, useSignup },
} }
export type BigcommerceProvider = typeof bigcommerceProvider export type BigcommerceProvider = typeof bigcommerceProvider

View File

@ -43,9 +43,6 @@ export type CartItemBody = Core.CartItemBody & {
optionSelections?: OptionSelections optionSelections?: OptionSelections
} }
type X = Core.CartItemBody extends CartItemBody ? any : never
type Y = CartItemBody extends Core.CartItemBody ? any : never
export type GetCartHandlerBody = Core.GetCartHandlerBody export type GetCartHandlerBody = Core.GetCartHandlerBody
export type AddCartItemBody = Core.AddCartItemBody<CartItemBody> export type AddCartItemBody = Core.AddCartItemBody<CartItemBody>

View File

@ -1,43 +1,24 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import { HookFetcher } from '@commerce/utils/types' import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors' import { CommerceError } from '@commerce/utils/errors'
import useWishlistAddItem, { import useAddItem, { UseAddItem } from '@commerce/wishlist/use-add-item'
AddItemInput,
} from '@commerce/wishlist/use-add-item'
import { UseWishlistInput } from '@commerce/wishlist/use-wishlist'
import type { ItemBody, AddItemBody } from '../api/wishlist' import type { ItemBody, AddItemBody } from '../api/wishlist'
import useCustomer from '../customer/use-customer' import useCustomer from '../customer/use-customer'
import useWishlist from './use-wishlist' import useWishlist from './use-wishlist'
import type { BigcommerceProvider } from '..'
const defaultOpts = { export default useAddItem as UseAddItem<typeof handler>
url: '/api/bigcommerce/wishlist',
method: 'POST',
}
// export type AddItemInput = ItemBody export const handler: MutationHook<any, {}, ItemBody, AddItemBody> = {
fetchOptions: {
export const fetcher: HookFetcher<any, AddItemBody> = ( url: '/api/bigcommerce/wishlist',
options, method: 'POST',
{ item }, },
fetch useHook: ({ fetch }) => () => {
) => {
// TODO: add validations before doing the fetch
return fetch({
...defaultOpts,
...options,
body: { item },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useAddItem = (opts?: UseWishlistInput<BigcommerceProvider>) => {
const { data: customer } = useCustomer() const { data: customer } = useCustomer()
const { revalidate } = useWishlist(opts) const { revalidate } = useWishlist()
const fn = useWishlistAddItem(defaultOpts, customFetcher)
return useCallback( return useCallback(
async function addItem(input: AddItemInput<any>) { async function addItem(item) {
if (!customer) { if (!customer) {
// A signed customer is required in order to have a wishlist // A signed customer is required in order to have a wishlist
throw new CommerceError({ throw new CommerceError({
@ -45,17 +26,12 @@ export function extendHook(customFetcher: typeof fetcher) {
}) })
} }
const data = await fn({ item: input }) // TODO: add validations before doing the fetch
const data = await fetch({ input: { item } })
await revalidate() await revalidate()
return data return data
}, },
[fn, revalidate, customer] [fetch, revalidate, customer]
) )
} },
useAddItem.extend = extendHook
return useAddItem
} }
export default extendHook(fetcher)

View File

@ -1,43 +1,32 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import { HookFetcher } from '@commerce/utils/types' import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors' import { CommerceError } from '@commerce/utils/errors'
import useWishlistRemoveItem from '@commerce/wishlist/use-remove-item' import useRemoveItem, {
import type { RemoveItemBody } from '../api/wishlist' RemoveItemInput,
UseRemoveItem,
} from '@commerce/wishlist/use-remove-item'
import type { RemoveItemBody, Wishlist } from '../api/wishlist'
import useCustomer from '../customer/use-customer' import useCustomer from '../customer/use-customer'
import useWishlist from './use-wishlist' import useWishlist, { UseWishlistInput } from './use-wishlist'
const defaultOpts = { export default useRemoveItem as UseRemoveItem<typeof handler>
url: '/api/bigcommerce/wishlist',
method: 'DELETE',
}
export type RemoveItemInput = { export const handler: MutationHook<
id: string | number Wishlist | null,
} { wishlist?: UseWishlistInput },
RemoveItemInput,
export const fetcher: HookFetcher<any | null, RemoveItemBody> = ( RemoveItemBody
options, > = {
{ itemId }, fetchOptions: {
fetch url: '/api/bigcommerce/wishlist',
) => { method: 'DELETE',
return fetch({ },
...defaultOpts, useHook: ({ fetch }) => ({ wishlist } = {}) => {
...options,
body: { itemId },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useRemoveItem = (opts?: any) => {
const { data: customer } = useCustomer() const { data: customer } = useCustomer()
const { revalidate } = useWishlist(opts) const { revalidate } = useWishlist(wishlist)
const fn = useWishlistRemoveItem<any | null, RemoveItemBody>(
defaultOpts,
customFetcher
)
return useCallback( return useCallback(
async function removeItem(input: RemoveItemInput) { async function removeItem(input) {
if (!customer) { if (!customer) {
// A signed customer is required in order to have a wishlist // A signed customer is required in order to have a wishlist
throw new CommerceError({ throw new CommerceError({
@ -45,17 +34,11 @@ export function extendHook(customFetcher: typeof fetcher) {
}) })
} }
const data = await fn({ itemId: String(input.id) }) const data = await fetch({ input: { itemId: String(input.id) } })
await revalidate() await revalidate()
return data return data
}, },
[fn, revalidate, customer] [fetch, revalidate, customer]
) )
} },
useRemoveItem.extend = extendHook
return useRemoveItem
} }
export default extendHook(fetcher)

View File

@ -1,16 +1,17 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import { HookHandler } from '@commerce/utils/types' import { SWRHook } from '@commerce/utils/types'
import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist' import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
import type { Wishlist } from '../api/wishlist' import type { Wishlist } from '../api/wishlist'
import useCustomer from '../customer/use-customer' import useCustomer from '../customer/use-customer'
import type { BigcommerceProvider } from '..'
export default useWishlist as UseWishlist<BigcommerceProvider> export type UseWishlistInput = { includeProducts?: boolean }
export const handler: HookHandler< export default useWishlist as UseWishlist<typeof handler>
export const handler: SWRHook<
Wishlist | null, Wishlist | null,
{ includeProducts?: boolean }, UseWishlistInput,
{ customerId?: number; includeProducts: boolean }, { customerId?: number } & UseWishlistInput,
{ isEmpty?: boolean } { isEmpty?: boolean }
> = { > = {
fetchOptions: { fetchOptions: {
@ -30,16 +31,16 @@ export const handler: HookHandler<
method: options.method, method: options.method,
}) })
}, },
useHook({ input, useData }) { useHook: ({ useData }) => (input) => {
const { data: customer } = useCustomer() const { data: customer } = useCustomer()
const response = useData({ const response = useData({
input: [ input: [
['customerId', (customer as any)?.id], ['customerId', (customer as any)?.id],
['includeProducts', input.includeProducts], ['includeProducts', input?.includeProducts],
], ],
swrOptions: { swrOptions: {
revalidateOnFocus: false, revalidateOnFocus: false,
...input.swrOptions, ...input?.swrOptions,
}, },
}) })

View File

@ -1,69 +1,23 @@
import { useCallback } from 'react' import { useHook, useMutationHook } from '../utils/use-hook'
import type { import { mutationFetcher } from '../utils/default-fetcher'
Prop, import type { HookFetcherFn, MutationHook } from '../utils/types'
HookFetcherFn,
UseHookInput,
UseHookResponse,
} from '../utils/types'
import type { Cart, CartItemBody, AddCartItemBody } from '../types' import type { Cart, CartItemBody, AddCartItemBody } from '../types'
import { Provider, useCommerce } from '..' import type { Provider } from '..'
import { BigcommerceProvider } from '@framework'
export type UseAddItemHandler<P extends Provider> = Prop< export type UseAddItem<
Prop<P, 'cart'>, H extends MutationHook<any, any, any> = MutationHook<Cart, {}, CartItemBody>
'useAddItem' > = ReturnType<H['useHook']>
>
// Input expected by the action returned by the `useAddItem` hook
export type UseAddItemInput<P extends Provider> = UseHookInput<
UseAddItemHandler<P>
>
export type UseAddItemResult<P extends Provider> = ReturnType<
UseHookResponse<UseAddItemHandler<P>>
>
export type UseAddItem<P extends Provider, Input> = Partial<
UseAddItemInput<P>
> extends UseAddItemInput<P>
? (input?: UseAddItemInput<P>) => (input: Input) => UseAddItemResult<P>
: (input: UseAddItemInput<P>) => (input: Input) => UseAddItemResult<P>
export const fetcher: HookFetcherFn< export const fetcher: HookFetcherFn<
Cart, Cart,
AddCartItemBody<CartItemBody> AddCartItemBody<CartItemBody>
> = async ({ options, input, fetch }) => { > = mutationFetcher
return fetch({ ...options, body: input })
const fn = (provider: Provider) => provider.cart?.useAddItem!
const useAddItem: UseAddItem = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
} }
type X = UseAddItemResult<BigcommerceProvider> export default useAddItem
export default function useAddItem<P extends Provider, Input>(
input: UseAddItemInput<P>
) {
const { providerRef, fetcherRef } = useCommerce<P>()
const provider = providerRef.current
const opts = provider.cart?.useAddItem
const fetcherFn = opts?.fetcher ?? fetcher
const useHook = opts?.useHook ?? (() => () => {})
const fetchFn = provider.fetcher ?? fetcherRef.current
const action = useHook({ input })
return useCallback(
function addItem(input: Input) {
return action({
input,
fetch({ input }) {
return fetcherFn({
input,
options: opts!.fetchOptions,
fetch: fetchFn,
})
},
})
},
[input, fetchFn, opts?.fetchOptions]
)
}

View File

@ -1,17 +0,0 @@
import type { HookFetcher, HookFetcherOptions } from '../utils/types'
import useAddItem from './use-add-item'
import useRemoveItem from './use-remove-item'
import useUpdateItem from './use-update-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 useCartActions<T, Input>(
options: HookFetcherOptions,
fetcher: HookFetcher<T, Input>
) {
const addItem = useAddItem<T, Input>(options, fetcher)
const updateItem = useUpdateItem<T, Input>(options, fetcher)
const removeItem = useRemoveItem<T, Input>(options, fetcher)
return { addItem, updateItem, removeItem }
}

View File

@ -1,34 +1,21 @@
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import { useHook, useSWRHook } from '../utils/use-hook'
import type { HookFetcherFn, SWRHook } from '../utils/types'
import type { Cart } from '../types' import type { Cart } from '../types'
import type {
Prop,
HookFetcherFn,
UseHookInput,
UseHookResponse,
} from '../utils/types'
import useData from '../utils/use-data'
import { Provider, useCommerce } from '..' import { Provider, useCommerce } from '..'
export type FetchCartInput = { export type FetchCartInput = {
cartId?: Cart['id'] cartId?: Cart['id']
} }
export type UseCartHandler<P extends Provider> = Prop< export type UseCart<
Prop<P, 'cart'>, H extends SWRHook<any, any, any> = SWRHook<
'useCart' Cart | null,
> {},
FetchCartInput,
export type UseCartInput<P extends Provider> = UseHookInput<UseCartHandler<P>> { isEmpty?: boolean }
>
export type CartResponse<P extends Provider> = UseHookResponse< > = ReturnType<H['useHook']>
UseCartHandler<P>
>
export type UseCart<P extends Provider> = Partial<
UseCartInput<P>
> extends UseCartInput<P>
? (input?: UseCartInput<P>) => CartResponse<P>
: (input: UseCartInput<P>) => CartResponse<P>
export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({ export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
options, options,
@ -38,32 +25,17 @@ export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
return cartId ? await fetch({ ...options }) : null return cartId ? await fetch({ ...options }) : null
} }
export default function useCart<P extends Provider>( const fn = (provider: Provider) => provider.cart?.useCart!
input: UseCartInput<P> = {}
) {
const { providerRef, fetcherRef, cartCookie } = useCommerce<P>()
const provider = providerRef.current
const opts = provider.cart?.useCart
const fetcherFn = opts?.fetcher ?? fetcher
const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
const useCart: UseCart = (input) => {
const hook = useHook(fn)
const { cartCookie } = useCommerce()
const fetcherFn = hook.fetcher ?? fetcher
const wrapper: typeof fetcher = (context) => { const wrapper: typeof fetcher = (context) => {
context.input.cartId = Cookies.get(cartCookie) context.input.cartId = Cookies.get(cartCookie)
return fetcherFn(context) return fetcherFn(context)
} }
return useSWRHook({ ...hook, fetcher: wrapper })(input)
return useHook({
input,
useData(ctx) {
const response = useData(
{ ...opts!, fetcher: wrapper },
ctx?.input ?? [],
provider.fetcher ?? fetcherRef.current,
ctx?.swrOptions ?? input.swrOptions
)
return response
},
})
} }
export default useCart

View File

@ -1,10 +1,35 @@
import useAction from '../utils/use-action' import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Cart, LineItem, RemoveCartItemBody } from '../types'
import type { Provider } from '..'
// Input expected by the action returned by the `useRemoveItem` hook /**
export interface RemoveItemInput { * Input expected by the action returned by the `useRemoveItem` hook
*/
export type RemoveItemInput = {
id: string id: string
} }
const useRemoveItem = useAction export type UseRemoveItem<
H extends MutationHook<any, any, any> = MutationHook<
Cart | null,
{ item?: LineItem },
RemoveItemInput,
RemoveCartItemBody
>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<
Cart | null,
RemoveCartItemBody
> = mutationFetcher
const fn = (provider: Provider) => provider.cart?.useRemoveItem!
const useRemoveItem: UseRemoveItem = (input) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(input)
}
export default useRemoveItem export default useRemoveItem

View File

@ -1,11 +1,38 @@
import useAction from '../utils/use-action' import { useHook, useMutationHook } from '../utils/use-hook'
import type { CartItemBody } from '../types' import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Cart, CartItemBody, LineItem, UpdateCartItemBody } from '../types'
import type { Provider } from '..'
// Input expected by the action returned by the `useUpdateItem` hook /**
* Input expected by the action returned by the `useUpdateItem` hook
*/
export type UpdateItemInput<T extends CartItemBody> = T & { export type UpdateItemInput<T extends CartItemBody> = T & {
id: string id: string
} }
const useUpdateItem = useAction export type UseUpdateItem<
H extends MutationHook<any, any, any> = MutationHook<
Cart | null,
{
item?: LineItem
wait?: number
},
UpdateItemInput<CartItemBody>,
UpdateCartItemBody<CartItemBody>
>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<
Cart | null,
UpdateCartItemBody<CartItemBody>
> = mutationFetcher
const fn = (provider: Provider) => provider.cart?.useUpdateItem!
const useUpdateItem: UseUpdateItem = (input) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(input)
}
export default useUpdateItem export default useUpdateItem

View File

@ -0,0 +1,5 @@
{
"features": {
"wishlist": true
}
}

View File

@ -1,56 +1,20 @@
import { useHook, useSWRHook } from '../utils/use-hook'
import { SWRFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, SWRHook } from '../utils/types'
import type { Customer } from '../types' import type { Customer } from '../types'
import type { import { Provider } from '..'
Prop,
HookFetcherFn,
UseHookInput,
UseHookResponse,
} from '../utils/types'
import defaultFetcher from '../utils/default-fetcher'
import useData from '../utils/use-data'
import { Provider, useCommerce } from '..'
export type UseCustomerHandler<P extends Provider> = Prop< export type UseCustomer<
Prop<P, 'customer'>, H extends SWRHook<any, any, any> = SWRHook<Customer | null>
'useCustomer' > = ReturnType<H['useHook']>
>
export type UseCustomerInput<P extends Provider> = UseHookInput< export const fetcher: HookFetcherFn<Customer | null, any> = SWRFetcher
UseCustomerHandler<P>
>
export type CustomerResponse<P extends Provider> = UseHookResponse< const fn = (provider: Provider) => provider.customer?.useCustomer!
UseCustomerHandler<P>
>
export type UseCustomer<P extends Provider> = Partial< const useCustomer: UseCustomer = (input) => {
UseCustomerInput<P> const hook = useHook(fn)
> extends UseCustomerInput<P> return useSWRHook({ fetcher, ...hook })(input)
? (input?: UseCustomerInput<P>) => CustomerResponse<P>
: (input: UseCustomerInput<P>) => CustomerResponse<P>
export const fetcher = defaultFetcher as HookFetcherFn<Customer | null>
export default function useCustomer<P extends Provider>(
input: UseCustomerInput<P> = {}
) {
const { providerRef, fetcherRef } = useCommerce<P>()
const provider = providerRef.current
const opts = provider.customer?.useCustomer
const fetcherFn = opts?.fetcher ?? fetcher
const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
return useHook({
input,
useData(ctx) {
const response = useData(
{ ...opts!, fetcher: fetcherFn },
ctx?.input ?? [],
provider.fetcher ?? fetcherRef.current,
ctx?.swrOptions ?? input.swrOptions
)
return response
},
})
} }
export default useCustomer

View File

@ -6,7 +6,7 @@ import {
useMemo, useMemo,
useRef, useRef,
} from 'react' } from 'react'
import { Fetcher, HookHandler, MutationHandler } from './utils/types' import { Fetcher, SWRHook, MutationHook } from './utils/types'
import type { FetchCartInput } from './cart/use-cart' import type { FetchCartInput } from './cart/use-cart'
import type { Cart, Wishlist, Customer, SearchProductsData } from './types' import type { Cart, Wishlist, Customer, SearchProductsData } from './types'
@ -15,17 +15,26 @@ const Commerce = createContext<CommerceContextValue<any> | {}>({})
export type Provider = CommerceConfig & { export type Provider = CommerceConfig & {
fetcher: Fetcher fetcher: Fetcher
cart?: { cart?: {
useCart?: HookHandler<Cart | null, any, FetchCartInput> useCart?: SWRHook<Cart | null, any, FetchCartInput>
useAddItem?: MutationHandler<Cart, any, any> useAddItem?: MutationHook<any, any, any>
useUpdateItem?: MutationHook<any, any, any>
useRemoveItem?: MutationHook<any, any, any>
} }
wishlist?: { wishlist?: {
useWishlist?: HookHandler<Wishlist | null, any, any> useWishlist?: SWRHook<Wishlist | null, any, any>
useAddItem?: MutationHook<any, any, any>
useRemoveItem?: MutationHook<any, any, any>
} }
customer: { customer?: {
useCustomer?: HookHandler<Customer | null, any, any> useCustomer?: SWRHook<Customer | null, any, any>
} }
products: { products?: {
useSearch?: HookHandler<SearchProductsData, any, any> useSearch?: SWRHook<SearchProductsData, any, any>
}
auth?: {
useSignup?: MutationHook<any, any, any>
useLogin?: MutationHook<any, any, any>
useLogout?: MutationHook<any, any, any>
} }
} }

View File

@ -0,0 +1,20 @@
import { useHook, useSWRHook } from '../utils/use-hook'
import { SWRFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, SWRHook } from '../utils/types'
import type { SearchProductsData } from '../types'
import { Provider } from '..'
export type UseSearch<
H extends SWRHook<any, any, any> = SWRHook<SearchProductsData>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<SearchProductsData, any> = SWRFetcher
const fn = (provider: Provider) => provider.products?.useSearch!
const useSearch: UseSearch = (input) => {
const hook = useHook(fn)
return useSWRHook({ fetcher, ...hook })(input)
}
export default useSearch

View File

@ -1,57 +0,0 @@
import type { SearchProductsData } from '../types'
import type {
Prop,
HookFetcherFn,
UseHookInput,
UseHookResponse,
} from '../utils/types'
import defaultFetcher from '../utils/default-fetcher'
import useData from '../utils/use-data'
import { Provider, useCommerce } from '..'
import { BigcommerceProvider } from '@framework'
export type UseSearchHandler<P extends Provider> = Prop<
Prop<P, 'products'>,
'useSearch'
>
export type UseSeachInput<P extends Provider> = UseHookInput<
UseSearchHandler<P>
>
export type SearchResponse<P extends Provider> = UseHookResponse<
UseSearchHandler<P>
>
export type UseSearch<P extends Provider> = Partial<
UseSeachInput<P>
> extends UseSeachInput<P>
? (input?: UseSeachInput<P>) => SearchResponse<P>
: (input: UseSeachInput<P>) => SearchResponse<P>
export const fetcher = defaultFetcher as HookFetcherFn<SearchProductsData>
export default function useSearch<P extends Provider>(
input: UseSeachInput<P> = {}
) {
const { providerRef, fetcherRef } = useCommerce<P>()
const provider = providerRef.current
const opts = provider.products?.useSearch
const fetcherFn = opts?.fetcher ?? fetcher
const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
return useHook({
input,
useData(ctx) {
const response = useData(
{ ...opts!, fetcher: fetcherFn },
ctx?.input ?? [],
provider.fetcher ?? fetcherRef.current,
ctx?.swrOptions ?? input.swrOptions
)
return response
},
})
}

View File

@ -2,6 +2,10 @@ import type { Wishlist as BCWishlist } from '@framework/api/wishlist'
import type { Customer as BCCustomer } from '@framework/api/customers' import type { Customer as BCCustomer } from '@framework/api/customers'
import type { SearchProductsData as BCSearchProductsData } from '@framework/api/catalog/products' import type { SearchProductsData as BCSearchProductsData } from '@framework/api/catalog/products'
export type CommerceProviderConfig = {
features: Record<string, boolean>
}
export type Discount = { export type Discount = {
// The value of the discount, can be an amount or percentage // The value of the discount, can be an amount or percentage
value: number value: number

View File

@ -1,5 +1,19 @@
import useAction from './utils/use-action' import { useHook, useMutationHook } from './utils/use-hook'
import { mutationFetcher } from './utils/default-fetcher'
import type { MutationHook, HookFetcherFn } from './utils/types'
import type { Provider } from '.'
const useLogin = useAction export type UseLogin<
H extends MutationHook<any, any, any> = MutationHook<null, {}, {}>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<null, {}> = mutationFetcher
const fn = (provider: Provider) => provider.auth?.useLogin!
const useLogin: UseLogin = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useLogin export default useLogin

View File

@ -1,5 +1,19 @@
import useAction from './utils/use-action' import { useHook, useMutationHook } from './utils/use-hook'
import { mutationFetcher } from './utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from './utils/types'
import type { Provider } from '.'
const useLogout = useAction export type UseLogout<
H extends MutationHook<any, any, any> = MutationHook<null>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<null> = mutationFetcher
const fn = (provider: Provider) => provider.auth?.useLogout!
const useLogout: UseLogout = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useLogout export default useLogout

View File

@ -1,5 +1,19 @@
import useAction from './utils/use-action' import { useHook, useMutationHook } from './utils/use-hook'
import { mutationFetcher } from './utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from './utils/types'
import type { Provider } from '.'
const useSignup = useAction export type UseSignup<
H extends MutationHook<any, any, any> = MutationHook<null>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<null> = mutationFetcher
const fn = (provider: Provider) => provider.auth?.useSignup!
const useSignup: UseSignup = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useSignup export default useSignup

View File

@ -1,6 +1,12 @@
import type { HookFetcherFn } from './types' import type { HookFetcherFn } from './types'
const defaultFetcher: HookFetcherFn<any> = ({ options, fetch }) => export const SWRFetcher: HookFetcherFn<any, any> = ({ options, fetch }) =>
fetch(options) fetch(options)
export default defaultFetcher export const mutationFetcher: HookFetcherFn<any, any> = ({
input,
options,
fetch,
}) => fetch({ ...options, body: input })
export default SWRFetcher

View File

@ -0,0 +1,37 @@
import commerceProviderConfig from '@framework/config.json'
import type { CommerceProviderConfig } from '../types'
import memo from 'lodash.memoize'
type FeaturesAPI = {
isEnabled: (desideredFeature: string) => boolean
}
function isFeatureEnabled(config: CommerceProviderConfig) {
const features = config.features
return (desideredFeature: string) =>
Object.keys(features)
.filter((k) => features[k])
.includes(desideredFeature)
}
function boostrap(): FeaturesAPI {
const basis = {
isEnabled: () => false,
}
if (!commerceProviderConfig) {
console.log('No config.json found - Please add a config.json')
return basis
}
if (commerceProviderConfig.features) {
return {
...basis,
isEnabled: memo(isFeatureEnabled(commerceProviderConfig)),
}
}
return basis
}
export default boostrap()

View File

@ -2,13 +2,18 @@ import type { ConfigInterface } from 'swr'
import type { CommerceError } from './errors' import type { CommerceError } from './errors'
import type { ResponseState } from './use-data' import type { ResponseState } from './use-data'
/**
* Returns the properties in T with the properties in type K, overriding properties defined in T
*/
export type Override<T, K> = Omit<T, keyof K> & K export type Override<T, K> = Omit<T, keyof K> & K
/** /**
* Returns the properties in T with the properties in type K changed from optional to required * Returns the properties in T with the properties in type K changed from optional to required
*/ */
export type PickRequired<T, K extends keyof T> = Omit<T, K> & export type PickRequired<T, K extends keyof T> = Omit<T, K> &
Required<Pick<T, K>> {
[P in K]-?: NonNullable<T[P]>
}
/** /**
* Core fetcher added by CommerceProvider * Core fetcher added by CommerceProvider
@ -31,16 +36,15 @@ export type HookFetcher<Data, Input = null, Result = any> = (
fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T> fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
) => Data | Promise<Data> ) => Data | Promise<Data>
export type HookFetcherFn< export type HookFetcherFn<Data, Input = undefined, Result = any, Body = any> = (
Data, context: HookFetcherContext<Input, Result, Body>
Input = never, ) => Data | Promise<Data>
Result = any,
Body = any export type HookFetcherContext<Input = undefined, Result = any, Body = any> = {
> = (context: {
options: HookFetcherOptions options: HookFetcherOptions
input: Input input: Input
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T> fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
}) => Data | Promise<Data> }
export type HookFetcherOptions = { method?: string } & ( export type HookFetcherOptions = { method?: string } & (
| { query: string; url?: string } | { query: string; url?: string }
@ -49,13 +53,20 @@ export type HookFetcherOptions = { method?: string } & (
export type HookInputValue = string | number | boolean | undefined export type HookInputValue = string | number | boolean | undefined
export type HookSwrInput = [string, HookInputValue][] export type HookSWRInput = [string, HookInputValue][]
export type HookFetchInput = { [k: string]: HookInputValue } export type HookFetchInput = { [k: string]: HookInputValue }
export type HookInput = {} export type HookFunction<
Input extends { [k: string]: unknown } | null,
T
> = keyof Input extends never
? () => T
: Partial<Input> extends Input
? (input?: Input) => T
: (input: Input) => T
export type HookHandler< export type SWRHook<
// Data obj returned by the hook and fetch operation // Data obj returned by the hook and fetch operation
Data, Data,
// Input expected by the hook // Input expected by the hook
@ -65,58 +76,56 @@ export type HookHandler<
// Custom state added to the response object of SWR // Custom state added to the response object of SWR
State = {} State = {}
> = { > = {
useHook?(context: { useHook(
input: Input & { swrOptions?: SwrOptions<Data, FetchInput> } context: SWRHookContext<Data, FetchInput>
useData(context?: { ): HookFunction<
input?: HookFetchInput | HookSwrInput Input & { swrOptions?: SwrOptions<Data, FetchInput> },
swrOptions?: SwrOptions<Data, FetchInput> ResponseState<Data> & State
}): ResponseState<Data> >
}): ResponseState<Data> & State
fetchOptions: HookFetcherOptions fetchOptions: HookFetcherOptions
fetcher?: HookFetcherFn<Data, FetchInput> fetcher?: HookFetcherFn<Data, FetchInput>
} }
export type MutationHandler< export type SWRHookContext<
Data,
FetchInput extends { [k: string]: unknown } = {}
> = {
useData(context?: {
input?: HookFetchInput | HookSWRInput
swrOptions?: SwrOptions<Data, FetchInput>
}): ResponseState<Data>
}
export type MutationHook<
// Data obj returned by the hook and fetch operation // Data obj returned by the hook and fetch operation
Data, Data,
// Input expected by the hook // Input expected by the hook
Input extends { [k: string]: unknown } = {}, Input extends { [k: string]: unknown } = {},
// Input expected by the action returned by the hook
ActionInput extends { [k: string]: unknown } = {},
// Input expected before doing a fetch operation // Input expected before doing a fetch operation
FetchInput extends { [k: string]: unknown } = {} FetchInput extends { [k: string]: unknown } = ActionInput
> = { > = {
useHook?(context: { useHook(
input: Input context: MutationHookContext<Data, FetchInput>
}): (context: { ): HookFunction<Input, HookFunction<ActionInput, Data | Promise<Data>>>
input: FetchInput
fetch: (context: { input: FetchInput }) => Data | Promise<Data>
}) => Data | Promise<Data>
fetchOptions: HookFetcherOptions fetchOptions: HookFetcherOptions
fetcher?: HookFetcherFn<Data, FetchInput> fetcher?: HookFetcherFn<Data, FetchInput>
} }
export type MutationHookContext<
Data,
FetchInput extends { [k: string]: unknown } | null = {}
> = {
fetch: keyof FetchInput extends never
? () => Data | Promise<Data>
: Partial<FetchInput> extends FetchInput
? (context?: { input?: FetchInput }) => Data | Promise<Data>
: (context: { input: FetchInput }) => Data | Promise<Data>
}
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface< export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
Data, Data,
CommerceError, CommerceError,
HookFetcher<Data, Input, Result> HookFetcher<Data, Input, Result>
> >
/**
* Returns the property K from type T excluding nullables
*/
export type Prop<T, K extends keyof T> = NonNullable<T[K]>
export type HookHandlerType =
| HookHandler<any, any, any>
| MutationHandler<any, any, any>
export type UseHookParameters<H extends HookHandlerType> = Parameters<
Prop<H, 'useHook'>
>
export type UseHookResponse<H extends HookHandlerType> = ReturnType<
Prop<H, 'useHook'>
>
export type UseHookInput<
H extends HookHandlerType
> = UseHookParameters<H>[0]['input']

View File

@ -1,15 +0,0 @@
import { useCallback } from 'react'
import type { HookFetcher, HookFetcherOptions } from './types'
import { useCommerce } from '..'
export default function useAction<T, Input = null>(
options: HookFetcherOptions,
fetcher: HookFetcher<T, Input>
) {
const { fetcherRef } = useCommerce()
return useCallback(
(input: Input) => fetcher(options, input, fetcherRef.current),
[fetcher]
)
}

View File

@ -1,11 +1,11 @@
import useSWR, { responseInterface } from 'swr' import useSWR, { responseInterface } from 'swr'
import type { import type {
HookHandler, HookSWRInput,
HookSwrInput,
HookFetchInput, HookFetchInput,
PickRequired,
Fetcher, Fetcher,
SwrOptions, SwrOptions,
HookFetcherOptions,
HookFetcherFn,
} from './types' } from './types'
import defineProperty from './define-property' import defineProperty from './define-property'
import { CommerceError } from './errors' import { CommerceError } from './errors'
@ -14,13 +14,12 @@ export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
isLoading: boolean isLoading: boolean
} }
export type UseData = < export type UseData = <Data = any, FetchInput extends HookFetchInput = {}>(
Data = any, options: {
Input extends { [k: string]: unknown } = {}, fetchOptions: HookFetcherOptions
FetchInput extends HookFetchInput = {} fetcher: HookFetcherFn<Data, FetchInput>
>( },
options: PickRequired<HookHandler<Data, Input, FetchInput>, 'fetcher'>, input: HookFetchInput | HookSWRInput,
input: HookFetchInput | HookSwrInput,
fetcherFn: Fetcher, fetcherFn: Fetcher,
swrOptions?: SwrOptions<Data, FetchInput> swrOptions?: SwrOptions<Data, FetchInput>
) => ResponseState<Data> ) => ResponseState<Data>

View File

@ -0,0 +1,50 @@
import { useCallback } from 'react'
import { Provider, useCommerce } from '..'
import type { MutationHook, PickRequired, SWRHook } from './types'
import useData from './use-data'
export function useFetcher() {
const { providerRef, fetcherRef } = useCommerce()
return providerRef.current.fetcher ?? fetcherRef.current
}
export function useHook<
P extends Provider,
H extends MutationHook<any, any, any> | SWRHook<any, any, any>
>(fn: (provider: P) => H) {
const { providerRef } = useCommerce<P>()
const provider = providerRef.current
return fn(provider)
}
export function useSWRHook<H extends SWRHook<any, any, any>>(
hook: PickRequired<H, 'fetcher'>
) {
const fetcher = useFetcher()
return hook.useHook({
useData(ctx) {
const response = useData(hook, ctx?.input ?? [], fetcher, ctx?.swrOptions)
return response
},
})
}
export function useMutationHook<H extends MutationHook<any, any, any>>(
hook: PickRequired<H, 'fetcher'>
) {
const fetcher = useFetcher()
return hook.useHook({
fetch: useCallback(
({ input } = {}) => {
return hook.fetcher({
input,
options: hook.fetchOptions,
fetch: fetcher,
})
},
[fetcher, hook.fetchOptions]
),
})
}

View File

@ -1,40 +0,0 @@
import { useMemo } from 'react'
import { responseInterface } from 'swr'
import { CommerceError } from './errors'
import { Override } from './types'
export type UseResponseOptions<
D,
R extends responseInterface<any, CommerceError>
> = {
descriptors?: PropertyDescriptorMap
normalizer?: (data: R['data']) => D
}
export type UseResponse = <D, R extends responseInterface<any, CommerceError>>(
response: R,
options: UseResponseOptions<D, R>
) => D extends object ? Override<R, { data?: D }> : R
const useResponse: UseResponse = (response, { descriptors, normalizer }) => {
const memoizedResponse = useMemo(
() =>
Object.create(response, {
...descriptors,
...(normalizer
? {
data: {
get() {
return response.data && normalizer(response.data)
},
enumerable: true,
},
}
: {}),
}),
[response]
)
return memoizedResponse
}
export default useResponse

View File

@ -1,12 +1,19 @@
import useAction from '../utils/use-action' import { useHook, useMutationHook } from '../utils/use-hook'
import type { CartItemBody } from '../types' import { mutationFetcher } from '../utils/default-fetcher'
import type { MutationHook } from '../utils/types'
import type { Provider } from '..'
// Input expected by the action returned by the `useAddItem` hook export type UseAddItem<
// export interface AddItemInput { H extends MutationHook<any, any, any> = MutationHook<any, {}, {}>
// includeProducts?: boolean > = ReturnType<H['useHook']>
// }
export type AddItemInput<T extends CartItemBody> = T
const useAddItem = useAction export const fetcher = mutationFetcher
const fn = (provider: Provider) => provider.wishlist?.useAddItem!
const useAddItem: UseAddItem = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useAddItem export default useAddItem

View File

@ -1,5 +1,28 @@
import useAction from '../utils/use-action' import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Provider } from '..'
const useRemoveItem = useAction export type RemoveItemInput = {
id: string | number
}
export type UseRemoveItem<
H extends MutationHook<any, any, any> = MutationHook<
any | null,
{ wishlist?: any },
RemoveItemInput,
{}
>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<any | null, {}> = mutationFetcher
const fn = (provider: Provider) => provider.wishlist?.useRemoveItem!
const useRemoveItem: UseRemoveItem = (input) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(input)
}
export default useRemoveItem export default useRemoveItem

View File

@ -1,56 +1,25 @@
import { useHook, useSWRHook } from '../utils/use-hook'
import { SWRFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, SWRHook } from '../utils/types'
import type { Wishlist } from '../types' import type { Wishlist } from '../types'
import type { import type { Provider } from '..'
Prop,
HookFetcherFn,
UseHookInput,
UseHookResponse,
} from '../utils/types'
import defaultFetcher from '../utils/default-fetcher'
import useData from '../utils/use-data'
import { Provider, useCommerce } from '..'
export type UseWishlistHandler<P extends Provider> = Prop< export type UseWishlist<
Prop<P, 'wishlist'>, H extends SWRHook<any, any, any> = SWRHook<
'useWishlist' Wishlist | null,
> { includeProducts?: boolean },
{ customerId?: number; includeProducts: boolean },
{ isEmpty?: boolean }
>
> = ReturnType<H['useHook']>
export type UseWishlistInput<P extends Provider> = UseHookInput< export const fetcher: HookFetcherFn<Wishlist | null, any> = SWRFetcher
UseWishlistHandler<P>
>
export type WishlistResponse<P extends Provider> = UseHookResponse< const fn = (provider: Provider) => provider.wishlist?.useWishlist!
UseWishlistHandler<P>
>
export type UseWishlist<P extends Provider> = Partial< const useWishlist: UseWishlist = (input) => {
UseWishlistInput<P> const hook = useHook(fn)
> extends UseWishlistInput<P> return useSWRHook({ fetcher, ...hook })(input)
? (input?: UseWishlistInput<P>) => WishlistResponse<P>
: (input: UseWishlistInput<P>) => WishlistResponse<P>
export const fetcher = defaultFetcher as HookFetcherFn<Wishlist | null>
export default function useWishlist<P extends Provider>(
input: UseWishlistInput<P> = {}
) {
const { providerRef, fetcherRef } = useCommerce<P>()
const provider = providerRef.current
const opts = provider.wishlist?.useWishlist
const fetcherFn = opts?.fetcher ?? fetcher
const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
return useHook({
input,
useData(ctx) {
const response = useData(
{ ...opts!, fetcher: fetcherFn },
ctx?.input ?? [],
provider.fetcher ?? fetcherRef.current,
ctx?.swrOptions ?? input.swrOptions
)
return response
},
})
} }
export default useWishlist

View File

@ -21,7 +21,7 @@
}, },
"dependencies": { "dependencies": {
"@reach/portal": "^0.11.2", "@reach/portal": "^0.11.2",
"@tailwindcss/ui": "^0.6.2", "@types/lodash.memoize": "^4.1.6",
"@vercel/fetch": "^6.1.0", "@vercel/fetch": "^6.1.0",
"body-scroll-lock": "^3.1.5", "body-scroll-lock": "^3.1.5",
"bowser": "^2.11.0", "bowser": "^2.11.0",
@ -33,20 +33,21 @@
"js-cookie": "^2.2.1", "js-cookie": "^2.2.1",
"keen-slider": "^5.2.4", "keen-slider": "^5.2.4",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.memoize": "^4.1.2",
"lodash.random": "^3.2.0", "lodash.random": "^3.2.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"next": "^10.0.5", "next": "^10.0.7-canary.3",
"next-seo": "^4.11.0", "next-seo": "^4.11.0",
"next-themes": "^0.0.4", "next-themes": "^0.0.4",
"normalizr": "^3.6.1", "postcss": "^8.2.4",
"postcss-nesting": "^7.0.1", "postcss-nesting": "^7.0.1",
"react": "^16.14.0", "react": "^17.0.1",
"react-dom": "^16.14.0", "react-dom": "^17.0.1",
"react-merge-refs": "^1.1.0", "react-merge-refs": "^1.1.0",
"react-ticker": "^1.2.2", "react-ticker": "^1.2.2",
"swr": "^0.4.0", "swr": "^0.4.0",
"tabbable": "^5.1.5", "tabbable": "^5.1.5",
"tailwindcss": "^1.9" "tailwindcss": "^2.0.2"
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/cli": "^1.20.0", "@graphql-codegen/cli": "^1.20.0",
@ -56,8 +57,6 @@
"@manifoldco/swagger-to-ts": "^2.1.0", "@manifoldco/swagger-to-ts": "^2.1.0",
"@next/bundle-analyzer": "^10.0.1", "@next/bundle-analyzer": "^10.0.1",
"@types/body-scroll-lock": "^2.6.1", "@types/body-scroll-lock": "^2.6.1",
"@types/bunyan": "^1.8.6",
"@types/bunyan-prettystream": "^0.1.31",
"@types/classnames": "^2.2.10", "@types/classnames": "^2.2.10",
"@types/cookie": "^0.4.0", "@types/cookie": "^0.4.0",
"@types/js-cookie": "^2.2.6", "@types/js-cookie": "^2.2.6",
@ -66,8 +65,6 @@
"@types/lodash.throttle": "^4.1.6", "@types/lodash.throttle": "^4.1.6",
"@types/node": "^14.14.16", "@types/node": "^14.14.16",
"@types/react": "^17.0.0", "@types/react": "^17.0.0",
"bunyan": "^1.8.14",
"bunyan-prettystream": "^0.1.3",
"graphql": "^15.4.0", "graphql": "^15.4.0",
"husky": "^4.3.8", "husky": "^4.3.8",
"lint-staged": "^10.5.3", "lint-staged": "^10.5.3",

View File

@ -1,12 +1,11 @@
import '@assets/main.css' import '@assets/main.css'
import 'keen-slider/keen-slider.min.css'
import '@assets/chrome-bug.css' import '@assets/chrome-bug.css'
import 'keen-slider/keen-slider.min.css'
import { FC, useEffect } from 'react' import { FC, useEffect } from 'react'
import type { AppProps } from 'next/app' import type { AppProps } from 'next/app'
import { ManagedUIContext } from '@components/ui/context'
import { Head } from '@components/common' import { Head } from '@components/common'
import { ManagedUIContext } from '@components/ui/context'
const Noop: FC = ({ children }) => <>{children}</> const Noop: FC = ({ children }) => <>{children}</>

View File

@ -1,13 +1,14 @@
import { Layout } from '@components/common' import { Layout } from '@components/common'
import { Grid, Marquee, Hero } from '@components/ui' import { Grid, Marquee, Hero } from '@components/ui'
import { ProductCard } from '@components/product' import { ProductCard } from '@components/product'
import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid' // import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid'
import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next' import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
import { getConfig } from '@framework/api' import { getConfig } from '@framework/api'
import getAllProducts from '@framework/product/get-all-products' import getAllProducts from '@framework/product/get-all-products'
import getSiteInfo from '@framework/common/get-site-info' import getSiteInfo from '@framework/common/get-site-info'
import getAllPages from '@framework/common/get-all-pages' import getAllPages from '@framework/common/get-all-pages'
import Features from '@commerce/utils/features'
export async function getStaticProps({ export async function getStaticProps({
preview, preview,
@ -23,6 +24,7 @@ export async function getStaticProps({
const { categories, brands } = await getSiteInfo({ config, preview }) const { categories, brands } = await getSiteInfo({ config, preview })
const { pages } = await getAllPages({ config, preview }) const { pages } = await getAllPages({ config, preview })
const isWishlistEnabled = Features.isEnabled('wishlist')
return { return {
props: { props: {
@ -30,6 +32,9 @@ export async function getStaticProps({
categories, categories,
brands, brands,
pages, pages,
commerceFeatures: {
wishlist: isWishlistEnabled,
},
}, },
revalidate: 1440, revalidate: 1440,
} }
@ -39,6 +44,7 @@ export default function Home({
products, products,
brands, brands,
categories, categories,
commerceFeatures,
}: InferGetStaticPropsType<typeof getStaticProps>) { }: InferGetStaticPropsType<typeof getStaticProps>) {
return ( return (
<> <>
@ -51,6 +57,7 @@ export default function Home({
width: i === 0 ? 1080 : 540, width: i === 0 ? 1080 : 540,
height: i === 0 ? 1080 : 540, height: i === 0 ? 1080 : 540,
}} }}
wishlist={commerceFeatures.wishlist}
/> />
))} ))}
</Grid> </Grid>
@ -64,6 +71,7 @@ export default function Home({
width: 320, width: 320,
height: 320, height: 320,
}} }}
wishlist={commerceFeatures.wishlist}
/> />
))} ))}
</Marquee> </Marquee>
@ -86,6 +94,7 @@ export default function Home({
width: i === 0 ? 1080 : 540, width: i === 0 ? 1080 : 540,
height: i === 0 ? 1080 : 540, height: i === 0 ? 1080 : 540,
}} }}
wishlist={commerceFeatures.wishlist}
/> />
))} ))}
</Grid> </Grid>
@ -99,6 +108,7 @@ export default function Home({
width: 320, width: 320,
height: 320, height: 320,
}} }}
wishlist={commerceFeatures.wishlist}
/> />
))} ))}
</Marquee> </Marquee>

View File

@ -1,9 +1,9 @@
import type { GetStaticPropsContext } from 'next' import type { GetStaticPropsContext } from 'next'
import { getConfig } from '@framework/api' import { Bag } from '@components/icons'
import getAllPages from '@framework/common/get-all-pages'
import { Layout } from '@components/common' import { Layout } from '@components/common'
import { Container, Text } from '@components/ui' import { Container, Text } from '@components/ui'
import { Bag } from '@components/icons' import { getConfig } from '@framework/api'
import getAllPages from '@framework/common/get-all-pages'
export async function getStaticProps({ export async function getStaticProps({
preview, preview,

View File

@ -11,14 +11,15 @@ import { getConfig } from '@framework/api'
import getProduct from '@framework/product/get-product' import getProduct from '@framework/product/get-product'
import getAllPages from '@framework/common/get-all-pages' import getAllPages from '@framework/common/get-all-pages'
import getAllProductPaths from '@framework/product/get-all-product-paths' import getAllProductPaths from '@framework/product/get-all-product-paths'
import Features from '@commerce/utils/features'
export async function getStaticProps({ export async function getStaticProps({
params, params,
locale, locale,
preview, preview,
}: GetStaticPropsContext<{ slug: string }>) { }: GetStaticPropsContext<{ slug: string }>) {
const isWishlistEnabled = Features.isEnabled('wishlist')
const config = getConfig({ locale }) const config = getConfig({ locale })
const { pages } = await getAllPages({ config, preview }) const { pages } = await getAllPages({ config, preview })
const { product } = await getProduct({ const { product } = await getProduct({
variables: { slug: params!.slug }, variables: { slug: params!.slug },
@ -31,7 +32,13 @@ export async function getStaticProps({
} }
return { return {
props: { pages, product }, props: {
pages,
product,
commerceFeatures: {
wishlist: isWishlistEnabled,
},
},
revalidate: 200, revalidate: 200,
} }
} }
@ -55,13 +62,17 @@ export async function getStaticPaths({ locales }: GetStaticPathsContext) {
export default function Slug({ export default function Slug({
product, product,
commerceFeatures,
}: InferGetStaticPropsType<typeof getStaticProps>) { }: InferGetStaticPropsType<typeof getStaticProps>) {
const router = useRouter() const router = useRouter()
return router.isFallback ? ( return router.isFallback ? (
<h1>Loading...</h1> // TODO (BC) Add Skeleton Views <h1>Loading...</h1> // TODO (BC) Add Skeleton Views
) : ( ) : (
<ProductView product={product as any} /> <ProductView
product={product as any}
wishlist={commerceFeatures.wishlist}
/>
) )
} }

View File

@ -26,6 +26,8 @@ const SORT = Object.entries({
'price-desc': 'Price: High to low', 'price-desc': 'Price: High to low',
}) })
import Features from '@commerce/utils/features'
import { import {
filterQuery, filterQuery,
getCategoryPath, getCategoryPath,
@ -40,14 +42,23 @@ export async function getStaticProps({
const config = getConfig({ locale }) const config = getConfig({ locale })
const { pages } = await getAllPages({ config, preview }) const { pages } = await getAllPages({ config, preview })
const { categories, brands } = await getSiteInfo({ config, preview }) const { categories, brands } = await getSiteInfo({ config, preview })
const isWishlistEnabled = Features.isEnabled('wishlist')
return { return {
props: { pages, categories, brands }, props: {
pages,
categories,
brands,
commerceFeatures: {
wishlist: isWishlistEnabled,
},
},
} }
} }
export default function Search({ export default function Search({
categories, categories,
brands, brands,
commerceFeatures: { wishlist },
}: InferGetStaticPropsType<typeof getStaticProps>) { }: InferGetStaticPropsType<typeof getStaticProps>) {
const [activeFilter, setActiveFilter] = useState('') const [activeFilter, setActiveFilter] = useState('')
const [toggleFilter, setToggleFilter] = useState(false) const [toggleFilter, setToggleFilter] = useState(false)
@ -337,7 +348,7 @@ export default function Search({
{data ? ( {data ? (
<Grid layout="normal"> <Grid layout="normal">
{data.products.map((product) => ( {data.products.map((product: Product) => (
<ProductCard <ProductCard
variant="simple" variant="simple"
key={product.path} key={product.path}
@ -347,6 +358,7 @@ export default function Search({
width: 480, width: 480,
height: 480, height: 480,
}} }}
wishlist={wishlist}
/> />
))} ))}
</Grid> </Grid>

View File

@ -1,28 +1,43 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import type { GetStaticPropsContext } from 'next' import type { GetStaticPropsContext } from 'next'
import { getConfig } from '@framework/api'
import getAllPages from '@framework/common/get-all-pages'
import useWishlist from '@framework/wishlist/use-wishlist'
import { Layout } from '@components/common'
import { Heart } from '@components/icons' import { Heart } from '@components/icons'
import { Layout } from '@components/common'
import { Text, Container } from '@components/ui' import { Text, Container } from '@components/ui'
import { WishlistCard } from '@components/wishlist'
import { defaultPageProps } from '@lib/defaults' import { defaultPageProps } from '@lib/defaults'
import { getConfig } from '@framework/api'
import { useCustomer } from '@framework/customer' import { useCustomer } from '@framework/customer'
import { WishlistCard } from '@components/wishlist'
import useWishlist from '@framework/wishlist/use-wishlist'
import getAllPages from '@framework/common/get-all-pages'
import Features from '@commerce/utils/features'
export async function getStaticProps({ export async function getStaticProps({
preview, preview,
locale, locale,
}: GetStaticPropsContext) { }: GetStaticPropsContext) {
// Disabling page if Feature is not available
if (Features.isEnabled('wishlist')) {
return {
notFound: true,
}
}
const config = getConfig({ locale }) const config = getConfig({ locale })
const { pages } = await getAllPages({ config, preview }) const { pages } = await getAllPages({ config, preview })
return { return {
props: { ...defaultPageProps, pages }, props: {
pages,
...defaultPageProps,
},
} }
} }
export default function Wishlist() { export default function Wishlist() {
const { data: customer } = useCustomer() const { data: customer } = useCustomer()
const { data, isLoading, isEmpty } = useWishlist() const { data, isLoading, isEmpty } = useWishlist()
const router = useRouter()
return ( return (
<Container> <Container>

View File

@ -26,6 +26,6 @@
"@framework": ["framework/shopify"] "@framework": ["framework/shopify"]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
"exclude": ["node_modules", "components/wishlist"] "exclude": ["node_modules"]
} }

View File

@ -1060,6 +1060,13 @@
dependencies: dependencies:
"@types/lodash" "*" "@types/lodash" "*"
"@types/lodash.memoize@^4.1.6":
version "4.1.6"
resolved "https://registry.yarnpkg.com/@types/lodash.memoize/-/lodash.memoize-4.1.6.tgz#3221f981790a415cab1a239f25c17efd8b604c23"
integrity sha512-mYxjKiKzRadRJVClLKxS4wb3Iy9kzwJ1CkbyKiadVxejnswnRByyofmPMscFKscmYpl36BEEhCMPuWhA1R/1ZQ==
dependencies:
"@types/lodash" "*"
"@types/lodash.random@^3.2.6": "@types/lodash.random@^3.2.6":
version "3.2.6" version "3.2.6"
resolved "https://registry.yarnpkg.com/@types/lodash.random/-/lodash.random-3.2.6.tgz#64b08abad168dca39c778ed40cce75b2f9e168eb" resolved "https://registry.yarnpkg.com/@types/lodash.random/-/lodash.random-3.2.6.tgz#64b08abad168dca39c778ed40cce75b2f9e168eb"
@ -4232,6 +4239,11 @@ lodash.isstring@^4.0.1:
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
lodash.once@^4.0.0: lodash.once@^4.0.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"