From 1898f094bc9b87fb9f651c3ad56181e15f050705 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Wed, 3 Feb 2021 17:14:19 -0500 Subject: [PATCH 01/12] Minor change --- components/wishlist/WishlistCard/WishlistCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/wishlist/WishlistCard/WishlistCard.tsx b/components/wishlist/WishlistCard/WishlistCard.tsx index 82147f575..d1a9403b3 100644 --- a/components/wishlist/WishlistCard/WishlistCard.tsx +++ b/components/wishlist/WishlistCard/WishlistCard.tsx @@ -42,8 +42,8 @@ const WishlistCard: FC = ({ product }) => { setLoading(true) try { await addItem({ - productId: Number(product.id), - variantId: Number(product.variants[0].id), + productId: product.id, + variantId: product.variants[0].id, }) openSidebar() setLoading(false) From 2c9b8b100dff224fe7401cd8aa951d9c04c12d89 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Fri, 5 Feb 2021 17:44:10 -0500 Subject: [PATCH 02/12] MOving stuff around and adding temporal new files --- framework/bigcommerce/index.tsx | 63 +++++++++++++++++++- framework/commerce/cart/use-cart-2.tsx | 28 +++++++++ framework/commerce/cart/use-fake.tsx | 42 ++++++++++++++ framework/commerce/index.tsx | 53 ++++++++++++++--- framework/commerce/utils/types.ts | 7 +++ framework/commerce/utils/use-data-2.ts | 79 ++++++++++++++++++++++++++ 6 files changed, 261 insertions(+), 11 deletions(-) create mode 100644 framework/commerce/cart/use-cart-2.tsx create mode 100644 framework/commerce/cart/use-fake.tsx create mode 100644 framework/commerce/utils/use-data-2.ts diff --git a/framework/bigcommerce/index.tsx b/framework/bigcommerce/index.tsx index a4c9fffa5..268b0bd02 100644 --- a/framework/bigcommerce/index.tsx +++ b/framework/bigcommerce/index.tsx @@ -1,11 +1,16 @@ import { ReactNode } from 'react' import * as React from 'react' +import { Fetcher } from '@commerce/utils/types' import { CommerceConfig, CommerceProvider as CoreCommerceProvider, useCommerce as useCoreCommerce, + HookHandler, } from '@commerce' import { FetcherError } from '@commerce/utils/errors' +import type { CartInput } from '@commerce/cart/use-cart' +import { normalizeCart } from './lib/normalize' +import { Cart } from './types' async function getText(res: Response) { try { @@ -23,6 +28,57 @@ async function getError(res: Response) { return new FetcherError({ message: await getText(res), status: res.status }) } +const fetcher: Fetcher = async ({ + url, + method = 'GET', + variables, + body: bodyObj, +}) => { + const hasBody = Boolean(variables || bodyObj) + const body = hasBody + ? JSON.stringify(variables ? { variables } : bodyObj) + : undefined + const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined + const res = await fetch(url!, { method, body, headers }) + + if (res.ok) { + const { data } = await res.json() + return data + } + + throw await getError(res) +} + +const useCart: HookHandler = { + fetchOptions: { + url: '/api/bigcommerce/cart', + method: 'GET', + }, + fetcher(context) { + return undefined as any + }, + onResponse(response) { + return Object.create(response, { + isEmpty: { + get() { + return (response.data?.lineItems.length ?? 0) <= 0 + }, + enumerable: true, + }, + }) + }, +} + +export const bigcommerceProvider = { + locale: 'en-us', + cartCookie: 'bc_cartId', + fetcher, + cartNormalizer: normalizeCart, + cart: { useCart }, +} + +export type BigcommerceProvider = typeof bigcommerceProvider + export const bigcommerceConfig: CommerceConfig = { locale: 'en-us', cartCookie: 'bc_cartId', @@ -52,10 +108,13 @@ export type BigcommerceProps = { export function CommerceProvider({ children, ...config }: BigcommerceProps) { return ( - + {children} ) } -export const useCommerce = () => useCoreCommerce() +export const useCommerce = () => useCoreCommerce() diff --git a/framework/commerce/cart/use-cart-2.tsx b/framework/commerce/cart/use-cart-2.tsx new file mode 100644 index 000000000..cfeb85c87 --- /dev/null +++ b/framework/commerce/cart/use-cart-2.tsx @@ -0,0 +1,28 @@ +import Cookies from 'js-cookie' +import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types' +import useData, { ResponseState, SwrOptions } from '../utils/use-data' +import type { Cart } from '../types' +import { useCommerce } from '..' + +export type CartResponse = ResponseState & { isEmpty?: boolean } + +// Input expected by the `useCart` hook +export type CartInput = { + cartId?: Cart['id'] +} + +export default function useCart( + options: HookFetcherOptions, + input: HookInput, + fetcherFn: HookFetcher, + swrOptions?: SwrOptions +): CartResponse { + const { providerRef, cartCookie } = useCommerce() + const fetcher: typeof fetcherFn = (options, input, fetch) => { + input.cartId = Cookies.get(cartCookie) + return fetcherFn(options, input, fetch) + } + const response = useData(options, input, fetcher, swrOptions) + + return response +} diff --git a/framework/commerce/cart/use-fake.tsx b/framework/commerce/cart/use-fake.tsx new file mode 100644 index 000000000..324e1c4d9 --- /dev/null +++ b/framework/commerce/cart/use-fake.tsx @@ -0,0 +1,42 @@ +import { useMemo } from 'react' +import Cookies from 'js-cookie' +import type { Cart } from '../types' +import type { HookFetcherFn } from '../utils/types' +import useData from '../utils/use-data-2' +import { Provider, useCommerce } from '..' + +// Input expected by the `useCart` hook +export type CartInput = { + cartId?: Cart['id'] +} + +const fetcher: HookFetcherFn = async ({ + options, + input: { cartId }, + fetch, + normalize, +}) => { + const data = cartId ? await fetch({ ...options }) : null + return data && normalize ? normalize(data) : data +} + +export default function useFake

() { + const { providerRef, cartCookie } = useCommerce

() + + const provider = providerRef.current + const opts = provider.cart?.useCart + const options = opts?.fetchOptions ?? {} + const fetcherFn = opts?.fetcher ?? fetcher + const wrapper: typeof fetcher = (context) => { + context.input.cartId = Cookies.get(cartCookie) + return fetcherFn(context) + } + + const response = useData(options, [], wrapper, opts?.swrOptions) + const memoizedResponse = useMemo( + () => (opts?.onResponse ? opts.onResponse(response) : response), + [response] + ) + + return memoizedResponse +} diff --git a/framework/commerce/index.tsx b/framework/commerce/index.tsx index 506502ea2..91a1be2e0 100644 --- a/framework/commerce/index.tsx +++ b/framework/commerce/index.tsx @@ -7,36 +7,71 @@ import { useRef, } from 'react' import * as React from 'react' -import { Fetcher } from './utils/types' +import { Fetcher, HookFetcherFn, HookFetcherOptions } from './utils/types' +import { Cart } from './types' +import type { ResponseState, SwrOptions } from './utils/use-data' +import type { CartInput } from './cart/use-cart' -const Commerce = createContext({}) +const Commerce = createContext | {}>({}) -export type CommerceProps = { +export type Provider = CommerceConfig & { + cart?: { + useCart?: HookHandler + } + cartNormalizer(data: any): Cart +} + +export type HookHandler = { + swrOptions?: SwrOptions + onResponse?(response: ResponseState): ResponseState + onMutation?: any + fetchOptions?: HookFetcherOptions +} & ( + | // TODO: Maybe the normalizer is not required if it's used by the API route directly? + { + fetcher: HookFetcherFn + normalizer?(data: Result): Data + } + | { + fetcher?: never + normalizer(data: Result): Data + } +) + +export type CommerceProps

= { children?: ReactNode + provider: P config: CommerceConfig } export type CommerceConfig = { fetcher: Fetcher } & Omit< - CommerceContextValue, - 'fetcherRef' + CommerceContextValue, + 'providerRef' | 'fetcherRef' > -export type CommerceContextValue = { +export type CommerceContextValue

= { + providerRef: MutableRefObject

fetcherRef: MutableRefObject> locale: string cartCookie: string } -export function CommerceProvider({ children, config }: CommerceProps) { +export function CommerceProvider

({ + provider, + children, + config, +}: CommerceProps

) { if (!config) { throw new Error('CommerceProvider requires a valid config object') } + const providerRef = useRef(provider) const fetcherRef = useRef(config.fetcher) // Because the config is an object, if the parent re-renders this provider // will re-render every consumer unless we memoize the config const cfg = useMemo( () => ({ + providerRef, fetcherRef, locale: config.locale, cartCookie: config.cartCookie, @@ -47,6 +82,6 @@ export function CommerceProvider({ children, config }: CommerceProps) { return {children} } -export function useCommerce() { - return useContext(Commerce) as T +export function useCommerce

() { + return useContext(Commerce) as CommerceContextValue

} diff --git a/framework/commerce/utils/types.ts b/framework/commerce/utils/types.ts index 010205f62..3428b5194 100644 --- a/framework/commerce/utils/types.ts +++ b/framework/commerce/utils/types.ts @@ -15,6 +15,13 @@ export type HookFetcher = ( fetch: (options: FetcherOptions) => Promise ) => Data | Promise +export type HookFetcherFn = (context: { + options: HookFetcherOptions | null + input: Input + fetch: (options: FetcherOptions) => Promise + normalize?(data: Result): Data +}) => Data | Promise + export type HookFetcherOptions = { query?: string url?: string diff --git a/framework/commerce/utils/use-data-2.ts b/framework/commerce/utils/use-data-2.ts new file mode 100644 index 000000000..69eb223b0 --- /dev/null +++ b/framework/commerce/utils/use-data-2.ts @@ -0,0 +1,79 @@ +import useSWR, { ConfigInterface, responseInterface } from 'swr' +import type { + HookInput, + HookFetcher, + HookFetcherOptions, + HookFetcherFn, +} from './types' +import defineProperty from './define-property' +import { CommerceError } from './errors' +import { useCommerce } from '..' + +export type SwrOptions = ConfigInterface< + Data, + CommerceError, + HookFetcher +> + +export type ResponseState = responseInterface & { + isLoading: boolean +} + +export type UseData = ( + options: HookFetcherOptions | (() => HookFetcherOptions | null), + input: HookInput, + fetcherFn: HookFetcherFn, + swrOptions?: SwrOptions +) => ResponseState + +const useData: UseData = (options, input, fetcherFn, swrOptions) => { + const { fetcherRef } = useCommerce() + const fetcher = async ( + url?: string, + query?: string, + method?: string, + ...args: any[] + ) => { + try { + return await fetcherFn({ + options: { url, query, method }, + // Transform the input array into an object + input: args.reduce((obj, val, i) => { + obj[input[i][0]!] = val + return obj + }, {}), + fetch: fetcherRef.current, + }) + } catch (error) { + // SWR will not log errors, but any error that's not an instance + // of CommerceError is not welcomed by this hook + if (!(error instanceof CommerceError)) { + console.error(error) + } + throw error + } + } + const response = useSWR( + () => { + const opts = typeof options === 'function' ? options() : options + return opts + ? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])] + : null + }, + fetcher, + swrOptions + ) + + if (!('isLoading' in response)) { + defineProperty(response, 'isLoading', { + get() { + return response.data === undefined + }, + enumerable: true, + }) + } + + return response +} + +export default useData From aab2e7f7cc97040daa05bdd4124125d0f6b5c7c0 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Mon, 8 Feb 2021 10:52:35 -0500 Subject: [PATCH 03/12] Replace use-cart with the new hook --- components/common/UserNav/UserNav.tsx | 7 +++- framework/bigcommerce/cart/use-cart.tsx | 54 ++----------------------- framework/bigcommerce/index.tsx | 10 +++-- framework/commerce/cart/use-cart-2.tsx | 28 ------------- framework/commerce/cart/use-cart.tsx | 54 ++++++++++++++++--------- framework/commerce/cart/use-fake.tsx | 9 +++-- framework/commerce/index.tsx | 6 ++- framework/commerce/utils/use-data-2.ts | 7 ++-- 8 files changed, 66 insertions(+), 109 deletions(-) delete mode 100644 framework/commerce/cart/use-cart-2.tsx diff --git a/components/common/UserNav/UserNav.tsx b/components/common/UserNav/UserNav.tsx index f8e6373d9..7048cc468 100644 --- a/components/common/UserNav/UserNav.tsx +++ b/components/common/UserNav/UserNav.tsx @@ -1,7 +1,10 @@ import { FC } from 'react' import Link from 'next/link' import cn from 'classnames' +import type { BigcommerceProvider } from '@framework' +import { LineItem } from '@framework/types' import useCart from '@framework/cart/use-cart' +import useFake from '@commerce/cart/use-fake' import useCustomer from '@framework/customer/use-customer' import { Heart, Bag } from '@components/icons' import { useUI } from '@components/ui/context' @@ -15,12 +18,14 @@ interface Props { const countItem = (count: number, item: LineItem) => count + item.quantity -const UserNav: FC = ({ className, children }) => { +const UserNav: FC = ({ className }) => { const { data } = useCart() const { data: customer } = useCustomer() const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI() const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0 + const x = useFake() + return (