diff --git a/README.md b/README.md index 8be54816a..ea6248c51 100644 --- a/README.md +++ b/README.md @@ -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. -## Troubleshoot - -
-I already own a BigCommerce store. What should I do? -
-First thing you do is: set your environment variables -
-
-.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. - -
- -
-BigCommerce shows a Coming Soon page and requests a Preview Code -
-After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard. -
-
-BigCommerce team has been notified and they plan to add more detailed about this subject. -
- -## 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 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 { 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 + +
+I already own a BigCommerce store. What should I do? +
+First thing you do is: set your environment variables +
+
+.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. + +
+ +
+BigCommerce shows a Coming Soon page and requests a Preview Code +
+After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard. +
+
+BigCommerce team has been notified and they plan to add more detailed about this subject. +
diff --git a/components/cart/CartItem/CartItem.tsx b/components/cart/CartItem/CartItem.tsx index bb57c3f25..cb7f8600e 100644 --- a/components/cart/CartItem/CartItem.tsx +++ b/components/cart/CartItem/CartItem.tsx @@ -33,7 +33,7 @@ const CartItem = ({ currencyCode, }) - const updateItem = useUpdateItem(item) + const updateItem = useUpdateItem({ item }) const removeItem = useRemoveItem() const [quantity, setQuantity] = useState(item.quantity) const [removing, setRemoving] = useState(false) diff --git a/components/cart/CartSidebarView/CartSidebarView.tsx b/components/cart/CartSidebarView/CartSidebarView.tsx index c25bd7c95..5b28fde27 100644 --- a/components/cart/CartSidebarView/CartSidebarView.tsx +++ b/components/cart/CartSidebarView/CartSidebarView.tsx @@ -9,7 +9,7 @@ import usePrice from '@framework/product/use-price' import CartItem from '../CartItem' import s from './CartSidebarView.module.css' -const CartSidebarView: FC = () => { +const CartSidebarView: FC<{ wishlist?: boolean }> = ({ wishlist }) => { const { closeSidebar } = useUI() const { data, isLoading, isEmpty } = useCart() @@ -48,7 +48,7 @@ const CartSidebarView: FC = () => {
- +
diff --git a/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx b/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx index 677757b7b..cafcdd1d3 100644 --- a/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx +++ b/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx @@ -5,14 +5,21 @@ import { Grid } from '@components/ui' import { ProductCard } from '@components/product' import s from './HomeAllProductsGrid.module.css' import { getCategoryPath, getDesignerPath } from '@lib/search' +import wishlist from '@framework/api/wishlist' interface Props { categories?: any brands?: any products?: Product[] + wishlist?: boolean } -const Head: FC = ({ categories, brands, products = [] }) => { +const HomeAllProductsGrid: FC = ({ + categories, + brands, + products = [], + wishlist = false, +}) => { return (
@@ -44,6 +51,7 @@ const Head: FC = ({ categories, brands, products = [] }) => { width: 480, height: 480, }} + wishlist={wishlist} /> ))} @@ -52,4 +60,4 @@ const Head: FC = ({ categories, brands, products = [] }) => { ) } -export default Head +export default HomeAllProductsGrid diff --git a/components/common/Layout/Layout.tsx b/components/common/Layout/Layout.tsx index 204c3a871..f4376bbf3 100644 --- a/components/common/Layout/Layout.tsx +++ b/components/common/Layout/Layout.tsx @@ -41,10 +41,14 @@ const FeatureBar = dynamic( interface Props { pageProps: { pages?: Page[] + commerceFeatures: Record } } -const Layout: FC = ({ children, pageProps }) => { +const Layout: FC = ({ + children, + pageProps: { commerceFeatures, ...pageProps }, +}) => { const { displaySidebar, displayModal, @@ -54,11 +58,11 @@ const Layout: FC = ({ children, pageProps }) => { } = useUI() const { acceptedCookies, onAcceptCookies } = useAcceptCookies() const { locale = 'en-US' } = useRouter() - + const isWishlistEnabled = commerceFeatures.wishlist return (
- +
{children}
@@ -69,7 +73,7 @@ const Layout: FC = ({ children, pageProps }) => { - + ( +const Navbar: FC<{ wishlist?: boolean }> = ({ wishlist }) => (
@@ -36,7 +36,7 @@ const Navbar: FC = () => (
- +
diff --git a/components/common/UserNav/UserNav.tsx b/components/common/UserNav/UserNav.tsx index c615c18b1..5d9d58fff 100644 --- a/components/common/UserNav/UserNav.tsx +++ b/components/common/UserNav/UserNav.tsx @@ -12,11 +12,12 @@ import { Avatar } from '@components/common' interface Props { className?: string + wishlist?: boolean } const countItem = (count: number, item: LineItem) => count + item.quantity -const UserNav: FC = ({ className }) => { +const UserNav: FC = ({ className, wishlist = false }) => { const { data } = useCart() const { data: customer } = useCustomer() const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI() @@ -30,13 +31,15 @@ const UserNav: FC = ({ className }) => { {itemsCount > 0 && {itemsCount}} -
  • - - - - - -
  • + {wishlist && ( +
  • + + + + + +
  • + )}
  • {customer ? ( diff --git a/components/product/ProductCard/ProductCard.tsx b/components/product/ProductCard/ProductCard.tsx index 5784e4b1a..b97937cd6 100644 --- a/components/product/ProductCard/ProductCard.tsx +++ b/components/product/ProductCard/ProductCard.tsx @@ -4,13 +4,14 @@ import Link from 'next/link' import type { Product } from '@commerce/types' import s from './ProductCard.module.css' import Image, { ImageProps } from 'next/image' -// import WishlistButton from '@components/wishlist/WishlistButton' +import WishlistButton from '@components/wishlist/WishlistButton' interface Props { className?: string product: Product variant?: 'slim' | 'simple' imgProps?: Omit + wishlist?: boolean } const placeholderImg = '/product-img-placeholder.svg' @@ -20,6 +21,7 @@ const ProductCard: FC = ({ product, variant, imgProps, + wishlist = false, ...props }) => ( @@ -57,11 +59,13 @@ const ProductCard: FC = ({ {product.price.currencyCode}
  • - {/* */} + /> + )}
    {product?.images && ( diff --git a/components/product/ProductSlider/ProductSlider.tsx b/components/product/ProductSlider/ProductSlider.tsx index 4ea7d2ec4..02244f5ba 100644 --- a/components/product/ProductSlider/ProductSlider.tsx +++ b/components/product/ProductSlider/ProductSlider.tsx @@ -50,10 +50,12 @@ const ProductSlider: FC = ({ children }) => { ) return () => { - sliderContainerRef.current!.removeEventListener( - 'touchstart', - preventNavigation - ) + if (sliderContainerRef.current) { + sliderContainerRef.current!.removeEventListener( + 'touchstart', + preventNavigation + ) + } } }, []) diff --git a/components/product/ProductView/ProductView.tsx b/components/product/ProductView/ProductView.tsx index 61beda7fe..c502303c4 100644 --- a/components/product/ProductView/ProductView.tsx +++ b/components/product/ProductView/ProductView.tsx @@ -13,15 +13,16 @@ import usePrice from '@framework/product/use-price' import { useAddItem } from '@framework/cart' import { getVariant, SelectedOptions } from '../helpers' -// import WishlistButton from '@components/wishlist/WishlistButton' +import WishlistButton from '@components/wishlist/WishlistButton' interface Props { className?: string children?: any product: Product + wishlist?: boolean } -const ProductView: FC = ({ product }) => { +const ProductView: FC = ({ product, wishlist = false }) => { const addItem = useAddItem() const { price } = usePrice({ amount: product.price.value, @@ -151,11 +152,13 @@ const ProductView: FC = ({ product }) => {
    - {/* */} + {wishlist && ( + + )} ) diff --git a/components/wishlist/WishlistButton/index.ts b/components/wishlist/WishlistButton/index.ts new file mode 100644 index 000000000..66e88074b --- /dev/null +++ b/components/wishlist/WishlistButton/index.ts @@ -0,0 +1 @@ +export { default } from './WishlistButton' diff --git a/components/wishlist/WishlistCard/WishlistCard.tsx b/components/wishlist/WishlistCard/WishlistCard.tsx index 38663ab68..5e4cce72a 100644 --- a/components/wishlist/WishlistCard/WishlistCard.tsx +++ b/components/wishlist/WishlistCard/WishlistCard.tsx @@ -22,7 +22,7 @@ const WishlistCard: FC = ({ product }) => { baseAmount: product.prices?.retailPrice?.value, currencyCode: product.prices?.price?.currencyCode!, }) - const removeItem = useRemoveItem({ includeProducts: true }) + const removeItem = useRemoveItem({ wishlist: { includeProducts: true } }) const [loading, setLoading] = useState(false) const [removing, setRemoving] = useState(false) const addItem = useAddItem() diff --git a/components/wishlist/index.ts b/components/wishlist/index.ts index 470e6682c..8aee9f816 100644 --- a/components/wishlist/index.ts +++ b/components/wishlist/index.ts @@ -1 +1,2 @@ export { default as WishlistCard } from './WishlistCard' +export { default as WishlistButton } from './WishlistButton' diff --git a/framework/bigcommerce/auth/use-login.tsx b/framework/bigcommerce/auth/use-login.tsx index fa2294666..b66fca493 100644 --- a/framework/bigcommerce/auth/use-login.tsx +++ b/framework/bigcommerce/auth/use-login.tsx @@ -1,54 +1,40 @@ 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 useCommerceLogin from '@commerce/use-login' +import useLogin, { UseLogin } from '@commerce/use-login' import type { LoginBody } from '../api/customers/login' import useCustomer from '../customer/use-customer' -const defaultOpts = { - url: '/api/bigcommerce/customers/login', - method: 'POST', -} +export default useLogin as UseLogin -export type LoginInput = LoginBody +export const handler: MutationHook = { + 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 = ( - options, - { email, password }, - fetch -) => { - if (!(email && password)) { - throw new CommerceError({ - message: - 'A first name, last name, email and password are required to login', + return fetch({ + ...options, + body: { email, password }, }) - } - - return fetch({ - ...defaultOpts, - ...options, - body: { email, password }, - }) -} - -export function extendHook(customFetcher: typeof fetcher) { - const useLogin = () => { + }, + useHook: ({ fetch }) => () => { const { revalidate } = useCustomer() - const fn = useCommerceLogin(defaultOpts, customFetcher) return useCallback( - async function login(input: LoginInput) { - const data = await fn(input) + async function login(input) { + const data = await fetch({ input }) await revalidate() return data }, - [fn] + [fetch, revalidate] ) - } - - useLogin.extend = extendHook - - return useLogin + }, } - -export default extendHook(fetcher) diff --git a/framework/bigcommerce/auth/use-logout.tsx b/framework/bigcommerce/auth/use-logout.tsx index 6aaee29f9..6278a4dd1 100644 --- a/framework/bigcommerce/auth/use-logout.tsx +++ b/framework/bigcommerce/auth/use-logout.tsx @@ -1,38 +1,25 @@ import { useCallback } from 'react' -import type { HookFetcher } from '@commerce/utils/types' -import useCommerceLogout from '@commerce/use-logout' +import type { MutationHook } from '@commerce/utils/types' +import useLogout, { UseLogout } from '@commerce/use-logout' import useCustomer from '../customer/use-customer' -const defaultOpts = { - url: '/api/bigcommerce/customers/logout', - method: 'GET', -} +export default useLogout as UseLogout -export const fetcher: HookFetcher = (options, _, fetch) => { - return fetch({ - ...defaultOpts, - ...options, - }) -} - -export function extendHook(customFetcher: typeof fetcher) { - const useLogout = () => { +export const handler: MutationHook = { + fetchOptions: { + url: '/api/bigcommerce/customers/logout', + method: 'GET', + }, + useHook: ({ fetch }) => () => { const { mutate } = useCustomer() - const fn = useCommerceLogout(defaultOpts, customFetcher) return useCallback( - async function login() { - const data = await fn(null) + async function logout() { + const data = await fetch() await mutate(null, false) return data }, - [fn] + [fetch, mutate] ) - } - - useLogout.extend = extendHook - - return useLogout + }, } - -export default extendHook(fetcher) diff --git a/framework/bigcommerce/auth/use-signup.tsx b/framework/bigcommerce/auth/use-signup.tsx index 0fb54d332..23b7ce9c6 100644 --- a/framework/bigcommerce/auth/use-signup.tsx +++ b/framework/bigcommerce/auth/use-signup.tsx @@ -1,55 +1,44 @@ 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 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 customerCreateMutation from '@framework/utils/mutations/customer-create' -import { CustomerCreateInput } from '@framework/schema' -const defaultOpts = { - query: customerCreateMutation, -} +export default useSignup as UseSignup -export const fetcher: HookFetcher = ( - options, - { firstName, lastName, email, password }, - fetch -) => { - if (!(firstName && lastName && email && password)) { - throw new CommerceError({ - message: - 'A first name, last name, email and password are required to signup', +export const handler: MutationHook = { + fetchOptions: { + url: '/api/bigcommerce/customers/signup', + method: 'POST', + }, + async fetcher({ + input: { firstName, lastName, email, password }, + options, + 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 }, }) - } - - return fetch({ - ...defaultOpts, - ...options, - variables: { firstName, lastName, email, password }, - }) -} - -export function extendHook(customFetcher: typeof fetcher) { - const useSignup = () => { + }, + useHook: ({ fetch }) => () => { const { revalidate } = useCustomer() - const fn = useCommerceSignup( - defaultOpts, - customFetcher - ) return useCallback( - async function signup(input: CustomerCreateInput) { - const data = await fn(input) + async function signup(input) { + const data = await fetch({ input }) await revalidate() return data }, - [fn] + [fetch, revalidate] ) - } - - useSignup.extend = extendHook - - return useSignup + }, } - -export default extendHook(fetcher) diff --git a/framework/bigcommerce/cart/index.ts b/framework/bigcommerce/cart/index.ts index 43c6db2b7..3b8ba990e 100644 --- a/framework/bigcommerce/cart/index.ts +++ b/framework/bigcommerce/cart/index.ts @@ -1,5 +1,4 @@ export { default as useCart } from './use-cart' export { default as useAddItem } from './use-add-item' export { default as useRemoveItem } from './use-remove-item' -export { default as useWishlistActions } from './use-cart-actions' -export { default as useUpdateItem } from './use-cart-actions' +export { default as useUpdateItem } from './use-update-item' diff --git a/framework/bigcommerce/cart/use-add-item.tsx b/framework/bigcommerce/cart/use-add-item.tsx index 7aec2f9e0..d74c23567 100644 --- a/framework/bigcommerce/cart/use-add-item.tsx +++ b/framework/bigcommerce/cart/use-add-item.tsx @@ -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 useAddItem, { UseAddItem } from '@commerce/cart/use-add-item' import { normalizeCart } from '../lib/normalize' import type { - AddCartItemBody, Cart, BigcommerceCart, CartItemBody, + AddCartItemBody, } from '../types' import useCart from './use-cart' -import { BigcommerceProvider } from '..' -const defaultOpts = { - url: '/api/bigcommerce/cart', - method: 'POST', -} +export default useAddItem as UseAddItem -export default useAddItem as UseAddItem - -export const handler: MutationHandler = { +export const handler: MutationHook = { fetchOptions: { url: '/api/bigcommerce/cart', - method: 'GET', + method: 'POST', }, - async fetcher({ input: { item }, options, fetch }) { + async fetcher({ input: item, options, fetch }) { if ( item.quantity && (!Number.isInteger(item.quantity) || item.quantity! < 1) @@ -34,20 +29,22 @@ export const handler: MutationHandler = { } const data = await fetch({ - ...defaultOpts, ...options, body: { item }, }) return normalizeCart(data) }, - useHook() { + useHook: ({ fetch }) => () => { const { mutate } = useCart() - return async function addItem({ input, fetch }) { - const data = await fetch({ input }) - await mutate(data, false) - return data - } + return useCallback( + async function addItem(input) { + const data = await fetch({ input }) + await mutate(data, false) + return data + }, + [fetch, mutate] + ) }, } diff --git a/framework/bigcommerce/cart/use-cart-actions.tsx b/framework/bigcommerce/cart/use-cart-actions.tsx deleted file mode 100644 index abb4a998e..000000000 --- a/framework/bigcommerce/cart/use-cart-actions.tsx +++ /dev/null @@ -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 } -} diff --git a/framework/bigcommerce/cart/use-cart.tsx b/framework/bigcommerce/cart/use-cart.tsx index b5cc0cccf..2098e7431 100644 --- a/framework/bigcommerce/cart/use-cart.tsx +++ b/framework/bigcommerce/cart/use-cart.tsx @@ -1,13 +1,12 @@ 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 { normalizeCart } from '../lib/normalize' import type { Cart } from '../types' -import type { BigcommerceProvider } from '..' -export default useCart as UseCart +export default useCart as UseCart -export const handler: HookHandler< +export const handler: SWRHook< Cart | null, {}, FetchCartInput, @@ -21,9 +20,9 @@ export const handler: HookHandler< const data = cartId ? await fetch(options) : null return data && normalizeCart(data) }, - useHook({ input, useData }) { + useHook: ({ useData }) => (input) => { const response = useData({ - swrOptions: { revalidateOnFocus: false, ...input.swrOptions }, + swrOptions: { revalidateOnFocus: false, ...input?.swrOptions }, }) return useMemo( diff --git a/framework/bigcommerce/cart/use-remove-item.tsx b/framework/bigcommerce/cart/use-remove-item.tsx index c8cdaeb0d..186780d6a 100644 --- a/framework/bigcommerce/cart/use-remove-item.tsx +++ b/framework/bigcommerce/cart/use-remove-item.tsx @@ -1,8 +1,12 @@ 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 useCartRemoveItem, { - RemoveItemInput as UseRemoveItemInput, +import useRemoveItem, { + RemoveItemInput as RemoveItemInputBase, + UseRemoveItem, } from '@commerce/cart/use-remove-item' import { normalizeCart } from '../lib/normalize' import type { @@ -13,41 +17,41 @@ import type { } from '../types' import useCart from './use-cart' -const defaultOpts = { - url: '/api/bigcommerce/cart', - method: 'DELETE', -} - export type RemoveItemFn = T extends LineItem ? (input?: RemoveItemInput) => Promise : (input: RemoveItemInput) => Promise export type RemoveItemInput = T extends LineItem - ? Partial - : UseRemoveItemInput + ? Partial + : RemoveItemInputBase -export const fetcher: HookFetcher = async ( - options, - { itemId }, - fetch -) => { - const data = await fetch({ - ...defaultOpts, - ...options, - body: { itemId }, - }) - return normalizeCart(data) -} +export default useRemoveItem as UseRemoveItem -export function extendHook(customFetcher: typeof fetcher) { - const useRemoveItem = ( - item?: T +export const handler = { + fetchOptions: { + url: '/api/bigcommerce/cart', + method: 'DELETE', + }, + async fetcher({ + input: { itemId }, + options, + fetch, + }: HookFetcherContext) { + const data = await fetch({ + ...options, + body: { itemId }, + }) + return normalizeCart(data) + }, + useHook: ({ + fetch, + }: MutationHookContext) => < + T extends LineItem | undefined = undefined + >( + ctx: { item?: T } = {} ) => { + const { item } = ctx const { mutate } = useCart() - const fn = useCartRemoveItem( - defaultOpts, - customFetcher - ) const removeItem: RemoveItemFn = async (input) => { 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) return data } - return useCallback(removeItem as RemoveItemFn, [fn, mutate]) - } - - useRemoveItem.extend = extendHook - - return useRemoveItem + return useCallback(removeItem as RemoveItemFn, [fetch, mutate]) + }, } - -export default extendHook(fetcher) diff --git a/framework/bigcommerce/cart/use-update-item.tsx b/framework/bigcommerce/cart/use-update-item.tsx index d1870c818..f1840f806 100644 --- a/framework/bigcommerce/cart/use-update-item.tsx +++ b/framework/bigcommerce/cart/use-update-item.tsx @@ -1,9 +1,13 @@ import { useCallback } from 'react' 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 useCartUpdateItem, { - UpdateItemInput as UseUpdateItemInput, +import useUpdateItem, { + UpdateItemInput as UpdateItemInputBase, + UseUpdateItem, } from '@commerce/cart/use-update-item' import { normalizeCart } from '../lib/normalize' import type { @@ -12,52 +16,59 @@ import type { BigcommerceCart, LineItem, } from '../types' -import { fetcher as removeFetcher } from './use-remove-item' +import { handler as removeItemHandler } from './use-remove-item' import useCart from './use-cart' -const defaultOpts = { - url: '/api/bigcommerce/cart', - method: 'PUT', -} - export type UpdateItemInput = T extends LineItem - ? Partial> - : UseUpdateItemInput + ? Partial> + : UpdateItemInputBase -export const fetcher: HookFetcher = async ( - options, - { itemId, item }, - fetch -) => { - 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 removeFetcher(null, { itemId }, fetch) +export default useUpdateItem as UseUpdateItem + +export const handler = { + fetchOptions: { + url: '/api/bigcommerce/cart', + method: 'PUT', + }, + async fetcher({ + input: { itemId, item }, + options, + fetch, + }: HookFetcherContext) { + 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({ - message: 'The item quantity has to be a valid integer', + + const data = await fetch({ + ...options, + body: { itemId, item }, }) - } - const data = await fetch({ - ...defaultOpts, - ...options, - body: { itemId, item }, - }) - - return normalizeCart(data) -} - -function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) { - const useUpdateItem = ( - item?: T + return normalizeCart(data) + }, + useHook: ({ + fetch, + }: MutationHookContext) => < + T extends LineItem | undefined = undefined + >( + ctx: { + item?: T + wait?: number + } = {} ) => { - const { mutate } = useCart() - const fn = useCartUpdateItem( - defaultOpts, - customFetcher - ) + const { item } = ctx + const { mutate } = useCart() as any return useCallback( debounce(async (input: UpdateItemInput) => { @@ -71,20 +82,16 @@ function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) { }) } - const data = await fn({ - itemId, - item: { productId, variantId, quantity: input.quantity }, + const data = await fetch({ + input: { + itemId, + item: { productId, variantId, quantity: input.quantity }, + }, }) await mutate(data, false) return data - }, cfg?.wait ?? 500), - [fn, mutate] + }, ctx.wait ?? 500), + [fetch, mutate] ) - } - - useUpdateItem.extend = extendHook - - return useUpdateItem + }, } - -export default extendHook(fetcher) diff --git a/framework/bigcommerce/config.json b/framework/bigcommerce/config.json new file mode 100644 index 000000000..17ef37e25 --- /dev/null +++ b/framework/bigcommerce/config.json @@ -0,0 +1,5 @@ +{ + "features": { + "wishlist": false + } +} diff --git a/framework/bigcommerce/customer/use-customer.tsx b/framework/bigcommerce/customer/use-customer.tsx index 3929002f7..093007824 100644 --- a/framework/bigcommerce/customer/use-customer.tsx +++ b/framework/bigcommerce/customer/use-customer.tsx @@ -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 type { Customer, CustomerData } from '../api/customers' -import type { BigcommerceProvider } from '..' -export default useCustomer as UseCustomer +export default useCustomer as UseCustomer -export const handler: HookHandler = { +export const handler: SWRHook = { fetchOptions: { url: '/api/bigcommerce/customers', method: 'GET', @@ -14,11 +13,11 @@ export const handler: HookHandler = { const data = await fetch(options) return data?.customer ?? null }, - useHook({ input, useData }) { + useHook: ({ useData }) => (input) => { return useData({ swrOptions: { revalidateOnFocus: false, - ...input.swrOptions, + ...input?.swrOptions, }, }) }, diff --git a/framework/bigcommerce/product/use-search.tsx b/framework/bigcommerce/product/use-search.tsx index 393a8c0b9..0ee135032 100644 --- a/framework/bigcommerce/product/use-search.tsx +++ b/framework/bigcommerce/product/use-search.tsx @@ -1,9 +1,8 @@ -import { HookHandler } from '@commerce/utils/types' -import useSearch, { UseSearch } from '@commerce/products/use-search' +import { SWRHook } from '@commerce/utils/types' +import useSearch, { UseSearch } from '@commerce/product/use-search' import type { SearchProductsData } from '../api/catalog/products' -import type { BigcommerceProvider } from '..' -export default useSearch as UseSearch +export default useSearch as UseSearch export type SearchProductsInput = { search?: string @@ -12,7 +11,7 @@ export type SearchProductsInput = { sort?: string } -export const handler: HookHandler< +export const handler: SWRHook< SearchProductsData, SearchProductsInput, SearchProductsInput @@ -37,7 +36,7 @@ export const handler: HookHandler< method: options.method, }) }, - useHook({ input, useData }) { + useHook: ({ useData }) => (input = {}) => { return useData({ input: [ ['search', input.search], diff --git a/framework/bigcommerce/provider.ts b/framework/bigcommerce/provider.ts index 08192df37..196855438 100644 --- a/framework/bigcommerce/provider.ts +++ b/framework/bigcommerce/provider.ts @@ -1,18 +1,34 @@ import { handler as useCart } from './cart/use-cart' 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 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 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' export const bigcommerceProvider = { locale: 'en-us', cartCookie: 'bc_cartId', fetcher, - cart: { useCart, useAddItem }, - wishlist: { useWishlist }, + cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, + wishlist: { + useWishlist, + useAddItem: useWishlistAddItem, + useRemoveItem: useWishlistRemoveItem, + }, customer: { useCustomer }, products: { useSearch }, + auth: { useLogin, useLogout, useSignup }, } export type BigcommerceProvider = typeof bigcommerceProvider diff --git a/framework/bigcommerce/types.ts b/framework/bigcommerce/types.ts index 16d1ea07a..beeab0223 100644 --- a/framework/bigcommerce/types.ts +++ b/framework/bigcommerce/types.ts @@ -43,9 +43,6 @@ export type CartItemBody = Core.CartItemBody & { 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 AddCartItemBody = Core.AddCartItemBody diff --git a/framework/bigcommerce/wishlist/use-add-item.tsx b/framework/bigcommerce/wishlist/use-add-item.tsx index eb961951a..402e7da8b 100644 --- a/framework/bigcommerce/wishlist/use-add-item.tsx +++ b/framework/bigcommerce/wishlist/use-add-item.tsx @@ -1,43 +1,24 @@ import { useCallback } from 'react' -import { HookFetcher } from '@commerce/utils/types' +import type { MutationHook } from '@commerce/utils/types' import { CommerceError } from '@commerce/utils/errors' -import useWishlistAddItem, { - AddItemInput, -} from '@commerce/wishlist/use-add-item' -import { UseWishlistInput } from '@commerce/wishlist/use-wishlist' +import useAddItem, { UseAddItem } from '@commerce/wishlist/use-add-item' import type { ItemBody, AddItemBody } from '../api/wishlist' import useCustomer from '../customer/use-customer' import useWishlist from './use-wishlist' -import type { BigcommerceProvider } from '..' -const defaultOpts = { - url: '/api/bigcommerce/wishlist', - method: 'POST', -} +export default useAddItem as UseAddItem -// export type AddItemInput = ItemBody - -export const fetcher: HookFetcher = ( - options, - { item }, - fetch -) => { - // TODO: add validations before doing the fetch - return fetch({ - ...defaultOpts, - ...options, - body: { item }, - }) -} - -export function extendHook(customFetcher: typeof fetcher) { - const useAddItem = (opts?: UseWishlistInput) => { +export const handler: MutationHook = { + fetchOptions: { + url: '/api/bigcommerce/wishlist', + method: 'POST', + }, + useHook: ({ fetch }) => () => { const { data: customer } = useCustomer() - const { revalidate } = useWishlist(opts) - const fn = useWishlistAddItem(defaultOpts, customFetcher) + const { revalidate } = useWishlist() return useCallback( - async function addItem(input: AddItemInput) { + async function addItem(item) { if (!customer) { // A signed customer is required in order to have a wishlist 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() return data }, - [fn, revalidate, customer] + [fetch, revalidate, customer] ) - } - - useAddItem.extend = extendHook - - return useAddItem + }, } - -export default extendHook(fetcher) diff --git a/framework/bigcommerce/wishlist/use-remove-item.tsx b/framework/bigcommerce/wishlist/use-remove-item.tsx index d00b3e78b..622f321db 100644 --- a/framework/bigcommerce/wishlist/use-remove-item.tsx +++ b/framework/bigcommerce/wishlist/use-remove-item.tsx @@ -1,43 +1,32 @@ import { useCallback } from 'react' -import { HookFetcher } from '@commerce/utils/types' +import type { MutationHook } from '@commerce/utils/types' import { CommerceError } from '@commerce/utils/errors' -import useWishlistRemoveItem from '@commerce/wishlist/use-remove-item' -import type { RemoveItemBody } from '../api/wishlist' +import useRemoveItem, { + RemoveItemInput, + UseRemoveItem, +} from '@commerce/wishlist/use-remove-item' +import type { RemoveItemBody, Wishlist } from '../api/wishlist' import useCustomer from '../customer/use-customer' -import useWishlist from './use-wishlist' +import useWishlist, { UseWishlistInput } from './use-wishlist' -const defaultOpts = { - url: '/api/bigcommerce/wishlist', - method: 'DELETE', -} +export default useRemoveItem as UseRemoveItem -export type RemoveItemInput = { - id: string | number -} - -export const fetcher: HookFetcher = ( - options, - { itemId }, - fetch -) => { - return fetch({ - ...defaultOpts, - ...options, - body: { itemId }, - }) -} - -export function extendHook(customFetcher: typeof fetcher) { - const useRemoveItem = (opts?: any) => { +export const handler: MutationHook< + Wishlist | null, + { wishlist?: UseWishlistInput }, + RemoveItemInput, + RemoveItemBody +> = { + fetchOptions: { + url: '/api/bigcommerce/wishlist', + method: 'DELETE', + }, + useHook: ({ fetch }) => ({ wishlist } = {}) => { const { data: customer } = useCustomer() - const { revalidate } = useWishlist(opts) - const fn = useWishlistRemoveItem( - defaultOpts, - customFetcher - ) + const { revalidate } = useWishlist(wishlist) return useCallback( - async function removeItem(input: RemoveItemInput) { + async function removeItem(input) { if (!customer) { // A signed customer is required in order to have a wishlist 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() return data }, - [fn, revalidate, customer] + [fetch, revalidate, customer] ) - } - - useRemoveItem.extend = extendHook - - return useRemoveItem + }, } - -export default extendHook(fetcher) diff --git a/framework/bigcommerce/wishlist/use-wishlist.tsx b/framework/bigcommerce/wishlist/use-wishlist.tsx index a93f0f6a4..3efba7ffd 100644 --- a/framework/bigcommerce/wishlist/use-wishlist.tsx +++ b/framework/bigcommerce/wishlist/use-wishlist.tsx @@ -1,16 +1,17 @@ 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 type { Wishlist } from '../api/wishlist' import useCustomer from '../customer/use-customer' -import type { BigcommerceProvider } from '..' -export default useWishlist as UseWishlist +export type UseWishlistInput = { includeProducts?: boolean } -export const handler: HookHandler< +export default useWishlist as UseWishlist + +export const handler: SWRHook< Wishlist | null, - { includeProducts?: boolean }, - { customerId?: number; includeProducts: boolean }, + UseWishlistInput, + { customerId?: number } & UseWishlistInput, { isEmpty?: boolean } > = { fetchOptions: { @@ -30,16 +31,16 @@ export const handler: HookHandler< method: options.method, }) }, - useHook({ input, useData }) { + useHook: ({ useData }) => (input) => { const { data: customer } = useCustomer() const response = useData({ input: [ ['customerId', (customer as any)?.id], - ['includeProducts', input.includeProducts], + ['includeProducts', input?.includeProducts], ], swrOptions: { revalidateOnFocus: false, - ...input.swrOptions, + ...input?.swrOptions, }, }) diff --git a/framework/commerce/cart/use-add-item.tsx b/framework/commerce/cart/use-add-item.tsx index 0a70ff30d..324464656 100644 --- a/framework/commerce/cart/use-add-item.tsx +++ b/framework/commerce/cart/use-add-item.tsx @@ -1,69 +1,23 @@ -import { useCallback } from 'react' -import type { - Prop, - HookFetcherFn, - UseHookInput, - UseHookResponse, -} from '../utils/types' +import { useHook, useMutationHook } from '../utils/use-hook' +import { mutationFetcher } from '../utils/default-fetcher' +import type { HookFetcherFn, MutationHook } from '../utils/types' import type { Cart, CartItemBody, AddCartItemBody } from '../types' -import { Provider, useCommerce } from '..' -import { BigcommerceProvider } from '@framework' +import type { Provider } from '..' -export type UseAddItemHandler

    = Prop< - Prop, - 'useAddItem' -> - -// Input expected by the action returned by the `useAddItem` hook -export type UseAddItemInput

    = UseHookInput< - UseAddItemHandler

    -> - -export type UseAddItemResult

    = ReturnType< - UseHookResponse> -> - -export type UseAddItem

    = Partial< - UseAddItemInput

    -> extends UseAddItemInput

    - ? (input?: UseAddItemInput

    ) => (input: Input) => UseAddItemResult

    - : (input: UseAddItemInput

    ) => (input: Input) => UseAddItemResult

    +export type UseAddItem< + H extends MutationHook = MutationHook +> = ReturnType export const fetcher: HookFetcherFn< Cart, AddCartItemBody -> = async ({ options, input, fetch }) => { - return fetch({ ...options, body: input }) +> = mutationFetcher + +const fn = (provider: Provider) => provider.cart?.useAddItem! + +const useAddItem: UseAddItem = (...args) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(...args) } -type X = UseAddItemResult - -export default function useAddItem

    ( - input: UseAddItemInput

    -) { - const { providerRef, fetcherRef } = useCommerce

    () - - 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] - ) -} +export default useAddItem diff --git a/framework/commerce/cart/use-cart-actions.tsx b/framework/commerce/cart/use-cart-actions.tsx deleted file mode 100644 index 3ba4b2e1a..000000000 --- a/framework/commerce/cart/use-cart-actions.tsx +++ /dev/null @@ -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( - options: HookFetcherOptions, - fetcher: HookFetcher -) { - const addItem = useAddItem(options, fetcher) - const updateItem = useUpdateItem(options, fetcher) - const removeItem = useRemoveItem(options, fetcher) - - return { addItem, updateItem, removeItem } -} diff --git a/framework/commerce/cart/use-cart.tsx b/framework/commerce/cart/use-cart.tsx index f7b384047..fbed715c8 100644 --- a/framework/commerce/cart/use-cart.tsx +++ b/framework/commerce/cart/use-cart.tsx @@ -1,34 +1,21 @@ 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 { - Prop, - HookFetcherFn, - UseHookInput, - UseHookResponse, -} from '../utils/types' -import useData from '../utils/use-data' import { Provider, useCommerce } from '..' export type FetchCartInput = { cartId?: Cart['id'] } -export type UseCartHandler

    = Prop< - Prop, - 'useCart' -> - -export type UseCartInput

    = UseHookInput> - -export type CartResponse

    = UseHookResponse< - UseCartHandler

    -> - -export type UseCart

    = Partial< - UseCartInput

    -> extends UseCartInput

    - ? (input?: UseCartInput

    ) => CartResponse

    - : (input: UseCartInput

    ) => CartResponse

    +export type UseCart< + H extends SWRHook = SWRHook< + Cart | null, + {}, + FetchCartInput, + { isEmpty?: boolean } + > +> = ReturnType export const fetcher: HookFetcherFn = async ({ options, @@ -38,32 +25,17 @@ export const fetcher: HookFetcherFn = async ({ return cartId ? await fetch({ ...options }) : null } -export default function useCart

    ( - input: UseCartInput

    = {} -) { - const { providerRef, fetcherRef, cartCookie } = useCommerce

    () - - const provider = providerRef.current - const opts = provider.cart?.useCart - - const fetcherFn = opts?.fetcher ?? fetcher - const useHook = opts?.useHook ?? ((ctx) => ctx.useData()) +const fn = (provider: Provider) => provider.cart?.useCart! +const useCart: UseCart = (input) => { + const hook = useHook(fn) + const { cartCookie } = useCommerce() + const fetcherFn = hook.fetcher ?? fetcher const wrapper: typeof fetcher = (context) => { context.input.cartId = Cookies.get(cartCookie) return fetcherFn(context) } - - return useHook({ - input, - useData(ctx) { - const response = useData( - { ...opts!, fetcher: wrapper }, - ctx?.input ?? [], - provider.fetcher ?? fetcherRef.current, - ctx?.swrOptions ?? input.swrOptions - ) - return response - }, - }) + return useSWRHook({ ...hook, fetcher: wrapper })(input) } + +export default useCart diff --git a/framework/commerce/cart/use-remove-item.tsx b/framework/commerce/cart/use-remove-item.tsx index 8a63b1b73..a9d1b37d2 100644 --- a/framework/commerce/cart/use-remove-item.tsx +++ b/framework/commerce/cart/use-remove-item.tsx @@ -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 } -const useRemoveItem = useAction +export type UseRemoveItem< + H extends MutationHook = MutationHook< + Cart | null, + { item?: LineItem }, + RemoveItemInput, + RemoveCartItemBody + > +> = ReturnType + +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 diff --git a/framework/commerce/cart/use-update-item.tsx b/framework/commerce/cart/use-update-item.tsx index e1adcb5fb..f8d0f1a40 100644 --- a/framework/commerce/cart/use-update-item.tsx +++ b/framework/commerce/cart/use-update-item.tsx @@ -1,11 +1,38 @@ -import useAction from '../utils/use-action' -import type { CartItemBody } from '../types' +import { useHook, useMutationHook } from '../utils/use-hook' +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 & { id: string } -const useUpdateItem = useAction +export type UseUpdateItem< + H extends MutationHook = MutationHook< + Cart | null, + { + item?: LineItem + wait?: number + }, + UpdateItemInput, + UpdateCartItemBody + > +> = ReturnType + +export const fetcher: HookFetcherFn< + Cart | null, + UpdateCartItemBody +> = mutationFetcher + +const fn = (provider: Provider) => provider.cart?.useUpdateItem! + +const useUpdateItem: UseUpdateItem = (input) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(input) +} export default useUpdateItem diff --git a/framework/commerce/config.json b/framework/commerce/config.json new file mode 100644 index 000000000..a0e7afc5d --- /dev/null +++ b/framework/commerce/config.json @@ -0,0 +1,5 @@ +{ + "features": { + "wishlist": true + } +} diff --git a/framework/commerce/customer/use-customer.tsx b/framework/commerce/customer/use-customer.tsx index 25112128e..5d6416a4b 100644 --- a/framework/commerce/customer/use-customer.tsx +++ b/framework/commerce/customer/use-customer.tsx @@ -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 { - Prop, - HookFetcherFn, - UseHookInput, - UseHookResponse, -} from '../utils/types' -import defaultFetcher from '../utils/default-fetcher' -import useData from '../utils/use-data' -import { Provider, useCommerce } from '..' +import { Provider } from '..' -export type UseCustomerHandler

    = Prop< - Prop, - 'useCustomer' -> +export type UseCustomer< + H extends SWRHook = SWRHook +> = ReturnType -export type UseCustomerInput

    = UseHookInput< - UseCustomerHandler

    -> +export const fetcher: HookFetcherFn = SWRFetcher -export type CustomerResponse

    = UseHookResponse< - UseCustomerHandler

    -> +const fn = (provider: Provider) => provider.customer?.useCustomer! -export type UseCustomer

    = Partial< - UseCustomerInput

    -> extends UseCustomerInput

    - ? (input?: UseCustomerInput

    ) => CustomerResponse

    - : (input: UseCustomerInput

    ) => CustomerResponse

    - -export const fetcher = defaultFetcher as HookFetcherFn - -export default function useCustomer

    ( - input: UseCustomerInput

    = {} -) { - const { providerRef, fetcherRef } = useCommerce

    () - - 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 - }, - }) +const useCustomer: UseCustomer = (input) => { + const hook = useHook(fn) + return useSWRHook({ fetcher, ...hook })(input) } + +export default useCustomer diff --git a/framework/commerce/index.tsx b/framework/commerce/index.tsx index 243fba2db..07bf74a22 100644 --- a/framework/commerce/index.tsx +++ b/framework/commerce/index.tsx @@ -6,7 +6,7 @@ import { useMemo, useRef, } 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 { Cart, Wishlist, Customer, SearchProductsData } from './types' @@ -15,17 +15,26 @@ const Commerce = createContext | {}>({}) export type Provider = CommerceConfig & { fetcher: Fetcher cart?: { - useCart?: HookHandler - useAddItem?: MutationHandler + useCart?: SWRHook + useAddItem?: MutationHook + useUpdateItem?: MutationHook + useRemoveItem?: MutationHook } wishlist?: { - useWishlist?: HookHandler + useWishlist?: SWRHook + useAddItem?: MutationHook + useRemoveItem?: MutationHook } - customer: { - useCustomer?: HookHandler + customer?: { + useCustomer?: SWRHook } - products: { - useSearch?: HookHandler + products?: { + useSearch?: SWRHook + } + auth?: { + useSignup?: MutationHook + useLogin?: MutationHook + useLogout?: MutationHook } } diff --git a/framework/commerce/product/use-search.tsx b/framework/commerce/product/use-search.tsx new file mode 100644 index 000000000..d2b782045 --- /dev/null +++ b/framework/commerce/product/use-search.tsx @@ -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 = SWRHook +> = ReturnType + +export const fetcher: HookFetcherFn = SWRFetcher + +const fn = (provider: Provider) => provider.products?.useSearch! + +const useSearch: UseSearch = (input) => { + const hook = useHook(fn) + return useSWRHook({ fetcher, ...hook })(input) +} + +export default useSearch diff --git a/framework/commerce/products/use-search.tsx b/framework/commerce/products/use-search.tsx deleted file mode 100644 index 1f887f5fe..000000000 --- a/framework/commerce/products/use-search.tsx +++ /dev/null @@ -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

    = Prop< - Prop, - 'useSearch' -> - -export type UseSeachInput

    = UseHookInput< - UseSearchHandler

    -> - -export type SearchResponse

    = UseHookResponse< - UseSearchHandler

    -> - -export type UseSearch

    = Partial< - UseSeachInput

    -> extends UseSeachInput

    - ? (input?: UseSeachInput

    ) => SearchResponse

    - : (input: UseSeachInput

    ) => SearchResponse

    - -export const fetcher = defaultFetcher as HookFetcherFn - -export default function useSearch

    ( - input: UseSeachInput

    = {} -) { - const { providerRef, fetcherRef } = useCommerce

    () - - 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 - }, - }) -} diff --git a/framework/commerce/types.ts b/framework/commerce/types.ts index bf635c9dc..0ae766095 100644 --- a/framework/commerce/types.ts +++ b/framework/commerce/types.ts @@ -2,6 +2,10 @@ import type { Wishlist as BCWishlist } from '@framework/api/wishlist' import type { Customer as BCCustomer } from '@framework/api/customers' import type { SearchProductsData as BCSearchProductsData } from '@framework/api/catalog/products' +export type CommerceProviderConfig = { + features: Record +} + export type Discount = { // The value of the discount, can be an amount or percentage value: number diff --git a/framework/commerce/use-login.tsx b/framework/commerce/use-login.tsx index 2a251fea3..755e10fd9 100644 --- a/framework/commerce/use-login.tsx +++ b/framework/commerce/use-login.tsx @@ -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 = MutationHook +> = ReturnType + +export const fetcher: HookFetcherFn = mutationFetcher + +const fn = (provider: Provider) => provider.auth?.useLogin! + +const useLogin: UseLogin = (...args) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(...args) +} export default useLogin diff --git a/framework/commerce/use-logout.tsx b/framework/commerce/use-logout.tsx index ef3fc4135..0a80c318b 100644 --- a/framework/commerce/use-logout.tsx +++ b/framework/commerce/use-logout.tsx @@ -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 = MutationHook +> = ReturnType + +export const fetcher: HookFetcherFn = mutationFetcher + +const fn = (provider: Provider) => provider.auth?.useLogout! + +const useLogout: UseLogout = (...args) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(...args) +} export default useLogout diff --git a/framework/commerce/use-signup.tsx b/framework/commerce/use-signup.tsx index 08ddb22c0..be3c32000 100644 --- a/framework/commerce/use-signup.tsx +++ b/framework/commerce/use-signup.tsx @@ -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 = MutationHook +> = ReturnType + +export const fetcher: HookFetcherFn = mutationFetcher + +const fn = (provider: Provider) => provider.auth?.useSignup! + +const useSignup: UseSignup = (...args) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(...args) +} export default useSignup diff --git a/framework/commerce/utils/default-fetcher.ts b/framework/commerce/utils/default-fetcher.ts index 8dc9def75..493a9b5f9 100644 --- a/framework/commerce/utils/default-fetcher.ts +++ b/framework/commerce/utils/default-fetcher.ts @@ -1,6 +1,12 @@ import type { HookFetcherFn } from './types' -const defaultFetcher: HookFetcherFn = ({ options, fetch }) => +export const SWRFetcher: HookFetcherFn = ({ options, fetch }) => fetch(options) -export default defaultFetcher +export const mutationFetcher: HookFetcherFn = ({ + input, + options, + fetch, +}) => fetch({ ...options, body: input }) + +export default SWRFetcher diff --git a/framework/commerce/utils/features.ts b/framework/commerce/utils/features.ts new file mode 100644 index 000000000..d84321967 --- /dev/null +++ b/framework/commerce/utils/features.ts @@ -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() diff --git a/framework/commerce/utils/types.ts b/framework/commerce/utils/types.ts index 1d3adef81..852afb208 100644 --- a/framework/commerce/utils/types.ts +++ b/framework/commerce/utils/types.ts @@ -2,13 +2,18 @@ import type { ConfigInterface } from 'swr' import type { CommerceError } from './errors' 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 = Omit & K /** * Returns the properties in T with the properties in type K changed from optional to required */ export type PickRequired = Omit & - Required> + { + [P in K]-?: NonNullable + } /** * Core fetcher added by CommerceProvider @@ -31,16 +36,15 @@ export type HookFetcher = ( fetch: (options: FetcherOptions) => Promise ) => Data | Promise -export type HookFetcherFn< - Data, - Input = never, - Result = any, - Body = any -> = (context: { +export type HookFetcherFn = ( + context: HookFetcherContext +) => Data | Promise + +export type HookFetcherContext = { options: HookFetcherOptions input: Input fetch: (options: FetcherOptions) => Promise -}) => Data | Promise +} export type HookFetcherOptions = { method?: string } & ( | { query: string; url?: string } @@ -49,13 +53,20 @@ export type HookFetcherOptions = { method?: string } & ( 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 HookInput = {} +export type HookFunction< + Input extends { [k: string]: unknown } | null, + T +> = keyof Input extends never + ? () => T + : Partial extends Input + ? (input?: Input) => T + : (input: Input) => T -export type HookHandler< +export type SWRHook< // Data obj returned by the hook and fetch operation Data, // Input expected by the hook @@ -65,58 +76,56 @@ export type HookHandler< // Custom state added to the response object of SWR State = {} > = { - useHook?(context: { - input: Input & { swrOptions?: SwrOptions } - useData(context?: { - input?: HookFetchInput | HookSwrInput - swrOptions?: SwrOptions - }): ResponseState - }): ResponseState & State + useHook( + context: SWRHookContext + ): HookFunction< + Input & { swrOptions?: SwrOptions }, + ResponseState & State + > fetchOptions: HookFetcherOptions fetcher?: HookFetcherFn } -export type MutationHandler< +export type SWRHookContext< + Data, + FetchInput extends { [k: string]: unknown } = {} +> = { + useData(context?: { + input?: HookFetchInput | HookSWRInput + swrOptions?: SwrOptions + }): ResponseState +} + +export type MutationHook< // Data obj returned by the hook and fetch operation Data, // Input expected by the hook 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 - FetchInput extends { [k: string]: unknown } = {} + FetchInput extends { [k: string]: unknown } = ActionInput > = { - useHook?(context: { - input: Input - }): (context: { - input: FetchInput - fetch: (context: { input: FetchInput }) => Data | Promise - }) => Data | Promise + useHook( + context: MutationHookContext + ): HookFunction>> fetchOptions: HookFetcherOptions fetcher?: HookFetcherFn } +export type MutationHookContext< + Data, + FetchInput extends { [k: string]: unknown } | null = {} +> = { + fetch: keyof FetchInput extends never + ? () => Data | Promise + : Partial extends FetchInput + ? (context?: { input?: FetchInput }) => Data | Promise + : (context: { input: FetchInput }) => Data | Promise +} + export type SwrOptions = ConfigInterface< Data, CommerceError, HookFetcher > - -/** - * Returns the property K from type T excluding nullables - */ -export type Prop = NonNullable - -export type HookHandlerType = - | HookHandler - | MutationHandler - -export type UseHookParameters = Parameters< - Prop -> - -export type UseHookResponse = ReturnType< - Prop -> - -export type UseHookInput< - H extends HookHandlerType -> = UseHookParameters[0]['input'] diff --git a/framework/commerce/utils/use-action.tsx b/framework/commerce/utils/use-action.tsx deleted file mode 100644 index 24593383f..000000000 --- a/framework/commerce/utils/use-action.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useCallback } from 'react' -import type { HookFetcher, HookFetcherOptions } from './types' -import { useCommerce } from '..' - -export default function useAction( - options: HookFetcherOptions, - fetcher: HookFetcher -) { - const { fetcherRef } = useCommerce() - - return useCallback( - (input: Input) => fetcher(options, input, fetcherRef.current), - [fetcher] - ) -} diff --git a/framework/commerce/utils/use-data.tsx b/framework/commerce/utils/use-data.tsx index 94679a0c6..9224b612c 100644 --- a/framework/commerce/utils/use-data.tsx +++ b/framework/commerce/utils/use-data.tsx @@ -1,11 +1,11 @@ import useSWR, { responseInterface } from 'swr' import type { - HookHandler, - HookSwrInput, + HookSWRInput, HookFetchInput, - PickRequired, Fetcher, SwrOptions, + HookFetcherOptions, + HookFetcherFn, } from './types' import defineProperty from './define-property' import { CommerceError } from './errors' @@ -14,13 +14,12 @@ export type ResponseState = responseInterface & { isLoading: boolean } -export type UseData = < - Data = any, - Input extends { [k: string]: unknown } = {}, - FetchInput extends HookFetchInput = {} ->( - options: PickRequired, 'fetcher'>, - input: HookFetchInput | HookSwrInput, +export type UseData = ( + options: { + fetchOptions: HookFetcherOptions + fetcher: HookFetcherFn + }, + input: HookFetchInput | HookSWRInput, fetcherFn: Fetcher, swrOptions?: SwrOptions ) => ResponseState diff --git a/framework/commerce/utils/use-hook.ts b/framework/commerce/utils/use-hook.ts new file mode 100644 index 000000000..da3431e3c --- /dev/null +++ b/framework/commerce/utils/use-hook.ts @@ -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 | SWRHook +>(fn: (provider: P) => H) { + const { providerRef } = useCommerce

    () + const provider = providerRef.current + return fn(provider) +} + +export function useSWRHook>( + hook: PickRequired +) { + const fetcher = useFetcher() + + return hook.useHook({ + useData(ctx) { + const response = useData(hook, ctx?.input ?? [], fetcher, ctx?.swrOptions) + return response + }, + }) +} + +export function useMutationHook>( + hook: PickRequired +) { + const fetcher = useFetcher() + + return hook.useHook({ + fetch: useCallback( + ({ input } = {}) => { + return hook.fetcher({ + input, + options: hook.fetchOptions, + fetch: fetcher, + }) + }, + [fetcher, hook.fetchOptions] + ), + }) +} diff --git a/framework/commerce/utils/use-response.tsx b/framework/commerce/utils/use-response.tsx deleted file mode 100644 index de1b5088c..000000000 --- a/framework/commerce/utils/use-response.tsx +++ /dev/null @@ -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 -> = { - descriptors?: PropertyDescriptorMap - normalizer?: (data: R['data']) => D -} - -export type UseResponse = >( - response: R, - options: UseResponseOptions -) => D extends object ? Override : 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 diff --git a/framework/commerce/wishlist/use-add-item.tsx b/framework/commerce/wishlist/use-add-item.tsx index d9b513694..11c8cc241 100644 --- a/framework/commerce/wishlist/use-add-item.tsx +++ b/framework/commerce/wishlist/use-add-item.tsx @@ -1,12 +1,19 @@ -import useAction from '../utils/use-action' -import type { CartItemBody } from '../types' +import { useHook, useMutationHook } from '../utils/use-hook' +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 interface AddItemInput { -// includeProducts?: boolean -// } -export type AddItemInput = T +export type UseAddItem< + H extends MutationHook = MutationHook +> = ReturnType -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 diff --git a/framework/commerce/wishlist/use-remove-item.tsx b/framework/commerce/wishlist/use-remove-item.tsx index dfa60c363..c8c34a5af 100644 --- a/framework/commerce/wishlist/use-remove-item.tsx +++ b/framework/commerce/wishlist/use-remove-item.tsx @@ -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 = MutationHook< + any | null, + { wishlist?: any }, + RemoveItemInput, + {} + > +> = ReturnType + +export const fetcher: HookFetcherFn = mutationFetcher + +const fn = (provider: Provider) => provider.wishlist?.useRemoveItem! + +const useRemoveItem: UseRemoveItem = (input) => { + const hook = useHook(fn) + return useMutationHook({ fetcher, ...hook })(input) +} export default useRemoveItem diff --git a/framework/commerce/wishlist/use-wishlist.tsx b/framework/commerce/wishlist/use-wishlist.tsx index dc912bc98..7a93b20b1 100644 --- a/framework/commerce/wishlist/use-wishlist.tsx +++ b/framework/commerce/wishlist/use-wishlist.tsx @@ -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 { - Prop, - HookFetcherFn, - UseHookInput, - UseHookResponse, -} from '../utils/types' -import defaultFetcher from '../utils/default-fetcher' -import useData from '../utils/use-data' -import { Provider, useCommerce } from '..' +import type { Provider } from '..' -export type UseWishlistHandler

    = Prop< - Prop, - 'useWishlist' -> +export type UseWishlist< + H extends SWRHook = SWRHook< + Wishlist | null, + { includeProducts?: boolean }, + { customerId?: number; includeProducts: boolean }, + { isEmpty?: boolean } + > +> = ReturnType -export type UseWishlistInput

    = UseHookInput< - UseWishlistHandler

    -> +export const fetcher: HookFetcherFn = SWRFetcher -export type WishlistResponse

    = UseHookResponse< - UseWishlistHandler

    -> +const fn = (provider: Provider) => provider.wishlist?.useWishlist! -export type UseWishlist

    = Partial< - UseWishlistInput

    -> extends UseWishlistInput

    - ? (input?: UseWishlistInput

    ) => WishlistResponse

    - : (input: UseWishlistInput

    ) => WishlistResponse

    - -export const fetcher = defaultFetcher as HookFetcherFn - -export default function useWishlist

    ( - input: UseWishlistInput

    = {} -) { - const { providerRef, fetcherRef } = useCommerce

    () - - 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 - }, - }) +const useWishlist: UseWishlist = (input) => { + const hook = useHook(fn) + return useSWRHook({ fetcher, ...hook })(input) } + +export default useWishlist diff --git a/package.json b/package.json index ff492a35e..2d8e32772 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@reach/portal": "^0.11.2", - "@tailwindcss/ui": "^0.6.2", + "@types/lodash.memoize": "^4.1.6", "@vercel/fetch": "^6.1.0", "body-scroll-lock": "^3.1.5", "bowser": "^2.11.0", @@ -33,20 +33,21 @@ "js-cookie": "^2.2.1", "keen-slider": "^5.2.4", "lodash.debounce": "^4.0.8", + "lodash.memoize": "^4.1.2", "lodash.random": "^3.2.0", "lodash.throttle": "^4.1.1", - "next": "^10.0.5", + "next": "^10.0.7-canary.3", "next-seo": "^4.11.0", "next-themes": "^0.0.4", - "normalizr": "^3.6.1", + "postcss": "^8.2.4", "postcss-nesting": "^7.0.1", - "react": "^16.14.0", - "react-dom": "^16.14.0", + "react": "^17.0.1", + "react-dom": "^17.0.1", "react-merge-refs": "^1.1.0", "react-ticker": "^1.2.2", "swr": "^0.4.0", "tabbable": "^5.1.5", - "tailwindcss": "^1.9" + "tailwindcss": "^2.0.2" }, "devDependencies": { "@graphql-codegen/cli": "^1.20.0", @@ -56,8 +57,6 @@ "@manifoldco/swagger-to-ts": "^2.1.0", "@next/bundle-analyzer": "^10.0.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/cookie": "^0.4.0", "@types/js-cookie": "^2.2.6", @@ -66,8 +65,6 @@ "@types/lodash.throttle": "^4.1.6", "@types/node": "^14.14.16", "@types/react": "^17.0.0", - "bunyan": "^1.8.14", - "bunyan-prettystream": "^0.1.3", "graphql": "^15.4.0", "husky": "^4.3.8", "lint-staged": "^10.5.3", diff --git a/pages/_app.tsx b/pages/_app.tsx index 132ce5f18..dae0311b4 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,12 +1,11 @@ import '@assets/main.css' -import 'keen-slider/keen-slider.min.css' import '@assets/chrome-bug.css' +import 'keen-slider/keen-slider.min.css' import { FC, useEffect } from 'react' import type { AppProps } from 'next/app' - -import { ManagedUIContext } from '@components/ui/context' import { Head } from '@components/common' +import { ManagedUIContext } from '@components/ui/context' const Noop: FC = ({ children }) => <>{children} diff --git a/pages/index.tsx b/pages/index.tsx index 4ddf561aa..8f6ce5f2a 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,13 +1,14 @@ import { Layout } from '@components/common' import { Grid, Marquee, Hero } from '@components/ui' import { ProductCard } from '@components/product' -import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid' +// import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid' import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next' import { getConfig } from '@framework/api' import getAllProducts from '@framework/product/get-all-products' import getSiteInfo from '@framework/common/get-site-info' import getAllPages from '@framework/common/get-all-pages' +import Features from '@commerce/utils/features' export async function getStaticProps({ preview, @@ -23,6 +24,7 @@ export async function getStaticProps({ const { categories, brands } = await getSiteInfo({ config, preview }) const { pages } = await getAllPages({ config, preview }) + const isWishlistEnabled = Features.isEnabled('wishlist') return { props: { @@ -30,6 +32,9 @@ export async function getStaticProps({ categories, brands, pages, + commerceFeatures: { + wishlist: isWishlistEnabled, + }, }, revalidate: 1440, } @@ -39,6 +44,7 @@ export default function Home({ products, brands, categories, + commerceFeatures, }: InferGetStaticPropsType) { return ( <> @@ -51,6 +57,7 @@ export default function Home({ width: i === 0 ? 1080 : 540, height: i === 0 ? 1080 : 540, }} + wishlist={commerceFeatures.wishlist} /> ))} @@ -64,6 +71,7 @@ export default function Home({ width: 320, height: 320, }} + wishlist={commerceFeatures.wishlist} /> ))} @@ -86,6 +94,7 @@ export default function Home({ width: i === 0 ? 1080 : 540, height: i === 0 ? 1080 : 540, }} + wishlist={commerceFeatures.wishlist} /> ))} @@ -99,6 +108,7 @@ export default function Home({ width: 320, height: 320, }} + wishlist={commerceFeatures.wishlist} /> ))} diff --git a/pages/orders.tsx b/pages/orders.tsx index 08e32c2b2..db4ab55b2 100644 --- a/pages/orders.tsx +++ b/pages/orders.tsx @@ -1,9 +1,9 @@ import type { GetStaticPropsContext } from 'next' -import { getConfig } from '@framework/api' -import getAllPages from '@framework/common/get-all-pages' +import { Bag } from '@components/icons' import { Layout } from '@components/common' 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({ preview, diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index 83aeaa54c..a705c001b 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -11,14 +11,15 @@ import { getConfig } from '@framework/api' import getProduct from '@framework/product/get-product' import getAllPages from '@framework/common/get-all-pages' import getAllProductPaths from '@framework/product/get-all-product-paths' +import Features from '@commerce/utils/features' export async function getStaticProps({ params, locale, preview, }: GetStaticPropsContext<{ slug: string }>) { + const isWishlistEnabled = Features.isEnabled('wishlist') const config = getConfig({ locale }) - const { pages } = await getAllPages({ config, preview }) const { product } = await getProduct({ variables: { slug: params!.slug }, @@ -31,7 +32,13 @@ export async function getStaticProps({ } return { - props: { pages, product }, + props: { + pages, + product, + commerceFeatures: { + wishlist: isWishlistEnabled, + }, + }, revalidate: 200, } } @@ -55,13 +62,17 @@ export async function getStaticPaths({ locales }: GetStaticPathsContext) { export default function Slug({ product, + commerceFeatures, }: InferGetStaticPropsType) { const router = useRouter() return router.isFallback ? (

    Loading...

    // TODO (BC) Add Skeleton Views ) : ( - + ) } diff --git a/pages/search.tsx b/pages/search.tsx index 97bee34d4..7c0a4e140 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -26,6 +26,8 @@ const SORT = Object.entries({ 'price-desc': 'Price: High to low', }) +import Features from '@commerce/utils/features' + import { filterQuery, getCategoryPath, @@ -40,14 +42,23 @@ export async function getStaticProps({ const config = getConfig({ locale }) const { pages } = await getAllPages({ config, preview }) const { categories, brands } = await getSiteInfo({ config, preview }) + const isWishlistEnabled = Features.isEnabled('wishlist') return { - props: { pages, categories, brands }, + props: { + pages, + categories, + brands, + commerceFeatures: { + wishlist: isWishlistEnabled, + }, + }, } } export default function Search({ categories, brands, + commerceFeatures: { wishlist }, }: InferGetStaticPropsType) { const [activeFilter, setActiveFilter] = useState('') const [toggleFilter, setToggleFilter] = useState(false) @@ -337,7 +348,7 @@ export default function Search({ {data ? ( - {data.products.map((product) => ( + {data.products.map((product: Product) => ( ))} diff --git a/pages/wishlist.tsx b/pages/wishlist.tsx index 6de798411..ca11152f4 100644 --- a/pages/wishlist.tsx +++ b/pages/wishlist.tsx @@ -1,28 +1,43 @@ +import { useEffect } from 'react' +import { useRouter } from 'next/router' 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 { Layout } from '@components/common' import { Text, Container } from '@components/ui' -import { WishlistCard } from '@components/wishlist' import { defaultPageProps } from '@lib/defaults' +import { getConfig } from '@framework/api' 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({ preview, locale, }: GetStaticPropsContext) { + // Disabling page if Feature is not available + if (Features.isEnabled('wishlist')) { + return { + notFound: true, + } + } + const config = getConfig({ locale }) const { pages } = await getAllPages({ config, preview }) return { - props: { ...defaultPageProps, pages }, + props: { + pages, + ...defaultPageProps, + }, } } export default function Wishlist() { const { data: customer } = useCustomer() const { data, isLoading, isEmpty } = useWishlist() + const router = useRouter() return ( diff --git a/tsconfig.json b/tsconfig.json index 4b4389c44..7aec4729c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,6 @@ "@framework": ["framework/shopify"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], - "exclude": ["node_modules", "components/wishlist"] + "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], + "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index 3cdabc5b4..aad5af97d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1060,6 +1060,13 @@ dependencies: "@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": version "3.2.6" 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" 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: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"