From 9309bff517baa6b3316831f75ce94cd424a65f4c Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Mon, 19 Oct 2020 12:40:26 -0500 Subject: [PATCH 1/8] Type fixes --- .../product/ProductCard/ProductCard.tsx | 18 ++++++----------- .../api/operations/get-all-products.ts | 6 ++++-- .../api/utils/fetch-graphql-api.ts | 2 +- pages/index.tsx | 20 +++++++++---------- pages/search.tsx | 5 +++-- 5 files changed, 24 insertions(+), 27 deletions(-) diff --git a/components/product/ProductCard/ProductCard.tsx b/components/product/ProductCard/ProductCard.tsx index 3d862e21e..30ffaffb1 100644 --- a/components/product/ProductCard/ProductCard.tsx +++ b/components/product/ProductCard/ProductCard.tsx @@ -1,30 +1,24 @@ import cn from 'classnames' import s from './ProductCard.module.css' import { FC, ReactNode, Component } from 'react' +import type { Product } from '@lib/bigcommerce/api/operations/get-all-products' import { Heart } from '@components/icon' import Link from 'next/link' interface Props { className?: string children?: ReactNode[] | Component[] | any[] - node: ProductData + product: Product['node'] variant?: 'slim' | 'simple' } -interface ProductData { - name: string - images: any - prices: any - path: string -} - -const ProductCard: FC = ({ className, node: p, variant }) => { +const ProductCard: FC = ({ className, product: p, variant }) => { if (variant === 'slim') { return (
@@ -41,7 +35,7 @@ const ProductCard: FC = ({ className, node: p, variant }) => {
@@ -50,7 +44,7 @@ const ProductCard: FC = ({ className, node: p, variant }) => {

{p.name}

- ${p.prices.price.value} + ${p.prices?.price.value}
diff --git a/lib/bigcommerce/api/operations/get-all-products.ts b/lib/bigcommerce/api/operations/get-all-products.ts index 5b3bd1f8b..39f7ba147 100644 --- a/lib/bigcommerce/api/operations/get-all-products.ts +++ b/lib/bigcommerce/api/operations/get-all-products.ts @@ -43,11 +43,13 @@ export const getAllProductsQuery = /* GraphQL */ ` ${productConnectionFragment} ` -export type Product = NonNullable< +export type ProductEdge = NonNullable< NonNullable[0] > -export type Products = Product[] +export type Product = ProductEdge + +export type Products = ProductEdge[] export type GetAllProductsResult< T extends Record = { products: Products } diff --git a/lib/bigcommerce/api/utils/fetch-graphql-api.ts b/lib/bigcommerce/api/utils/fetch-graphql-api.ts index 26246249c..aaf6e75ea 100644 --- a/lib/bigcommerce/api/utils/fetch-graphql-api.ts +++ b/lib/bigcommerce/api/utils/fetch-graphql-api.ts @@ -6,7 +6,7 @@ export default async function fetchGraphqlApi( query: string, { variables, preview }: CommerceAPIFetchOptions = {} ): Promise { - log.warn(query) + // log.warn(query) const config = getConfig() const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), { method: 'POST', diff --git a/pages/index.tsx b/pages/index.tsx index 253cec917..65afe747f 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -28,13 +28,13 @@ export default function Home({ return (
- {featuredProducts.map((p: any) => ( - + {featuredProducts.map(({ node }) => ( + ))} - {products.slice(0, 3).map((p: any) => ( - + {products.slice(0, 3).map(({ node }) => ( + ))} - {products.slice(3, 6).map((p: any) => ( - + {products.slice(3, 6).map(({ node }) => ( + ))} - {products.slice(0, 3).map((p: any) => ( - + {products.slice(0, 3).map(({ node }) => ( + ))}
@@ -84,8 +84,8 @@ export default function Home({
- {products.map((p: any) => ( - + {products.map(({ node }) => ( + ))}
diff --git a/pages/search.tsx b/pages/search.tsx index 7d0721f3f..391acf22e 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -141,10 +141,11 @@ export default function Search({ {data ? ( - {data.products.map((p: any) => ( + {data.products.map(({ node }) => ( ))} From f4bc27666bf4510fcaaa2926642a254ec90502ac Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Mon, 19 Oct 2020 12:52:09 -0500 Subject: [PATCH 2/8] Updated types --- .../product/ProductCard/ProductCard.tsx | 4 ++-- .../product/ProductView/ProductView.tsx | 6 +++--- components/product/helpers.ts | 4 ++-- .../api/catalog/handlers/get-products.ts | 20 ++++++++----------- lib/bigcommerce/api/catalog/products.ts | 4 ++-- .../api/operations/get-all-products.ts | 10 +++++----- lib/bigcommerce/api/operations/get-product.ts | 4 ++-- 7 files changed, 24 insertions(+), 28 deletions(-) diff --git a/components/product/ProductCard/ProductCard.tsx b/components/product/ProductCard/ProductCard.tsx index 30ffaffb1..cae070fbd 100644 --- a/components/product/ProductCard/ProductCard.tsx +++ b/components/product/ProductCard/ProductCard.tsx @@ -1,14 +1,14 @@ import cn from 'classnames' import s from './ProductCard.module.css' import { FC, ReactNode, Component } from 'react' -import type { Product } from '@lib/bigcommerce/api/operations/get-all-products' +import type { ProductNode } from '@lib/bigcommerce/api/operations/get-all-products' import { Heart } from '@components/icon' import Link from 'next/link' interface Props { className?: string children?: ReactNode[] | Component[] | any[] - product: Product['node'] + product: ProductNode variant?: 'slim' | 'simple' } diff --git a/components/product/ProductView/ProductView.tsx b/components/product/ProductView/ProductView.tsx index 71d874a25..a8acdbd39 100644 --- a/components/product/ProductView/ProductView.tsx +++ b/components/product/ProductView/ProductView.tsx @@ -1,18 +1,18 @@ import { NextSeo } from 'next-seo' import { FC, useState } from 'react' -import s from './ProductView.module.css' +import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product' import { Colors } from '@components/ui/types' import { useUI } from '@components/ui/context' import { Button, Container } from '@components/ui' import { Swatch, ProductSlider } from '@components/product' import useAddItem from '@lib/bigcommerce/cart/use-add-item' -import type { Product } from '@lib/bigcommerce/api/operations/get-product' import { getProductOptions } from '../helpers' +import s from './ProductView.module.css' interface Props { className?: string children?: any - product: Product + product: ProductNode } const ProductView: FC = ({ product, className }) => { diff --git a/components/product/helpers.ts b/components/product/helpers.ts index 8d40a74dd..f6ec90f22 100644 --- a/components/product/helpers.ts +++ b/components/product/helpers.ts @@ -1,6 +1,6 @@ -import type { Product } from '@lib/bigcommerce/api/operations/get-product' +import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product' -export function getProductOptions(product: Product) { +export function getProductOptions(product: ProductNode) { // console.log(product) const options = product.productOptions.edges?.map(({ node }: any) => ({ displayName: node.displayName.toLowerCase(), diff --git a/lib/bigcommerce/api/catalog/handlers/get-products.ts b/lib/bigcommerce/api/catalog/handlers/get-products.ts index 0f845d771..c8dfd8ce3 100644 --- a/lib/bigcommerce/api/catalog/handlers/get-products.ts +++ b/lib/bigcommerce/api/catalog/handlers/get-products.ts @@ -1,7 +1,4 @@ -import getAllProducts, { - Products, - Product, -} from '../../operations/get-all-products' +import getAllProducts, { ProductEdge } from '../../operations/get-all-products' import type { ProductsHandlers } from '../products' const SORT: { [key: string]: string | undefined } = { @@ -54,14 +51,13 @@ const getProducts: ProductsHandlers['getProducts'] = async ({ variables: { first: LIMIT, entityIds }, }) // Put the products in an object that we can use to get them by id - const productsById = graphqlData.products.reduce<{ [k: number]: Product }>( - (prods, p) => { - prods[p.node.entityId] = p - return prods - }, - {} - ) - const products: Products = found ? [] : graphqlData.products + const productsById = graphqlData.products.reduce<{ + [k: number]: ProductEdge + }>((prods, p) => { + prods[p.node.entityId] = p + return prods + }, {}) + const products: ProductEdge[] = found ? [] : graphqlData.products // Populate the products array with the graphql products, in the order // assigned by the list of entity ids diff --git a/lib/bigcommerce/api/catalog/products.ts b/lib/bigcommerce/api/catalog/products.ts index 0af21149e..95d780474 100644 --- a/lib/bigcommerce/api/catalog/products.ts +++ b/lib/bigcommerce/api/catalog/products.ts @@ -4,11 +4,11 @@ import createApiHandler, { BigcommerceHandler, } from '../utils/create-api-handler' import { BigcommerceApiError } from '../utils/errors' -import type { Products } from '../operations/get-all-products' +import type { ProductEdge } from '../operations/get-all-products' import getProducts from './handlers/get-products' export type SearchProductsData = { - products: Products + products: ProductEdge[] found: boolean } diff --git a/lib/bigcommerce/api/operations/get-all-products.ts b/lib/bigcommerce/api/operations/get-all-products.ts index 39f7ba147..c4408a4b6 100644 --- a/lib/bigcommerce/api/operations/get-all-products.ts +++ b/lib/bigcommerce/api/operations/get-all-products.ts @@ -1,7 +1,7 @@ import type { GetAllProductsQuery, GetAllProductsQueryVariables, -} from 'lib/bigcommerce/schema' +} from '@lib/bigcommerce/schema' import type { RecursivePartial, RecursiveRequired } from '../utils/types' import filterEdges from '../utils/filter-edges' import { productConnectionFragment } from '../fragments/product' @@ -47,12 +47,12 @@ export type ProductEdge = NonNullable< NonNullable[0] > -export type Product = ProductEdge - -export type Products = ProductEdge[] +export type ProductNode = ProductEdge['node'] export type GetAllProductsResult< - T extends Record = { products: Products } + T extends Record = { + products: ProductEdge[] + } > = T const FIELDS = [ diff --git a/lib/bigcommerce/api/operations/get-product.ts b/lib/bigcommerce/api/operations/get-product.ts index 4f052bbee..a4f91afdb 100644 --- a/lib/bigcommerce/api/operations/get-product.ts +++ b/lib/bigcommerce/api/operations/get-product.ts @@ -33,13 +33,13 @@ export const getProductQuery = /* GraphQL */ ` ${productInfoFragment} ` -export type Product = Extract< +export type ProductNode = Extract< GetProductQuery['site']['route']['node'], { __typename: 'Product' } > export type GetProductResult< - T extends { product?: any } = { product?: Product } + T extends { product?: any } = { product?: ProductNode } > = T export type ProductVariables = Images & From e5ee8caaeca0225bf0d22af4a956f7b5932a9cca Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Mon, 19 Oct 2020 13:49:02 -0500 Subject: [PATCH 3/8] Filtered products for the landing --- lib/range-map.ts | 7 ++++++ pages/index.tsx | 61 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 lib/range-map.ts diff --git a/lib/range-map.ts b/lib/range-map.ts new file mode 100644 index 000000000..886f20d6b --- /dev/null +++ b/lib/range-map.ts @@ -0,0 +1,7 @@ +export default function rangeMap(n: number, fn: (i: number) => any) { + const arr = [] + while (n > arr.length) { + arr.push(fn(arr.length)) + } + return arr +} diff --git a/pages/index.tsx b/pages/index.tsx index 65afe747f..4cad5bd6d 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,39 +1,76 @@ +import { useMemo } from 'react' import { GetStaticPropsContext, InferGetStaticPropsType } from 'next' import getAllProducts from '@lib/bigcommerce/api/operations/get-all-products' +import getSiteInfo from '@lib/bigcommerce/api/operations/get-site-info' +import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages' +import rangeMap from '@lib/range-map' import { Layout } from '@components/core' import { Grid, Marquee, Hero } from '@components/ui' import { ProductCard } from '@components/product' -import getSiteInfo from '@lib/bigcommerce/api/operations/get-site-info' -import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages' export async function getStaticProps({ preview }: GetStaticPropsContext) { - const { pages } = await getAllPages() - const { products } = await getAllProducts() const { products: featuredProducts } = await getAllProducts({ - variables: { field: 'featuredProducts', first: 3 }, + variables: { field: 'featuredProducts', first: 6 }, + }) + const { products: bestSellingProducts } = await getAllProducts({ + variables: { field: 'bestSellingProducts', first: 6 }, + }) + const { products: newestProducts } = await getAllProducts({ + variables: { field: 'newestProducts', first: 12 }, }) const { categories, brands } = await getSiteInfo() + const { pages } = await getAllPages() return { - props: { pages, products, featuredProducts, categories, brands }, + props: { + featuredProducts, + bestSellingProducts, + newestProducts, + categories, + brands, + pages, + }, + revalidate: 10, } } +const nonNullable = (v: any) => v + export default function Home({ - products, featuredProducts, + bestSellingProducts, + newestProducts, categories, brands, }: InferGetStaticPropsType) { + const { featured, bestSelling } = useMemo(() => { + // Create a copy of products that we can mutate + const products = [...newestProducts] + // If the lists of featured and best selling products don't have enough + // products, then fill them with products from the products list, this + // is useful for new commerce sites that don't have a lot of products + return { + featured: rangeMap( + 6, + (i) => featuredProducts[i] ?? products.shift() + ).filter(nonNullable), + bestSelling: rangeMap( + 6, + (i) => bestSellingProducts[i] ?? products.shift() + ).filter(nonNullable), + } + // Props from getStaticProps won't change + }, []) + return (
- {featuredProducts.map(({ node }) => ( + {featured.slice(0, 3).map(({ node }) => ( ))} - {products.slice(0, 3).map(({ node }) => ( + {bestSelling.slice(0, 3).map(({ node }) => ( ))} @@ -48,12 +85,12 @@ export default function Home({ ‘Natural’." /> - {products.slice(3, 6).map(({ node }) => ( + {featured.slice(3, 6).map(({ node }) => ( ))} - {products.slice(0, 3).map(({ node }) => ( + {bestSelling.slice(3, 6).map(({ node }) => ( ))} @@ -84,7 +121,7 @@ export default function Home({
- {products.map(({ node }) => ( + {newestProducts.map(({ node }) => ( ))} From f6e0fd761e0c81f120438fe4db6d6a9b0aa2cfa8 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Mon, 19 Oct 2020 13:56:29 -0500 Subject: [PATCH 4/8] Don't import from 'lodash' --- lib/colors.ts | 2 +- package.json | 2 ++ pages/search.tsx | 9 +++++---- yarn.lock | 12 ++++++++++++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/colors.ts b/lib/colors.ts index 2d4ca14cc..08f605e0e 100644 --- a/lib/colors.ts +++ b/lib/colors.ts @@ -1,4 +1,4 @@ -import { random } from 'lodash' +import random from 'lodash.random' export function getRandomPairOfColors() { const colors = ['#37B679', '#DA3C3C', '#3291FF', '#7928CA', '#79FFE1'] diff --git a/package.json b/package.json index 16277c1d6..1c16e52d6 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "cookie": "^0.4.1", "js-cookie": "^2.2.1", "lodash.debounce": "^4.0.8", + "lodash.random": "^3.2.0", "next": "^9.5.6-canary.4", "next-seo": "^4.11.0", "next-themes": "^0.0.4", @@ -56,6 +57,7 @@ "@types/cookie": "^0.4.0", "@types/js-cookie": "^2.2.6", "@types/lodash.debounce": "^4.0.6", + "@types/lodash.random": "^3.2.6", "@types/node": "^14.11.2", "@types/react": "^16.9.49", "graphql": "^15.3.0", diff --git a/pages/search.tsx b/pages/search.tsx index 391acf22e..be2b28ee2 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -1,15 +1,15 @@ import cn from 'classnames' +import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next' import Link from 'next/link' -import { range } from 'lodash' import { useRouter } from 'next/router' -import { GetStaticPropsContext, InferGetStaticPropsType } from 'next' import { Layout } from '@components/core' import { ProductCard } from '@components/product' import { Container, Grid, Skeleton } from '@components/ui' -import getSlug from '@utils/get-slug' import useSearch from '@lib/bigcommerce/products/use-search' import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages' import getSiteInfo from '@lib/bigcommerce/api/operations/get-site-info' +import rangeMap from '@lib/range-map' +import getSlug from '@utils/get-slug' import { filterQuery, getCategoryPath, @@ -151,8 +151,9 @@ export default function Search({ ) : ( - {range(12).map(() => ( + {rangeMap(12, (i) => ( diff --git a/yarn.lock b/yarn.lock index a39341220..23ff17fe4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2206,6 +2206,13 @@ 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" + integrity sha512-RRr0pKm+3USvG/HTkuRKA8v2EqXu19VXC09j4VL2UQec8Yx8Fn6wYTPGjYdmX4UFd23ykS7SLFkiULS/rv8kTA== + dependencies: + "@types/lodash" "*" + "@types/lodash@*": version "4.14.161" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.161.tgz#a21ca0777dabc6e4f44f3d07f37b765f54188b18" @@ -5511,6 +5518,11 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= +lodash.random@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.random/-/lodash.random-3.2.0.tgz#96e24e763333199130d2c9e2fd57f91703cc262d" + integrity sha1-luJOdjMzGZEw0sni/Vf5FwPMJi0= + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" From 93d99c025306f9a315cffb5309d617005179b807 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Mon, 19 Oct 2020 13:57:40 -0500 Subject: [PATCH 5/8] Move types packages to dev deps --- package.json | 8 ++++---- yarn.lock | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 1c16e52d6..d46251f58 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,6 @@ "dependencies": { "@headlessui/react": "^0.2.0", "@tailwindcss/ui": "^0.6.2", - "@types/bunyan": "^1.8.6", - "@types/bunyan-prettystream": "^0.1.31", - "@types/classnames": "^2.2.10", - "@types/react-swipeable-views": "^0.13.0", "animate.css": "^4.1.1", "bunyan": "^1.8.14", "bunyan-prettystream": "^0.1.3", @@ -54,12 +50,16 @@ "@graphql-codegen/typescript": "^1.17.10", "@graphql-codegen/typescript-operations": "^1.17.8", "@manifoldco/swagger-to-ts": "^2.1.0", + "@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", "@types/lodash.debounce": "^4.0.6", "@types/lodash.random": "^3.2.6", "@types/node": "^14.11.2", "@types/react": "^16.9.49", + "@types/react-swipeable-views": "^0.13.0", "graphql": "^15.3.0", "postcss-flexbugs-fixes": "^4.2.1", "postcss-preset-env": "^6.7.0", diff --git a/yarn.lock b/yarn.lock index 23ff17fe4..93301d39e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2251,9 +2251,9 @@ "@types/react" "*" "@types/react@*": - version "16.9.52" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.52.tgz#c46c72d1a1d8d9d666f4dd2066c0e22600ccfde1" - integrity sha512-EHRjmnxiNivwhGdMh9sz1Yw9AUxTSZFxKqdBWAAzyZx3sufWwx6ogqHYh/WB1m/I4ZpjkoZLExF5QTy2ekVi/Q== + version "16.9.53" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.53.tgz#40cd4f8b8d6b9528aedd1fff8fcffe7a112a3d23" + integrity sha512-4nW60Sd4L7+WMXH1D6jCdVftuW7j4Za6zdp6tJ33Rqv0nk1ZAmQKML9ZLD4H0dehA3FZxXR/GM8gXplf82oNGw== dependencies: "@types/prop-types" "*" csstype "^3.0.2" From 20c08fcbb4c98d6194586ef1dabfd2015b5fb540 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Mon, 19 Oct 2020 21:29:27 -0500 Subject: [PATCH 6/8] Allow the API to create customers --- lib/bigcommerce/api/customers.ts | 56 ------------------ .../api/customers/handlers/create-customer.ts | 36 +++++++++++ lib/bigcommerce/api/customers/index.ts | 59 +++++++++++++++++++ pages/api/bigcommerce/customers.ts | 3 + 4 files changed, 98 insertions(+), 56 deletions(-) delete mode 100644 lib/bigcommerce/api/customers.ts create mode 100644 lib/bigcommerce/api/customers/handlers/create-customer.ts create mode 100644 lib/bigcommerce/api/customers/index.ts create mode 100644 pages/api/bigcommerce/customers.ts diff --git a/lib/bigcommerce/api/customers.ts b/lib/bigcommerce/api/customers.ts deleted file mode 100644 index c4734c6e5..000000000 --- a/lib/bigcommerce/api/customers.ts +++ /dev/null @@ -1,56 +0,0 @@ -import createApiHandler, { - BigcommerceApiHandler, - BigcommerceHandler, -} from './utils/create-api-handler' -import isAllowedMethod from './utils/is-allowed-method' -import { BigcommerceApiError } from './utils/errors' - -type Body = Partial | undefined - -export type Customer = any - -export type AddCustomerBody = { item: any } - -export type CartHandlers = { - addItem: BigcommerceHandler> -} - -const METHODS = ['POST'] - -const customersApi: BigcommerceApiHandler = async ( - req, - res, - config -) => { - if (!isAllowedMethod(req, res, METHODS)) return - - try { - if (req.method === 'POST') { - // let result = {} as any - // const - // result = await config.storeApiFetch('/v3/customers') - } - } catch (error) { - console.error(error) - - const message = - error instanceof BigcommerceApiError - ? 'An unexpected error ocurred with the Bigcommerce API' - : 'An unexpected error ocurred' - - res.status(500).json({ data: null, errors: [{ message }] }) - } -} - -const createCustomer: BigcommerceHandler = ({ - req, - res, - body, - config, -}) => {} - -const handlers = { - createCustomer, -} - -export default createApiHandler(customersApi, handlers, {}) diff --git a/lib/bigcommerce/api/customers/handlers/create-customer.ts b/lib/bigcommerce/api/customers/handlers/create-customer.ts new file mode 100644 index 000000000..73f3738cd --- /dev/null +++ b/lib/bigcommerce/api/customers/handlers/create-customer.ts @@ -0,0 +1,36 @@ +import { CustomersHandlers } from '..' + +const createCustomer: CustomersHandlers['createCustomer'] = async ({ + res, + body: { firstName, lastName, email, password }, + config, +}) => { + // TODO: Add proper validations with something like Ajv + if (!(firstName && lastName && email && password)) { + return res.status(400).json({ + data: null, + errors: [{ message: 'Invalid request' }], + }) + } + // TODO: validate the password. + // Passwords must be at least 7 characters and contain both alphabetic + // and numeric characters. + + const { data } = await config.storeApiFetch('/v3/customers', { + method: 'POST', + body: JSON.stringify([ + { + first_name: firstName, + last_name: lastName, + email, + authentication: { + new_password: password, + }, + }, + ]), + }) + + res.status(200).json({ data }) +} + +export default createCustomer diff --git a/lib/bigcommerce/api/customers/index.ts b/lib/bigcommerce/api/customers/index.ts new file mode 100644 index 000000000..25b2fd270 --- /dev/null +++ b/lib/bigcommerce/api/customers/index.ts @@ -0,0 +1,59 @@ +import createApiHandler, { + BigcommerceApiHandler, + BigcommerceHandler, +} from '../utils/create-api-handler' +import isAllowedMethod from '../utils/is-allowed-method' +import { BigcommerceApiError } from '../utils/errors' +import createCustomer from './handlers/create-customer' + +type Body = Partial | undefined + +export type Customer = any + +export type CreateCustomerBody = { + firstName: string + lastName: string + email: string + password: string +} + +export type CustomersHandlers = { + createCustomer: BigcommerceHandler< + Customer, + { cartId?: string } & Body + > +} + +const METHODS = ['POST'] + +const customersApi: BigcommerceApiHandler = async ( + req, + res, + config +) => { + if (!isAllowedMethod(req, res, METHODS)) return + + const { cookies } = req + const cartId = cookies[config.cartCookie] + + try { + if (req.method === 'POST') { + console.log('BODY', req.body) + const body = { cartId, ...req.body } + return await handlers['createCustomer']({ req, res, config, body }) + } + } catch (error) { + console.error(error) + + const message = + error instanceof BigcommerceApiError + ? 'An unexpected error ocurred with the Bigcommerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +const handlers = { createCustomer } + +export default createApiHandler(customersApi, handlers, {}) diff --git a/pages/api/bigcommerce/customers.ts b/pages/api/bigcommerce/customers.ts new file mode 100644 index 000000000..67e342604 --- /dev/null +++ b/pages/api/bigcommerce/customers.ts @@ -0,0 +1,3 @@ +import customersApi from '@lib/bigcommerce/api/customers' + +export default customersApi() From 9257b44c0d02b2d7c603c774f85b764209cc7743 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Mon, 19 Oct 2020 21:48:22 -0500 Subject: [PATCH 7/8] Improved error handling for the fetch api --- lib/bigcommerce/api/utils/errors.ts | 4 +++- .../api/utils/fetch-graphql-api.ts | 2 +- lib/bigcommerce/api/utils/fetch-store-api.ts | 23 +++++++++++-------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/lib/bigcommerce/api/utils/errors.ts b/lib/bigcommerce/api/utils/errors.ts index 1cdd72214..bd7a995d8 100644 --- a/lib/bigcommerce/api/utils/errors.ts +++ b/lib/bigcommerce/api/utils/errors.ts @@ -4,12 +4,14 @@ export class BigcommerceGraphQLError extends Error {} export class BigcommerceApiError extends Error { status: number res: Response + data: any - constructor(msg: string, res: Response) { + constructor(msg: string, res: Response, data?: any) { super(msg) this.name = 'BigcommerceApiError' this.status = res.status this.res = res + this.data = data } } diff --git a/lib/bigcommerce/api/utils/fetch-graphql-api.ts b/lib/bigcommerce/api/utils/fetch-graphql-api.ts index aaf6e75ea..1e79f7c2f 100644 --- a/lib/bigcommerce/api/utils/fetch-graphql-api.ts +++ b/lib/bigcommerce/api/utils/fetch-graphql-api.ts @@ -23,7 +23,7 @@ export default async function fetchGraphqlApi( const json = await res.json() if (json.errors) { console.error(json.errors) - throw new Error('Failed to fetch API') + throw new Error('Failed to fetch BigCommerce API') } return json.data } diff --git a/lib/bigcommerce/api/utils/fetch-store-api.ts b/lib/bigcommerce/api/utils/fetch-store-api.ts index 0f0daedfe..44ec93b41 100644 --- a/lib/bigcommerce/api/utils/fetch-store-api.ts +++ b/lib/bigcommerce/api/utils/fetch-store-api.ts @@ -24,13 +24,22 @@ export default async function fetchStoreApi( ) } + const contentType = res.headers.get('Content-Type') + const isJSON = contentType?.includes('application/json') + if (!res.ok) { - throw new BigcommerceApiError(await getErrorText(res), res) + const data = isJSON ? await res.json() : await getTextOrNull(res) + const headers = getRawHeaders(res) + const msg = `Big Commerce API error (${ + res.status + }) \nHeaders: ${JSON.stringify(headers, null, 2)}\n${ + typeof data === 'string' ? data : JSON.stringify(data, null, 2) + }` + + throw new BigcommerceApiError(msg, res, data) } - const contentType = res.headers.get('Content-Type') - - if (!contentType?.includes('application/json')) { + if (!isJSON) { throw new BigcommerceApiError( `Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`, res @@ -41,12 +50,6 @@ export default async function fetchStoreApi( return res.status === 204 ? null : await res.json() } -async function getErrorText(res: Response) { - return `Big Commerce API error (${res.status}) \n${JSON.stringify( - getRawHeaders(res) - )}\n ${await getTextOrNull(res)}` -} - function getRawHeaders(res: Response) { const headers: { [key: string]: string } = {} From 0922c87621a6a34986bf811a1f2e55ed0bc7edf4 Mon Sep 17 00:00:00 2001 From: Luis Alvarez Date: Mon, 19 Oct 2020 22:08:01 -0500 Subject: [PATCH 8/8] Handle validation for duplicated emails --- .../api/customers/handlers/create-customer.ts | 50 +++++++++++++------ .../api/utils/create-api-handler.ts | 2 +- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/lib/bigcommerce/api/customers/handlers/create-customer.ts b/lib/bigcommerce/api/customers/handlers/create-customer.ts index 73f3738cd..caefeb245 100644 --- a/lib/bigcommerce/api/customers/handlers/create-customer.ts +++ b/lib/bigcommerce/api/customers/handlers/create-customer.ts @@ -1,3 +1,4 @@ +import { BigcommerceApiError } from '../../utils/errors' import { CustomersHandlers } from '..' const createCustomer: CustomersHandlers['createCustomer'] = async ({ @@ -12,25 +13,46 @@ const createCustomer: CustomersHandlers['createCustomer'] = async ({ errors: [{ message: 'Invalid request' }], }) } - // TODO: validate the password. + // TODO: validate the password and email // Passwords must be at least 7 characters and contain both alphabetic // and numeric characters. - const { data } = await config.storeApiFetch('/v3/customers', { - method: 'POST', - body: JSON.stringify([ - { - first_name: firstName, - last_name: lastName, - email, - authentication: { - new_password: password, + try { + const { data } = await config.storeApiFetch('/v3/customers', { + method: 'POST', + body: JSON.stringify([ + { + first_name: firstName, + last_name: lastName, + email, + authentication: { + new_password: password, + }, }, - }, - ]), - }) + ]), + }) - res.status(200).json({ data }) + res.status(200).json({ data }) + } catch (error) { + if (error instanceof BigcommerceApiError && error.status === 422) { + const hasEmailError = '0.email' in error.data?.errors + + // If there's an error with the email, it most likely means it's duplicated + if (hasEmailError) { + return res.status(400).json({ + data: null, + errors: [ + { + message: 'The email is already in use', + code: 'duplicated_email', + }, + ], + }) + } + } + + throw error + } } export default createCustomer diff --git a/lib/bigcommerce/api/utils/create-api-handler.ts b/lib/bigcommerce/api/utils/create-api-handler.ts index 2cb91c534..c6363cb15 100644 --- a/lib/bigcommerce/api/utils/create-api-handler.ts +++ b/lib/bigcommerce/api/utils/create-api-handler.ts @@ -27,7 +27,7 @@ export type BigcommerceHandlers = { export type BigcommerceApiResponse = { data: T | null - errors?: { message: string }[] + errors?: { message: string; code?: string }[] } export default function createApiHandler<