From e1d90b3ee0d9c3087dc6b0127d8ac9ababd7d9b1 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 29 Oct 2020 12:51:53 -0300 Subject: [PATCH] Setting up the right data hooks --- .../cart/CartSidebarView/CartSidebarView.tsx | 2 +- components/commerce/context.tsx | 41 +++++++++++++ components/commerce/errors.ts | 40 +++++++++++++ components/commerce/hooks/useCart.ts | 31 ++++++++++ components/commerce/hooks/useCommerce.ts | 10 ++++ components/commerce/hooks/useData.ts | 60 +++++++++++++++++++ components/commerce/index.ts | 1 + components/commerce/providersMap.ts | 7 +++ components/commerce/types.ts | 23 +++++++ components/common/Footer/Footer.tsx | 2 +- components/common/Layout/Layout.tsx | 2 +- components/common/Navbar/Navbar.tsx | 2 +- components/common/UserNav/UserNav.tsx | 2 +- .../product/ProductCard/ProductCard.tsx | 2 +- .../product/ProductView/ProductView.tsx | 2 +- .../wishlist/WishlistCard/WishlistCard.tsx | 2 +- hooks/index.ts | 1 - hooks/useCart.ts | 0 package.json | 3 + pages/[...pages].tsx | 2 +- pages/_app.tsx | 2 +- pages/blog.tsx | 2 +- pages/cart.tsx | 2 +- pages/index.tsx | 4 +- pages/orders.tsx | 2 +- pages/product/[slug].tsx | 2 +- pages/profile.tsx | 2 +- pages/search.tsx | 2 +- pages/wishlist.tsx | 2 +- yarn.lock | 14 ++++- 30 files changed, 248 insertions(+), 21 deletions(-) create mode 100644 components/commerce/context.tsx create mode 100644 components/commerce/errors.ts create mode 100644 components/commerce/hooks/useCart.ts create mode 100644 components/commerce/hooks/useCommerce.ts create mode 100644 components/commerce/hooks/useData.ts create mode 100644 components/commerce/index.ts create mode 100644 components/commerce/providersMap.ts create mode 100644 components/commerce/types.ts delete mode 100644 hooks/index.ts delete mode 100644 hooks/useCart.ts diff --git a/components/cart/CartSidebarView/CartSidebarView.tsx b/components/cart/CartSidebarView/CartSidebarView.tsx index 3f86cb0e7..0ad7b95f1 100644 --- a/components/cart/CartSidebarView/CartSidebarView.tsx +++ b/components/cart/CartSidebarView/CartSidebarView.tsx @@ -1,6 +1,6 @@ import { FC } from 'react' import cn from 'classnames' -import { UserNav } from '@components/core' +import { UserNav } from '@components/common' import { Button } from '@components/ui' import { Bag, Cross, Check } from '@components/icons' import { useUI } from '@components/ui/context' diff --git a/components/commerce/context.tsx b/components/commerce/context.tsx new file mode 100644 index 000000000..f70a88058 --- /dev/null +++ b/components/commerce/context.tsx @@ -0,0 +1,41 @@ +import React, { ReactNode, FC, useRef, useMemo, MutableRefObject } from 'react' +import { Fetcher } from './types' + +export interface State { + fetcherRef: MutableRefObject> + locale: string + cartCookie: string +} + +export type CommerceProps = { + children: ReactNode + config: CommerceConfig +} + +export type CommerceConfig = { fetcher: Fetcher } & Omit< + State, + 'fetcherRef' +> + +const initialState = {} + +export const CommerceContext = React.createContext(initialState) +CommerceContext.displayName = 'CommerceContext' + +export const CommerceProvider: FC = ({ children, config }) => { + 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( + () => ({ + fetcherRef, + locale: config.locale, + cartCookie: config.cartCookie, + }), + [config.locale, config.cartCookie] + ) + + return ( + {children} + ) +} diff --git a/components/commerce/errors.ts b/components/commerce/errors.ts new file mode 100644 index 000000000..76f899ab7 --- /dev/null +++ b/components/commerce/errors.ts @@ -0,0 +1,40 @@ +export type ErrorData = { + message: string + code?: string +} + +export type ErrorProps = { + code?: string +} & ( + | { message: string; errors?: never } + | { message?: never; errors: ErrorData[] } +) + +export class CommerceError extends Error { + code?: string + errors: ErrorData[] + + constructor({ message, code, errors }: ErrorProps) { + const error: ErrorData = message + ? { message, ...(code ? { code } : {}) } + : errors![0] + + super(error.message) + this.errors = message ? [error] : errors! + + if (error.code) this.code = error.code + } +} + +export class FetcherError extends CommerceError { + status: number + + constructor( + options: { + status: number + } & ErrorProps + ) { + super(options) + this.status = options.status + } +} diff --git a/components/commerce/hooks/useCart.ts b/components/commerce/hooks/useCart.ts new file mode 100644 index 000000000..017fe58cd --- /dev/null +++ b/components/commerce/hooks/useCart.ts @@ -0,0 +1,31 @@ +import type { responseInterface } from 'swr' +import Cookies from 'js-cookie' +import type { HookInput, HookFetcher, HookFetcherOptions } from '../types' +import useData, { SwrOptions } from './useData' +import { useCommerce } from './useCommerce' + +export type CartResponse = responseInterface & { + isEmpty: boolean +} + +export type CartInput = { + cartId: string | undefined +} + +export default function useCart( + options: HookFetcherOptions, + input: HookInput, + fetcherFn: HookFetcher, + swrOptions?: SwrOptions +) { + const { 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 Object.assign(response, { isEmpty: true }) as CartResponse +} diff --git a/components/commerce/hooks/useCommerce.ts b/components/commerce/hooks/useCommerce.ts new file mode 100644 index 000000000..985c536ba --- /dev/null +++ b/components/commerce/hooks/useCommerce.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react' +import { CommerceContext } from '../context' + +export const useCommerce = () => { + const context = useContext(CommerceContext) + if (context === undefined) { + throw new Error(`useCommerce must be used within a CommerceProvider`) + } + return context +} diff --git a/components/commerce/hooks/useData.ts b/components/commerce/hooks/useData.ts new file mode 100644 index 000000000..0667d8181 --- /dev/null +++ b/components/commerce/hooks/useData.ts @@ -0,0 +1,60 @@ +import useSWR, { ConfigInterface, responseInterface } from 'swr' +import type { HookInput, HookFetcher, HookFetcherOptions } from '../types' +import { CommerceError } from '../errors' +import { useCommerce } from './useCommerce' + +export type SwrOptions = ConfigInterface< + Result, + CommerceError, + HookFetcher +> + +export type UseData = ( + options: HookFetcherOptions | (() => HookFetcherOptions | null), + input: HookInput, + fetcherFn: HookFetcher, + swrOptions?: SwrOptions +) => responseInterface + +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( + { url, query, method }, + // Transform the input array into an object + args.reduce((obj, val, i) => { + obj[input[i][0]!] = val + return obj + }, {}), + 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 + ) + + return response +} + +export default useData diff --git a/components/commerce/index.ts b/components/commerce/index.ts new file mode 100644 index 000000000..ff381c2c1 --- /dev/null +++ b/components/commerce/index.ts @@ -0,0 +1 @@ +export { default as useCart } from './hooks/useCart' diff --git a/components/commerce/providersMap.ts b/components/commerce/providersMap.ts new file mode 100644 index 000000000..8096ba47f --- /dev/null +++ b/components/commerce/providersMap.ts @@ -0,0 +1,7 @@ +const bigcommerce = { + cart: { + url: '/api/bigcommerce/cart', + }, +} + +export { bigcommerce } diff --git a/components/commerce/types.ts b/components/commerce/types.ts new file mode 100644 index 000000000..c0e6d3b16 --- /dev/null +++ b/components/commerce/types.ts @@ -0,0 +1,23 @@ +export type Fetcher = (options: FetcherOptions) => T | Promise + +export type FetcherOptions = { + url?: string + query?: string + method?: string + variables?: any + body?: any +} + +export type HookFetcher = ( + options: HookFetcherOptions | null, + input: Input, + fetch: (options: FetcherOptions) => Promise +) => Result | Promise + +export type HookFetcherOptions = { + query?: string + url?: string + method?: string +} + +export type HookInput = [string, string | number | boolean | undefined][] diff --git a/components/common/Footer/Footer.tsx b/components/common/Footer/Footer.tsx index 2de87ace8..abc1ed0e8 100644 --- a/components/common/Footer/Footer.tsx +++ b/components/common/Footer/Footer.tsx @@ -6,7 +6,7 @@ import type { Page } from '@bigcommerce/storefront-data-hooks/api/operations/get import getSlug from '@lib/get-slug' import { Github } from '@components/icons' import { Logo, Container } from '@components/ui' -import { I18nWidget } from '@components/core' +import { I18nWidget } from '@components/common' import s from './Footer.module.css' interface Props { diff --git a/components/common/Layout/Layout.tsx b/components/common/Layout/Layout.tsx index 9ac0a74ed..a3a5f93ec 100644 --- a/components/common/Layout/Layout.tsx +++ b/components/common/Layout/Layout.tsx @@ -5,7 +5,7 @@ import type { Page } from '@bigcommerce/storefront-data-hooks/api/operations/get import { CommerceProvider } from '@bigcommerce/storefront-data-hooks' import { CartSidebarView } from '@components/cart' import { Container, Sidebar, Button, Modal, Toast } from '@components/ui' -import { Navbar, Featurebar, Footer } from '@components/core' +import { Navbar, Featurebar, Footer } from '@components/common' import { LoginView, SignUpView, ForgotPassword } from '@components/auth' import { useUI } from '@components/ui/context' import { usePreventScroll } from '@react-aria/overlays' diff --git a/components/common/Navbar/Navbar.tsx b/components/common/Navbar/Navbar.tsx index 70bb125f5..28b3f8910 100644 --- a/components/common/Navbar/Navbar.tsx +++ b/components/common/Navbar/Navbar.tsx @@ -2,7 +2,7 @@ import { FC } from 'react' import Link from 'next/link' import s from './Navbar.module.css' import { Logo } from '@components/ui' -import { Searchbar, UserNav } from '@components/core' +import { Searchbar, UserNav } from '@components/common' interface Props { className?: string } diff --git a/components/common/UserNav/UserNav.tsx b/components/common/UserNav/UserNav.tsx index b8c11ddba..b5f79233e 100644 --- a/components/common/UserNav/UserNav.tsx +++ b/components/common/UserNav/UserNav.tsx @@ -5,7 +5,7 @@ import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart' import useCustomer from '@bigcommerce/storefront-data-hooks/use-customer' import { Menu } from '@headlessui/react' import { Heart, Bag } from '@components/icons' -import { Avatar } from '@components/core' +import { Avatar } from '@components/common' import { useUI } from '@components/ui/context' import DropdownMenu from './DropdownMenu' import s from './UserNav.module.css' diff --git a/components/product/ProductCard/ProductCard.tsx b/components/product/ProductCard/ProductCard.tsx index f047136b4..c3f889f9e 100644 --- a/components/product/ProductCard/ProductCard.tsx +++ b/components/product/ProductCard/ProductCard.tsx @@ -3,7 +3,7 @@ import cn from 'classnames' import Link from 'next/link' import type { ProductNode } from '@bigcommerce/storefront-data-hooks/api/operations/get-all-products' import usePrice from '@bigcommerce/storefront-data-hooks/use-price' -import { EnhancedImage } from '@components/core' +import { EnhancedImage } from '@components/common' import s from './ProductCard.module.css' import WishlistButton from '@components/wishlist/WishlistButton' diff --git a/components/product/ProductView/ProductView.tsx b/components/product/ProductView/ProductView.tsx index 57df704c7..6a5cc922a 100644 --- a/components/product/ProductView/ProductView.tsx +++ b/components/product/ProductView/ProductView.tsx @@ -7,7 +7,7 @@ import s from './ProductView.module.css' import { useUI } from '@components/ui/context' import { Swatch, ProductSlider } from '@components/product' import { Button, Container } from '@components/ui' -import { HTMLContent } from '@components/core' +import { HTMLContent } from '@components/common' import useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item' import type { ProductNode } from '@bigcommerce/storefront-data-hooks/api/operations/get-product' diff --git a/components/wishlist/WishlistCard/WishlistCard.tsx b/components/wishlist/WishlistCard/WishlistCard.tsx index 44054d674..e0601862b 100644 --- a/components/wishlist/WishlistCard/WishlistCard.tsx +++ b/components/wishlist/WishlistCard/WishlistCard.tsx @@ -8,7 +8,7 @@ import useRemoveItem from '@bigcommerce/storefront-data-hooks/wishlist/use-remov import useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item' import { useUI } from '@components/ui/context' import { Button } from '@components/ui' -import { HTMLContent } from '@components/core' +import { HTMLContent } from '@components/common' import { Trash } from '@components/icons' import s from './WishlistCard.module.css' diff --git a/hooks/index.ts b/hooks/index.ts deleted file mode 100644 index 68ee6c61e..000000000 --- a/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as useCart } from './useCart' diff --git a/hooks/useCart.ts b/hooks/useCart.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/package.json b/package.json index 1d2bce171..2731258c9 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,13 @@ "@next/bundle-analyzer": "^9.5.5", "@react-aria/overlays": "^3.4.0", "@tailwindcss/ui": "^0.6.2", + "@types/js-cookie": "^2.2.6", "@vercel/fetch": "^6.1.0", "bowser": "^2.11.0", "classnames": "^2.2.6", "email-validator": "^2.0.4", "intersection-observer": "^0.11.0", + "js-cookie": "^2.2.1", "keen-slider": "^5.2.4", "lodash.debounce": "^4.0.8", "lodash.random": "^3.2.0", @@ -36,6 +38,7 @@ "react-intersection-observer": "^8.29.1", "react-merge-refs": "^1.1.0", "react-ticker": "^1.2.2", + "swr": "^0.3.7", "tailwindcss": "^1.9" }, "devDependencies": { diff --git a/pages/[...pages].tsx b/pages/[...pages].tsx index ad67d581a..9ebd77149 100644 --- a/pages/[...pages].tsx +++ b/pages/[...pages].tsx @@ -3,7 +3,7 @@ import { getConfig } from '@bigcommerce/storefront-data-hooks/api' import getPage from '@bigcommerce/storefront-data-hooks/api/operations/get-page' import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages' import getSlug from '@lib/get-slug' -import { Layout, HTMLContent } from '@components/core' +import { Layout, HTMLContent } from '@components/common' export async function getStaticProps({ preview, diff --git a/pages/_app.tsx b/pages/_app.tsx index 69f3ce223..dabaaf8d0 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -5,7 +5,7 @@ import { FC } from 'react' import type { AppProps } from 'next/app' import { ManagedUIContext } from '@components/ui/context' -import { Head } from '@components/core' +import { Head } from '@components/common' const Noop: FC = ({ children }) => <>{children} diff --git a/pages/blog.tsx b/pages/blog.tsx index 905c7e2b6..3e779ac25 100644 --- a/pages/blog.tsx +++ b/pages/blog.tsx @@ -1,7 +1,7 @@ import type { GetStaticPropsContext } from 'next' import { getConfig } from '@bigcommerce/storefront-data-hooks/api' import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages' -import { Layout } from '@components/core' +import { Layout } from '@components/common' import { Container } from '@components/ui' export async function getStaticProps({ diff --git a/pages/cart.tsx b/pages/cart.tsx index 8666b92c9..13c8df2e4 100644 --- a/pages/cart.tsx +++ b/pages/cart.tsx @@ -3,7 +3,7 @@ import { getConfig } from '@bigcommerce/storefront-data-hooks/api' import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages' import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart' import usePrice from '@bigcommerce/storefront-data-hooks/use-price' -import { Layout } from '@components/core' +import { Layout } from '@components/common' import { Button } from '@components/ui' import { Bag, Cross, Check } from '@components/icons' import { CartItem } from '@components/cart' diff --git a/pages/index.tsx b/pages/index.tsx index 0b838d84c..9145d9294 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,10 +1,10 @@ import { useMemo } from 'react' import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next' import rangeMap from '@lib/range-map' -import { Layout } from '@components/core' +import { Layout } from '@components/common' import { Grid, Marquee, Hero } from '@components/ui' import { ProductCard } from '@components/product' -import HomeAllProductsGrid from '@components/core/HomeAllProductsGrid' +import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid' import { getConfig } from '@bigcommerce/storefront-data-hooks/api' import getAllProducts from '@bigcommerce/storefront-data-hooks/api/operations/get-all-products' diff --git a/pages/orders.tsx b/pages/orders.tsx index a6c4cf2c2..4fec58726 100644 --- a/pages/orders.tsx +++ b/pages/orders.tsx @@ -1,7 +1,7 @@ import type { GetStaticPropsContext } from 'next' import { getConfig } from '@bigcommerce/storefront-data-hooks/api' import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages' -import { Layout } from '@components/core' +import { Layout } from '@components/common' import { Container, Text } from '@components/ui' import { Bag } from '@components/icons' diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index 2432c6c35..0348bdf45 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -4,7 +4,7 @@ import type { InferGetStaticPropsType, } from 'next' import { useRouter } from 'next/router' -import { Layout } from '@components/core' +import { Layout } from '@components/common' import { ProductView } from '@components/product' // Data diff --git a/pages/profile.tsx b/pages/profile.tsx index c960df19e..1239ecc31 100644 --- a/pages/profile.tsx +++ b/pages/profile.tsx @@ -2,7 +2,7 @@ import type { GetStaticPropsContext } from 'next' import { getConfig } from '@bigcommerce/storefront-data-hooks/api' import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages' import useCustomer from '@bigcommerce/storefront-data-hooks/use-customer' -import { Layout } from '@components/core' +import { Layout } from '@components/common' import { Container, Text } from '@components/ui' export async function getStaticProps({ diff --git a/pages/search.tsx b/pages/search.tsx index bb892bdaf..2b272e265 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -6,7 +6,7 @@ import { getConfig } from '@bigcommerce/storefront-data-hooks/api' import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages' import getSiteInfo from '@bigcommerce/storefront-data-hooks/api/operations/get-site-info' import useSearch from '@bigcommerce/storefront-data-hooks/products/use-search' -import { Layout } from '@components/core' +import { Layout } from '@components/common' import { ProductCard } from '@components/product' import { Container, Grid, Skeleton } from '@components/ui' diff --git a/pages/wishlist.tsx b/pages/wishlist.tsx index a5c9ad59f..fbfa19373 100644 --- a/pages/wishlist.tsx +++ b/pages/wishlist.tsx @@ -2,7 +2,7 @@ import type { GetStaticPropsContext } from 'next' import { getConfig } from '@bigcommerce/storefront-data-hooks/api' import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages' import useWishlist from '@bigcommerce/storefront-data-hooks/wishlist/use-wishlist' -import { Layout } from '@components/core' +import { Layout } from '@components/common' import { Heart } from '@components/icons' import { Container, Text } from '@components/ui' import { WishlistCard } from '@components/wishlist' diff --git a/yarn.lock b/yarn.lock index bbd30ddc9..0362d4a8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1815,6 +1815,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884" integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g== +"@types/js-cookie@^2.2.6": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f" + integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== + "@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6": version "7.0.6" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" @@ -3884,7 +3889,7 @@ jest-worker@^26.6.1: merge-stream "^2.0.0" supports-color "^7.0.0" -js-cookie@2.2.1: +js-cookie@2.2.1, js-cookie@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== @@ -5850,6 +5855,13 @@ swr@0.3.6: dependencies: dequal "2.0.2" +swr@^0.3.7: + version "0.3.7" + resolved "https://registry.yarnpkg.com/swr/-/swr-0.3.7.tgz#331c704b135fd0f320f48a7355e11e624efb5cd0" + integrity sha512-/w7ZFBRpyWZJTOfl1OPsf93goeBPnfBXgJ7BBqqRGNidynqTO2sqinlLbXBbTNcNdoJwNJLVEjn2aDHJh5p1Kw== + dependencies: + dequal "2.0.2" + tailwindcss@^1.9: version "1.9.6" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.9.6.tgz#0c5089911d24e1e98e592a31bfdb3d8f34ecf1a0"