4
0
forked from crowetic/commerce

Merge branch 'master' into arzafran/tweak-banner

This commit is contained in:
Franco Arza 2020-10-19 19:29:34 -03:00
commit 0a270e7d23
46 changed files with 912 additions and 173 deletions

View File

@ -22,16 +22,16 @@
--violet: #7928ca;
--blue: #0070f3;
--accents-0: #fff;
--accents-1: #fafafa;
--accents-2: #eaeaea;
--accents-3: #999999;
--accents-4: #888888;
--accents-5: #666666;
--accents-6: #444444;
--accents-7: #333333;
--accents-8: #111111;
--accents-9: #000;
--accents-0: #f8f9fa;
--accents-1: #f1f3f5;
--accents-2: #e9ecef;
--accents-3: #dee2e6;
--accents-4: #ced4da;
--accents-5: #adb5bd;
--accents-6: #868e96;
--accents-7: #495057;
--accents-8: #343a40;
--accents-9: #212529;
}
[data-theme='dark'] {
@ -46,16 +46,16 @@
--text-primary: white;
--text-secondary: black;
--accents-0: #000;
--accents-1: #111111;
--accents-2: #333333;
--accents-3: #444444;
--accents-4: #666666;
--accents-5: #888888;
--accents-6: #999999;
--accents-7: #eaeaea;
--accents-8: #fafafa;
--accents-9: #fff;
--accents-0: #212529;
--accents-1: #343a40;
--accents-2: #495057;
--accents-3: #868e96;
--accents-4: #adb5bd;
--accents-5: #ced4da;
--accents-6: #dee2e6;
--accents-7: #e9ecef;
--accents-8: #f1f3f5;
--accents-9: #f8f9fa;
}
.fit {

View File

@ -1,5 +1,5 @@
.input {
@apply bg-transparent px-3 py-2 appearance-none w-full transition duration-150 ease-in-out rounded-lg placeholder-accents-4 pr-10;
@apply bg-transparent px-3 py-2 appearance-none w-full transition duration-150 ease-in-out placeholder-accents-5 pr-10;
min-width: 300px;
}

View File

@ -17,7 +17,7 @@ const Searchbar: FC<Props> = ({ className }) => {
return (
<div
className={cn(
'relative rounded-lg text-sm bg-accents-1 text-base w-full border border-accents-2',
'relative text-sm bg-accents-1 text-base w-full',
className
)}
>

View File

@ -2,12 +2,11 @@
}
.list {
@apply flex flex-row items-center;
@apply flex flex-row items-center h-full;
}
.item {
@apply mr-6 cursor-pointer relative transition ease-in-out duration-100 text-base;
@apply mr-6 cursor-pointer relative transition ease-in-out duration-100 text-base flex items-center;
&:hover {
@apply text-accents-8;
}

View File

@ -10,15 +10,15 @@ const ArrowLeft = ({ ...props }) => {
>
<path
d="M19 12H5"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12 19L5 12L12 5"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)

View File

@ -10,9 +10,9 @@ const Check = ({ ...props }) => {
>
<path
d="M20 6L9 17L4 12"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)

View File

@ -4,9 +4,9 @@ const Minus = ({ ...props }) => {
<path
d="M5 12H19"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)

View File

@ -4,16 +4,16 @@ const Plus = ({ ...props }) => {
<path
d="M12 5V19"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5 12H19"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)

View File

@ -55,7 +55,7 @@
}
.squareBg,
.productTitle,
.productTitle > span,
.productPrice,
.wishlistButton {
@apply transition ease-in-out duration-300;
@ -65,9 +65,13 @@
@apply transform absolute inset-0 z-0 bg-secondary;
}
.squareBg.gray {
@apply bg-gray-300 !important;
}
.productTitle {
line-height: 51px;
width: 200px;
line-height: 40px;
width: 18vw;
& span {
@apply inline text-2xl leading-6 p-4 bg-primary text-primary font-bold;

View File

@ -8,7 +8,7 @@ interface Props {
className?: string
children?: ReactNode[] | Component[] | any[]
node: ProductData
variant?: 'slim'
variant?: 'slim' | 'simple'
}
interface ProductData {
@ -23,7 +23,7 @@ const ProductCard: FC<Props> = ({ className, node: p, variant }) => {
return (
<div className="relative overflow-hidden box-border">
<img
className="object-scale-down h-24"
className="object-scale-down h-48"
src={p.images.edges[0].node.urlSmall}
/>
<div className="absolute inset-0 flex items-center justify-end mr-8">
@ -44,12 +44,12 @@ const ProductCard: FC<Props> = ({ className, node: p, variant }) => {
src={p.images.edges[0].node.urlXL}
/>
</div>
<div className={s.squareBg} />
<div className={cn(s.squareBg, { [s.gray]: variant === 'simple' })} />
<div className="flex flex-row justify-between box-border w-full z-10 relative">
<div className="">
<div className={s.productTitle}>
<p className={s.productTitle}>
<span>{p.name}</span>
</div>
</p>
<span className={s.productPrice}>${p.prices.price.value}</span>
</div>
<div className={s.wishlistButton}>

View File

@ -1,7 +1,6 @@
import { NextSeo } from 'next-seo'
import { FC, useState } from 'react'
import s from './ProductView.module.css'
import { Colors } from '@components/ui/types'
import { useUI } from '@components/ui/context'
import { Button, Container } from '@components/ui'
import { Swatch, ProductSlider } from '@components/product'
@ -15,19 +14,12 @@ interface Props {
product: Product
}
interface Choices {
size?: string | null
color?: string | null
}
const ProductView: FC<Props> = ({ product, className }) => {
const options = getProductOptions(product)
// console.log(options)
const addItem = useAddItem()
const { openSidebar } = useUI()
const options = getProductOptions(product)
const [choices, setChoices] = useState<Choices>({
const [choices, setChoices] = useState<Record<string, any>>({
size: null,
color: null,
})
@ -48,9 +40,6 @@ const ProductView: FC<Props> = ({ product, className }) => {
}
}
const activeSize = choices.size
const activeColor = choices.color
return (
<Container>
<NextSeo
@ -88,6 +77,7 @@ const ProductView: FC<Props> = ({ product, className }) => {
{/** TODO: Change with Image Component */}
{product.images.edges?.map((image, i) => (
<img
key={image?.node.urlSmall}
className="w-full object-cover"
src={image?.node.urlXL}
loading={i === 0 ? 'eager' : 'lazy'}
@ -104,25 +94,28 @@ const ProductView: FC<Props> = ({ product, className }) => {
<div className="flex-1 flex flex-col pt-24">
<section>
{options?.map((opt: any) => (
<div className="pb-4">
<div className="pb-4" key={opt.displayName}>
<h2 className="uppercase font-medium">{opt.displayName}</h2>
<div className="flex flex-row py-4">
{opt.values.map((v: any) => {
const active = choices[opt.displayName]
return (
<Swatch
key={v.entityId}
active={v.label === activeColor}
active={v.label === active}
variant={opt.displayName}
color={v.hexColors ? v.hexColors[0] : ''}
label={v.label}
onClick={() =>
onClick={() => {
setChoices((choices) => {
console.log(choices)
return {
...choices,
[opt.displayName]: v.label,
}
})
}
}}
/>
)
})}

View File

@ -1,11 +1,11 @@
.root {
@apply h-12 w-12 bg-primary text-primary rounded-full mr-3 inline-flex
items-center justify-center cursor-pointer transition duration-75 ease-in-out
p-0 shadow-none border-gray-200 border box-border;
items-center justify-center cursor-pointer transition duration-100 ease-in-out
p-0 shadow-none border-gray-200 border box-border text-black;
}
.active.size {
@apply border-accents-2 border-2;
@apply border-accents-9 border-2;
}
.root:hover {

View File

@ -1,10 +1,9 @@
import cn from 'classnames'
import { FC } from 'react'
import s from './Swatch.module.css'
import { Colors } from '@components/ui/types'
import { Check } from '@components/icon'
import Button, { ButtonProps } from '@components/ui/Button'
import { isDark } from '@lib/colors'
interface Props {
active?: boolean
children?: any
@ -24,7 +23,8 @@ const Swatch: FC<Props & ButtonProps> = ({
}) => {
variant = variant?.toLowerCase()
label = label?.toLowerCase()
// console.log(variant)
const isDarkBg = isDark(color)
const rootClassName = cn(
s.root,
{
@ -38,12 +38,12 @@ const Swatch: FC<Props & ButtonProps> = ({
<Button
className={rootClassName}
style={color ? { backgroundColor: color } : {}}
{...props}
>
{variant === 'color' && active && (
<span
className={cn('absolute', {
'text-white': label !== 'white',
'text-black': label === 'white',
'text-white': isDarkBg,
})}
>
<Check />

View File

@ -20,3 +20,7 @@
.loading {
@apply bg-accents-1 text-accents-3 border-accents-2 cursor-not-allowed;
}
.slim {
@apply py-2 transform-none normal-case;
}

View File

@ -13,7 +13,7 @@ import { LoadingDots } from '@components/ui'
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
href?: string
className?: string
variant?: 'filled' | 'outlined' | 'flat' | 'none'
variant?: 'flat' | 'slim'
active?: boolean
type?: 'submit' | 'reset' | 'button'
Component?: string | JSXElementConstructor<any>
@ -24,7 +24,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
const {
className,
variant = 'filled',
variant = 'flat',
children,
active,
onClick,
@ -50,6 +50,7 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
const rootClassName = cn(
s.root,
{
[s.slim]: variant === 'slim',
[s.loading]: loading,
},
className
@ -57,16 +58,16 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
return (
<Component
className={rootClassName}
aria-pressed={active}
data-variant={variant}
ref={mergeRefs([ref, buttonRef])}
{...buttonProps}
data-active={isPressed ? '' : undefined}
className={rootClassName}
style={{
width,
...style,
}}
data-active={isPressed ? '' : undefined}
>
{children}
{loading && (

View File

@ -1,12 +1,14 @@
const Logo = () => (
const Logo = ({ className = '', ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
{...props}
>
<rect width="32" height="32" rx="16" fill="var(--secondary)" />
<rect width="100%" height="100%" rx="16" fill="var(--secondary)" />
<path
fillRule="evenodd"
clipRule="evenodd"

View File

@ -8,7 +8,7 @@
& > * {
@apply flex-1 px-16 py-4;
width: 400px;
width: 430px;
}
}

View File

@ -0,0 +1,8 @@
.root {
@apply fixed bg-black flex items-center inset-0 z-50 justify-center;
background-color: rgba(0, 0, 0, 0.35);
}
.modal {
@apply bg-white p-12;
}

View File

@ -0,0 +1,52 @@
import cn from 'classnames'
import { FC, useRef } from 'react'
import s from './Modal.module.css'
import { useDialog } from '@react-aria/dialog'
import {
useOverlay,
usePreventScroll,
useModal,
OverlayProvider,
OverlayContainer,
} from '@react-aria/overlays'
import { FocusScope } from '@react-aria/focus'
interface Props {
className?: string
children?: any
show?: boolean
close: () => void
}
const Modal: FC<Props> = ({
className,
children,
show = true,
close,
...props
}) => {
const rootClassName = cn(s.root, className)
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
usePreventScroll()
let { modalProps } = useModal()
let { overlayProps } = useOverlay(props, ref)
let { dialogProps } = useDialog(props, ref)
return (
<div className={rootClassName}>
<FocusScope contain restoreFocus autoFocus>
<div
{...overlayProps}
{...dialogProps}
{...modalProps}
ref={ref}
className={s.modal}
>
{children}
</div>
</FocusScope>
</div>
)
}
export default Modal

View File

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

View File

@ -7,3 +7,4 @@ export { default as Marquee } from './Marquee'
export { default as Container } from './Container'
export { default as LoadingDots } from './LoadingDots'
export { default as Skeleton } from './Skeleton'
export { default as Modal } from './Modal'

View File

@ -22,7 +22,7 @@ export type ProductsHandlers = {
const METHODS = ['GET']
// TODO: a complete implementation should have schema validation for `req.body`
const cartApi: BigcommerceApiHandler<
const productApi: BigcommerceApiHandler<
SearchProductsData,
ProductsHandlers
> = async (req, res, config, handlers) => {
@ -45,4 +45,4 @@ const cartApi: BigcommerceApiHandler<
export const handlers = { getProducts }
export default createApiHandler(cartApi, handlers, {})
export default createApiHandler(productApi, handlers, {})

View File

@ -9,12 +9,15 @@ export const responsiveImageFragment = /* GraphQL */ `
isDefault
}
`
export const multipleChoiceFragment = /* GraphQL */ `
export const swatchOptionFragment = /* GraphQL */ `
fragment swatchOption on SwatchOptionValue {
isDefault
hexColors
}
`
export const multipleChoiceOptionFragment = /* GraphQL */ `
fragment multipleChoiceOption on MultipleChoiceOption {
entityId
values {
@ -26,6 +29,8 @@ export const multipleChoiceFragment = /* GraphQL */ `
}
}
}
${swatchOptionFragment}
`
export const productInfoFragment = /* GraphQL */ `
@ -76,5 +81,22 @@ export const productInfoFragment = /* GraphQL */ `
}
${responsiveImageFragment}
${multipleChoiceFragment}
${multipleChoiceOptionFragment}
`
export const productConnectionFragment = /* GraphQL */ `
fragment productConnnection on ProductConnection {
pageInfo {
startCursor
endCursor
}
edges {
cursor
node {
...productInfo
}
}
}
${productInfoFragment}
`

View File

@ -4,7 +4,7 @@ import type {
} from 'lib/bigcommerce/schema'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import filterEdges from '../utils/filter-edges'
import { productInfoFragment } from '../fragments/product'
import { productConnectionFragment } from '../fragments/product'
import { BigcommerceConfig, getConfig, Images, ProductImageVariables } from '..'
export const getAllProductsQuery = /* GraphQL */ `
@ -19,24 +19,28 @@ export const getAllProductsQuery = /* GraphQL */ `
$imgLargeHeight: Int
$imgXLWidth: Int = 1280
$imgXLHeight: Int
$products: Boolean = false
$featuredProducts: Boolean = false
$bestSellingProducts: Boolean = false
$newestProducts: Boolean = false
) {
site {
products(first: $first, entityIds: $entityIds) {
pageInfo {
startCursor
endCursor
}
edges {
cursor
node {
...productInfo
}
}
products(first: $first, entityIds: $entityIds) @include(if: $products) {
...productConnnection
}
featuredProducts(first: $first) @include(if: $featuredProducts) {
...productConnnection
}
bestSellingProducts(first: $first) @include(if: $bestSellingProducts) {
...productConnnection
}
newestProducts(first: $first) @include(if: $newestProducts) {
...productConnnection
}
}
}
${productInfoFragment}
${productConnectionFragment}
`
export type Product = NonNullable<
@ -46,18 +50,34 @@ export type Product = NonNullable<
export type Products = Product[]
export type GetAllProductsResult<
T extends { products: any[] } = { products: Products }
T extends Record<keyof GetAllProductsResult, any[]> = { products: Products }
> = T
export type ProductVariables = Images &
Omit<GetAllProductsQueryVariables, keyof ProductImageVariables>
const FIELDS = [
'products',
'featuredProducts',
'bestSellingProducts',
'newestProducts',
]
export type ProductTypes =
| 'products'
| 'featuredProducts'
| 'bestSellingProducts'
| 'newestProducts'
export type ProductVariables = { field?: ProductTypes } & Images &
Omit<GetAllProductsQueryVariables, ProductTypes | keyof ProductImageVariables>
async function getAllProducts(opts?: {
variables?: ProductVariables
config?: BigcommerceConfig
}): Promise<GetAllProductsResult>
async function getAllProducts<T extends { products: any[] }, V = any>(opts: {
async function getAllProducts<
T extends Record<keyof GetAllProductsResult, any[]>,
V = any
>(opts: {
query: string
variables?: V
config?: BigcommerceConfig
@ -65,7 +85,7 @@ async function getAllProducts<T extends { products: any[] }, V = any>(opts: {
async function getAllProducts({
query = getAllProductsQuery,
variables: vars,
variables: { field = 'products', ...vars } = {},
config,
}: {
query?: string
@ -73,17 +93,27 @@ async function getAllProducts({
config?: BigcommerceConfig
} = {}): Promise<GetAllProductsResult> {
config = getConfig(config)
const variables: GetAllProductsQueryVariables = {
...config.imageVariables,
...vars,
}
if (!FIELDS.includes(field)) {
throw new Error(
`The field variable has to match one of ${FIELDS.join(', ')}`
)
}
variables[field] = true
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query`
const data = await config.fetch<RecursivePartial<GetAllProductsQuery>>(
query,
{ variables }
)
const products = data.site?.products?.edges
const products = data.site?.[field]?.edges
return {
products: filterEdges(products as RecursiveRequired<typeof products>),

View File

@ -0,0 +1,30 @@
import type { WishlistHandlers } from '..'
// Return current wishlist info
const addItem: WishlistHandlers['addItem'] = async ({
res,
body: { wishlistId, item },
config,
}) => {
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
const options = {
method: 'POST',
body: JSON.stringify({
items: [item],
}),
}
const { data } = await config.storeApiFetch(
`/v3/wishlists/${wishlistId}/items`,
options
)
res.status(200).json({ data })
}
export default addItem

View File

@ -0,0 +1,25 @@
import type { WishlistHandlers } from '..'
// Return current wishlist info
const addWishlist: WishlistHandlers['addWishlist'] = async ({
res,
body: { wishlist },
config,
}) => {
if (!wishlist) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing wishlist data' }],
})
}
const options = {
method: 'POST',
body: JSON.stringify(wishlist),
}
const { data } = await config.storeApiFetch(`/v3/wishlists/`, options)
res.status(200).json({ data })
}
export default addWishlist

View File

@ -0,0 +1,22 @@
import { BigcommerceApiError } from '../../utils/errors'
import type { WishlistList, WishlistHandlers } from '..'
// Return all wishlists
const getAllWishlists: WishlistHandlers['getAllWishlists'] = async ({
res,
body: { customerId },
config,
}) => {
let result: { data?: WishlistList } = {}
try {
result = await config.storeApiFetch(`/v3/wishlists/customer_id=${customerId}`)
} catch (error) {
throw error
}
const data = (result.data ?? []) as any
res.status(200).json({ data })
}
export default getAllWishlists

View File

@ -0,0 +1,20 @@
import type { Wishlist, WishlistHandlers } from '..'
// Return wishlist info
const getWishlist: WishlistHandlers['getWishlist'] = async ({
res,
body: { wishlistId },
config,
}) => {
let result: { data?: Wishlist } = {}
try {
result = await config.storeApiFetch(`/v3/wishlists/${wishlistId}`)
} catch (error) {
throw error
}
res.status(200).json({ data: result.data ?? null })
}
export default getWishlist

View File

@ -0,0 +1,25 @@
import type { WishlistHandlers } from '..'
// Return current wishlist info
const removeItem: WishlistHandlers['removeItem'] = async ({
res,
body: { wishlistId, itemId },
config,
}) => {
if (!wishlistId || !itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const result = await config.storeApiFetch<{ data: any } | null>(
`/v3/wishlists/${wishlistId}/items/${itemId}`,
{ method: 'DELETE' }
)
const data = result?.data ?? null
res.status(200).json({ data })
}
export default removeItem

View File

@ -0,0 +1,25 @@
import type { WishlistHandlers } from '..'
// Return current wishlist info
const removeWishlist: WishlistHandlers['removeWishlist'] = async ({
res,
body: { wishlistId },
config,
}) => {
if (!wishlistId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const result = await config.storeApiFetch<{ data: any } | null>(
`/v3/wishlists/${wishlistId}/`,
{ method: 'DELETE' }
)
const data = result?.data ?? null
res.status(200).json({ data })
}
export default removeWishlist

View File

@ -0,0 +1,27 @@
import type { WishlistHandlers } from '..'
// Update wish info
const updateWishlist: WishlistHandlers['updateWishlist'] = async ({
res,
body: { wishlistId, wishlist },
config,
}) => {
if (!wishlistId || !wishlist) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const { data } = await config.storeApiFetch(
`/v3/wishlists/${wishlistId}/`,
{
method: 'PUT',
body: JSON.stringify(wishlist),
}
)
res.status(200).json({ data })
}
export default updateWishlist

View File

@ -0,0 +1,152 @@
import isAllowedMethod from '../utils/is-allowed-method'
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import { BigcommerceApiError } from '../utils/errors'
import getWishlist from './handlers/get-wishlist'
import getAllWishlists from './handlers/get-all-wishlists'
import addItem from './handlers/add-item'
import removeItem from './handlers/remove-item'
import updateWishlist from './handlers/update-wishlist'
import removeWishlist from './handlers/remove-wishlist'
import addWishlist from './handlers/add-wishlist'
type Body<T> = Partial<T> | undefined
export type ItemBody = {
product_id: number
variant_id: number
}
export type AddItemBody = { wishlistId: string; item: ItemBody }
export type RemoveItemBody = { wishlistId: string; itemId: string }
export type WishlistBody = {
customer_id: number
is_public: number
name: string
items: any[]
}
export type AddWishlistBody = { wishlist: WishlistBody }
// TODO: this type should match:
// https://developer.bigcommerce.com/api-reference/store-management/wishlists/wishlists/wishlistsbyidget
export type Wishlist = {
id: string
customer_id: number
name: string
is_public: boolean
token: string
items: any[]
// TODO: add missing fields
}
export type WishlistList = Wishlist[]
export type WishlistHandlers = {
getAllWishlists: BigcommerceHandler<WishlistList, { customerId?: string }>
getWishlist: BigcommerceHandler<Wishlist, { wishlistId?: string }>
addWishlist: BigcommerceHandler<
Wishlist,
{ wishlistId: string } & Body<AddWishlistBody>
>
updateWishlist: BigcommerceHandler<
Wishlist,
{ wishlistId: string } & Body<AddWishlistBody>
>
addItem: BigcommerceHandler<Wishlist, { wishlistId: string } & Body<AddItemBody>>
removeItem: BigcommerceHandler<
Wishlist,
{ wishlistId: string } & Body<RemoveItemBody>
>
removeWishlist: BigcommerceHandler<Wishlist, { wishlistId: string }>
}
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
// TODO: a complete implementation should have schema validation for `req.body`
const wishlistApi: BigcommerceApiHandler<Wishlist, WishlistHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) return
try {
const { wishlistId, itemId, customerId } = req.body
// Return current wishlist info
if (req.method === 'GET' && wishlistId) {
const body = { wishlistId: wishlistId as string }
return await handlers['getWishlist']({ req, res, config, body })
}
// Add an item to the wishlist
if (req.method === 'POST' && wishlistId) {
const body = { wishlistId, ...req.body }
return await handlers['addItem']({ req, res, config, body })
}
// Update a wishlist
if (req.method === 'PUT' && wishlistId) {
const body = { wishlistId, ...req.body }
return await handlers['updateWishlist']({ req, res, config, body })
}
// Remove an item from the wishlist
if (req.method === 'DELETE' && wishlistId && itemId) {
const body = {
wishlistId: wishlistId as string,
itemId: itemId as string,
}
return await handlers['removeItem']({ req, res, config, body })
}
// Remove the wishlist
if (req.method === 'DELETE' && wishlistId && !itemId) {
const body = { wishlistId: wishlistId as string }
return await handlers['removeWishlist']({ req, res, config, body })
}
// Get all the wishlists
if (req.method === 'GET' && !wishlistId) {
const body = { customerId: customerId as string }
return await handlers['getAllWishlists']({
req,
res: res as any,
config,
body,
})
}
// Create a wishlist
if (req.method === 'POST' && !wishlistId) {
const { body } = req
return await handlers['addWishlist']({ 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 }] })
}
}
export const handlers = {
getWishlist,
addItem,
updateWishlist,
removeItem,
removeWishlist,
getAllWishlists,
addWishlist,
}
export default createApiHandler(wishlistApi, handlers, {})

View File

@ -1785,6 +1785,24 @@ export type ProductInfoFragment = { __typename?: 'Product' } & Pick<
}
}
export type ProductConnnectionFragment = {
__typename?: 'ProductConnection'
} & {
pageInfo: { __typename?: 'PageInfo' } & Pick<
PageInfo,
'startCursor' | 'endCursor'
>
edges?: Maybe<
Array<
Maybe<
{ __typename?: 'ProductEdge' } & Pick<ProductEdge, 'cursor'> & {
node: { __typename?: 'Product' } & ProductInfoFragment
}
>
>
>
}
export type GetAllProductPathsQueryVariables = Exact<{ [key: string]: never }>
export type GetAllProductPathsQuery = { __typename?: 'Query' } & {
@ -1814,25 +1832,24 @@ export type GetAllProductsQueryVariables = Exact<{
imgLargeHeight?: Maybe<Scalars['Int']>
imgXLWidth?: Maybe<Scalars['Int']>
imgXLHeight?: Maybe<Scalars['Int']>
products?: Maybe<Scalars['Boolean']>
featuredProducts?: Maybe<Scalars['Boolean']>
bestSellingProducts?: Maybe<Scalars['Boolean']>
newestProducts?: Maybe<Scalars['Boolean']>
}>
export type GetAllProductsQuery = { __typename?: 'Query' } & {
site: { __typename?: 'Site' } & {
products: { __typename?: 'ProductConnection' } & {
pageInfo: { __typename?: 'PageInfo' } & Pick<
PageInfo,
'startCursor' | 'endCursor'
>
edges?: Maybe<
Array<
Maybe<
{ __typename?: 'ProductEdge' } & Pick<ProductEdge, 'cursor'> & {
node: { __typename?: 'Product' } & ProductInfoFragment
}
>
>
>
}
products: { __typename?: 'ProductConnection' } & ProductConnnectionFragment
featuredProducts: {
__typename?: 'ProductConnection'
} & ProductConnnectionFragment
bestSellingProducts: {
__typename?: 'ProductConnection'
} & ProductConnnectionFragment
newestProducts: {
__typename?: 'ProductConnection'
} & ProductConnnectionFragment
}
}

View File

@ -0,0 +1,46 @@
import { useCallback } from 'react'
import { HookFetcher } from '@lib/commerce/utils/types'
import useAction from '@lib/commerce/utils/use-action'
import type { ItemBody, AddItemBody } from '../api/wishlist'
import useWishlist, { Wishlist } from './use-wishlist'
const defaultOpts = {
url: '/api/bigcommerce/wishlist',
method: 'POST',
}
export type AddItemInput = ItemBody
export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
options,
{ wishlistId, item },
fetch
) => {
return fetch({
url: options?.url ?? defaultOpts.url,
method: options?.method ?? defaultOpts.method,
body: { wishlistId, item },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useAddItem = (wishlistId: string) => {
const { mutate } = useWishlist(wishlistId)
const fn = useAction<Wishlist, AddItemBody>(defaultOpts, customFetcher)
return useCallback(
async function addItem(input: AddItemInput) {
const data = await fn({ wishlistId, item: input })
await mutate(data, false)
return data
},
[fn, mutate]
)
}
useAddItem.extend = extendHook
return useAddItem
}
export default extendHook(fetcher)

View File

@ -0,0 +1,51 @@
import { useCallback } from 'react'
import { HookFetcher } from '@lib/commerce/utils/types'
import useAction from '@lib/commerce/utils/use-action'
import type { RemoveItemBody } from '../api/wishlist'
import useWishlist, { Wishlist } from './use-wishlist'
const defaultOpts = {
url: '/api/bigcommerce/wishlists',
method: 'DELETE',
}
export type RemoveItemInput = {
id: string
}
export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = (
options,
{ wishlistId, itemId },
fetch
) => {
return fetch({
url: options?.url ?? defaultOpts.url,
method: options?.method ?? defaultOpts.method,
body: { wishlistId, itemId },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useRemoveItem = (wishlistId: string, item?: any) => {
const { mutate } = useWishlist(wishlistId)
const fn = useAction<Wishlist | null, RemoveItemBody>(
defaultOpts,
customFetcher
)
return useCallback(
async function removeItem(input: RemoveItemInput) {
const data = await fn({ wishlistId, itemId: input.id ?? item?.id })
await mutate(data, false)
return data
},
[fn, mutate]
)
}
useRemoveItem.extend = extendHook
return useRemoveItem
}
export default extendHook(fetcher)

View File

@ -0,0 +1,11 @@
import useAddItem from './use-add-item'
import useRemoveItem from './use-remove-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 useWishlistActions(wishlistId: string) {
const addItem = useAddItem(wishlistId)
const removeItem = useRemoveItem(wishlistId)
return { addItem, removeItem }
}

View File

@ -0,0 +1,41 @@
import { HookFetcher } from '@lib/commerce/utils/types'
import useData from '@lib/commerce/utils/use-data'
import type { Wishlist } from '../api/wishlist'
const defaultOpts = {
url: '/api/bigcommerce/wishlists',
}
export type { Wishlist }
export type WishlistInput = {
wishlistId: string | undefined
}
export const fetcher: HookFetcher<Wishlist | null, WishlistInput> = (
options,
{ wishlistId },
fetch
) => {
return fetch({
url: options?.url,
body: { wishlistId },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useWishlists = (wishlistId: string) => {
const fetchFn: typeof fetcher = (options, input, fetch) => {
return customFetcher(options, input, fetch)
}
const response = useData(defaultOpts, [['wishlistId', wishlistId]], fetchFn)
return response
}
useWishlists.extend = extendHook
return useWishlists
}
export default extendHook(fetcher)

View File

@ -14,3 +14,37 @@ export function getRandomPairOfColors() {
// Returns a pair of colors
return [colors[idx], colors[idx2]]
}
function hexToRgb(hex: string = '') {
// @ts-ignore
const match = hex.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i)
if (!match) {
return [0, 0, 0]
}
let colorString = match[0]
if (match[0].length === 3) {
colorString = colorString
.split('')
.map((char: string) => {
return char + char
})
.join('')
}
const integer = parseInt(colorString, 16)
const r = (integer >> 16) & 0xff
const g = (integer >> 8) & 0xff
const b = integer & 0xff
return [r, g, b]
}
export function isDark(color = '') {
// Equation from http://24ways.org/2010/calculating-color-contrast
const rgb = hexToRgb(color)
const res = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000
return res < 128
}

View File

@ -19,10 +19,12 @@ export default function useCart<T>(
swrOptions?: ConfigInterface<T | null>
) {
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<T>

View File

@ -8,7 +8,7 @@ import {
} from 'react'
import { Fetcher } from './utils/types'
const Commerce = createContext<CommerceContextValue | null>(null)
const Commerce = createContext<CommerceContextValue | {}>({})
export type CommerceProps = {
children?: ReactNode

View File

@ -1,5 +1,18 @@
import bunyan from 'bunyan'
import PrettyStream from 'bunyan-prettystream'
const log = bunyan.createLogger({ name: 'Next.js - Commerce' })
const prettyStdOut = new PrettyStream()
const log = bunyan.createLogger({
name: 'Next.js - Commerce',
level: 'debug',
streams: [
{
level: 'debug',
type: 'raw',
stream: prettyStdOut,
},
],
})
export default log

View File

@ -22,15 +22,17 @@
"@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",
"classnames": "^2.2.6",
"cookie": "^0.4.1",
"js-cookie": "^2.2.1",
"lodash.debounce": "^4.0.8",
"next": "^9.5.4",
"next": "^9.5.6-canary.4",
"next-seo": "^4.11.0",
"next-themes": "^0.0.4",
"nextjs-progressbar": "^0.0.6",

View File

@ -9,22 +9,26 @@ 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 },
})
const { categories, brands } = await getSiteInfo()
return {
props: { pages, products, categories, brands },
props: { pages, products, featuredProducts, categories, brands },
}
}
export default function Home({
products,
featuredProducts,
categories,
brands,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<div className="mt-3">
<Grid>
{products.slice(0, 3).map((p: any) => (
{featuredProducts.slice(0, 3).map((p: any) => (
<ProductCard key={p.id} {...p} />
))}
</Grid>
@ -44,42 +48,44 @@ export default function Home({
Natural."
/>
<Grid layout="B">
{products.slice(3, 6).map((p: any) => (
{featuredProducts.slice(3, 6).map((p: any) => (
<ProductCard key={p.id} {...p} />
))}
</Grid>
<Marquee>
{products.slice(0, 3).map((p: any) => (
{products.slice(3, 6).map((p: any) => (
<ProductCard key={p.id} {...p} variant="slim" />
))}
</Marquee>
<div className="py-12 flex flex-row w-full px-12">
<div className="pr-3 w-48">
<ul className="mb-10">
<li className="py-1 text-base font-bold tracking-wide">
All Categories
</li>
{categories.map((cat) => (
<li key={cat.path} className="py-1 text-accents-8">
<a href="#">{cat.name}</a>
<div className="pr-3 w-48 relative">
<div className="sticky top-2">
<ul className="mb-10">
<li className="py-1 text-base font-bold tracking-wide">
All Categories
</li>
))}
</ul>
<ul className="">
<li className="py-1 text-base font-bold tracking-wide">
All Designers
</li>
{brands.flatMap(({ node }) => (
<li key={node.path} className="py-1 text-accents-8">
<a href="#">{node.name}</a>
{categories.map((cat) => (
<li key={cat.path} className="py-1 text-accents-8">
<a href="#">{cat.name}</a>
</li>
))}
</ul>
<ul className="">
<li className="py-1 text-base font-bold tracking-wide">
All Designers
</li>
))}
</ul>
{brands.flatMap(({ node }) => (
<li key={node.path} className="py-1 text-accents-8">
<a href="#">{node.name}</a>
</li>
))}
</ul>
</div>
</div>
<div className="flex-1">
<Grid layout="normal">
{products.map((p: any) => (
<ProductCard key={p.id} {...p} />
<ProductCard key={p.id} {...p} variant="simple" />
))}
</Grid>
</div>

40
pages/login.tsx Normal file
View File

@ -0,0 +1,40 @@
import { Layout } from '@components/core'
import { Logo, Modal, Button } from '@components/ui'
export default function Login() {
return (
<div className="pb-20">
<Modal close={() => {}}>
<div className="h-80 w-80 flex flex-col justify-between py-3 px-3">
<div className="flex justify-center pb-12">
<Logo width="64px" height="64px" />
</div>
<div className="flex flex-col space-y-3">
<div className="border border-accents-3 text-accents-6">
<input
placeholder="Email"
className="focus:outline-none focus:shadow-outline-gray border-none py-2 px-6 w-full appearance-none transition duration-150 ease-in-out placeholder-accents-5 pr-10"
/>
</div>
<div className="border border-accents-3 text-accents-6">
<input
placeholder="Password"
className="focus:outline-none focus:shadow-outline-gray border-none py-2 px-6 w-full appearance-none transition duration-150 ease-in-out placeholder-accents-5 pr-10"
/>
</div>
<Button variant="slim">Log In</Button>
<span className="pt-3 text-center text-sm">
<span className="text-accents-7">Don't have an account?</span>
{` `}
<a className="text-accent-9 font-bold hover:underline cursor-pointer">
Sign Up
</a>
</span>
</div>
</div>
</Modal>
</div>
)
}
Login.Layout = Layout

View File

@ -149,7 +149,7 @@ export default function Search({
))}
</Grid>
) : (
<Grid>
<Grid layout="normal">
{range(12).map(() => (
<Skeleton
className="w-full animate__animated animate__fadeIn"

View File

@ -1397,6 +1397,26 @@
is-promise "4.0.0"
tslib "~2.0.1"
"@hapi/accept@5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.1.tgz#068553e867f0f63225a506ed74e899441af53e10"
integrity sha512-fMr4d7zLzsAXo28PRRQPXR1o2Wmu+6z+VY1UzDp0iFo13Twj8WePakwXBiqn3E1aAlTpSNzCXdnnQXFhst8h8Q==
dependencies:
"@hapi/boom" "9.x.x"
"@hapi/hoek" "9.x.x"
"@hapi/boom@9.x.x":
version "9.1.0"
resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.0.tgz#0d9517657a56ff1e0b42d0aca9da1b37706fec56"
integrity sha512-4nZmpp4tXbm162LaZT45P7F7sgiem8dwAh2vHWT6XX24dozNjGMg6BvKCRvtCUcmcXqeMIUqWN8Rc5X8yKuROQ==
dependencies:
"@hapi/hoek" "9.x.x"
"@hapi/hoek@9.x.x":
version "9.1.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6"
integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw==
"@headlessui/react@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-0.2.0.tgz#a31f90892d736243ba91c1474f534b3256d0c538"
@ -1412,20 +1432,20 @@
meow "^7.0.0"
prettier "^2.0.5"
"@next/env@9.5.4":
version "9.5.4"
resolved "https://registry.yarnpkg.com/@next/env/-/env-9.5.4.tgz#950f3370151a940ecac6e7e19cf125e6113e101e"
integrity sha512-uGnUO68/u9C8bqHj5obIvyGRDqe/jh1dFSLx03mJmlESjcCmV4umXYJOnt3XzU1VhVntSE+jUZtnS5bjYmmLfQ==
"@next/env@9.5.6-canary.4":
version "9.5.6-canary.4"
resolved "https://registry.yarnpkg.com/@next/env/-/env-9.5.6-canary.4.tgz#785956d1e2d26e25377a2dcafbd3fe785cd88c35"
integrity sha512-c+JrotEtdgcaF6m4xSZ6D7eY4NWSDBsqcaLwkKAXh+pSEHfH2eKnZv7cRIaR3s7Ll37gE/RNfXGVC2l43vCcuw==
"@next/polyfill-module@9.5.4":
version "9.5.4"
resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-9.5.4.tgz#35ea31ce5f6bbf0ac31aac483b60d4ba17a79861"
integrity sha512-GA2sW7gs33s7RGPFqkMiT9asYpaV/Hhw9+XM9/UlPrkNdTaxZWaPa2iHgmqJ7k6OHiOmy+CBLFrUBgzqKNhs3Q==
"@next/polyfill-module@9.5.6-canary.4":
version "9.5.6-canary.4"
resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-9.5.6-canary.4.tgz#6144ee81ae42aa44905f6c2b864ff5be19c67c38"
integrity sha512-r+kAXpF8AjVZGanJBwveCR5GCqnNIjA0WBsotat4MP+KKQwrptocRYsP+eZZo7gyhHEwSMdMag2NWahqjW+Vlg==
"@next/react-dev-overlay@9.5.4":
version "9.5.4"
resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-9.5.4.tgz#7d88a710d23021020cca213bc77106df18950b2b"
integrity sha512-tYvNmOQ0inykSvcimkTiONMv4ZyFB2G2clsy9FKLLRZ2OA+Jiov6T7Pq6YpKbBwTLu/BQGVc7Qn4BZ5CDHR8ig==
"@next/react-dev-overlay@9.5.6-canary.4":
version "9.5.6-canary.4"
resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-9.5.6-canary.4.tgz#2218f9a9d5874e838d24178f657ec6dd8e1aa961"
integrity sha512-zYKkkdWi2rSyWYnOn7BEAQeZwOP2O6wHC4A/nB2YdRBDvD9p3qLawmribuiQ7vspPmAIgv2JgQ8DauC5YQU7DA==
dependencies:
"@babel/code-frame" "7.10.4"
ally.js "1.4.1"
@ -1438,10 +1458,10 @@
stacktrace-parser "0.1.10"
strip-ansi "6.0.0"
"@next/react-refresh-utils@9.5.4":
version "9.5.4"
resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-9.5.4.tgz#3bfe067f0cfc717f079482d956211708c9e81126"
integrity sha512-TPhEiYxK5YlEuzVuTzgZiDN7SDh4drvUAqsO9Yccd8WLcfYqOLRN2fCALremW5mNLAZQZW3iFgW8PW8Gckq4EQ==
"@next/react-refresh-utils@9.5.6-canary.4":
version "9.5.6-canary.4"
resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-9.5.6-canary.4.tgz#098a55714d906248ef46e9a1c077a1dc6e231331"
integrity sha512-hxSF3/lWX/a+6vPLViklLK6DNDzRzd8T/EUJpkUgLdwdHnABKFB6p9m1qvdGuqBQug2RNDcLQy7oKucy42tpVQ==
"@nodelib/fs.scandir@2.1.3":
version "2.1.3"
@ -2095,6 +2115,13 @@
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
"@types/bunyan-prettystream@^0.1.31":
version "0.1.31"
resolved "https://registry.yarnpkg.com/@types/bunyan-prettystream/-/bunyan-prettystream-0.1.31.tgz#3864836abb907ab151f7edf7c64c323c9609e1d1"
integrity sha512-NE7fq2ZcX7OSMK+VhTNJkVEHlo+hm0uVXpuLeH1ifGm52Qwuo/kLD2GHo7UcEXMFu3duKver/AFo8C4TME93zw==
dependencies:
"@types/node" "*"
"@types/bunyan@^1.8.6":
version "1.8.6"
resolved "https://registry.yarnpkg.com/@types/bunyan/-/bunyan-1.8.6.tgz#6527641cca30bedec5feb9ab527b7803b8000582"
@ -2977,6 +3004,11 @@ bufferutil@^4.0.1:
dependencies:
node-gyp-build "~3.7.0"
bunyan-prettystream@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/bunyan-prettystream/-/bunyan-prettystream-0.1.3.tgz#6c3b713266f6ad32007c7b6ab1e998a245349d98"
integrity sha1-bDtxMmb2rTIAfHtqsemYokU0nZg=
bunyan@^1.8.14:
version "1.8.14"
resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.14.tgz#3d8c1afea7de158a5238c7cb8a66ab6b38dd45b4"
@ -5887,10 +5919,10 @@ next-tick@~1.0.0:
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
next@^9.5.4:
version "9.5.4"
resolved "https://registry.yarnpkg.com/next/-/next-9.5.4.tgz#3c6aa3fd38ff1711e956ea2b6833475e0262ec35"
integrity sha512-dicsJSxiUFcRjeZ/rNMAO3HS5ttFFuRHhdAn5g7lHnWUZ3MnEX4ggBIihaoUr6qu2So9KoqUPXpS91MuSXUmBw==
next@^9.5.6-canary.4:
version "9.5.6-canary.4"
resolved "https://registry.yarnpkg.com/next/-/next-9.5.6-canary.4.tgz#af3ed55845f6005ac155f7c5d9d21b7493be7141"
integrity sha512-sd29wpQer1Ha9EWpvrBvN6XXmLtJnR5JWAyYnlBmrA835BolN0cQw6++sOGcdTJZV5+ZWbhTEzBVOUTKsSLd5w==
dependencies:
"@ampproject/toolbox-optimizer" "2.6.0"
"@babel/code-frame" "7.10.4"
@ -5910,10 +5942,11 @@ next@^9.5.4:
"@babel/preset-typescript" "7.10.4"
"@babel/runtime" "7.11.2"
"@babel/types" "7.11.5"
"@next/env" "9.5.4"
"@next/polyfill-module" "9.5.4"
"@next/react-dev-overlay" "9.5.4"
"@next/react-refresh-utils" "9.5.4"
"@hapi/accept" "5.0.1"
"@next/env" "9.5.6-canary.4"
"@next/polyfill-module" "9.5.6-canary.4"
"@next/react-dev-overlay" "9.5.6-canary.4"
"@next/react-refresh-utils" "9.5.6-canary.4"
ast-types "0.13.2"
babel-plugin-transform-define "2.0.0"
babel-plugin-transform-react-remove-prop-types "0.4.24"