forked from crowetic/commerce
Merge branch 'master' into arzafran/tweak-banner
This commit is contained in:
commit
0a270e7d23
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)}
|
||||
>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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;
|
||||
|
@ -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}>
|
||||
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
@ -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 {
|
||||
|
@ -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 />
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 && (
|
||||
|
@ -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"
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
& > * {
|
||||
@apply flex-1 px-16 py-4;
|
||||
width: 400px;
|
||||
width: 430px;
|
||||
}
|
||||
}
|
||||
|
||||
|
8
components/ui/Modal/Modal.module.css
Normal file
8
components/ui/Modal/Modal.module.css
Normal 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;
|
||||
}
|
52
components/ui/Modal/Modal.tsx
Normal file
52
components/ui/Modal/Modal.tsx
Normal 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
|
1
components/ui/Modal/index.ts
Normal file
1
components/ui/Modal/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Modal'
|
@ -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'
|
||||
|
@ -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, {})
|
||||
|
@ -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}
|
||||
`
|
||||
|
@ -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>),
|
||||
|
30
lib/bigcommerce/api/wishlist/handlers/add-item.ts
Normal file
30
lib/bigcommerce/api/wishlist/handlers/add-item.ts
Normal 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
|
25
lib/bigcommerce/api/wishlist/handlers/add-wishlist.ts
Normal file
25
lib/bigcommerce/api/wishlist/handlers/add-wishlist.ts
Normal 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
|
22
lib/bigcommerce/api/wishlist/handlers/get-all-wishlists.ts
Normal file
22
lib/bigcommerce/api/wishlist/handlers/get-all-wishlists.ts
Normal 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
|
20
lib/bigcommerce/api/wishlist/handlers/get-wishlist.ts
Normal file
20
lib/bigcommerce/api/wishlist/handlers/get-wishlist.ts
Normal 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
|
25
lib/bigcommerce/api/wishlist/handlers/remove-item.ts
Normal file
25
lib/bigcommerce/api/wishlist/handlers/remove-item.ts
Normal 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
|
25
lib/bigcommerce/api/wishlist/handlers/remove-wishlist.ts
Normal file
25
lib/bigcommerce/api/wishlist/handlers/remove-wishlist.ts
Normal 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
|
27
lib/bigcommerce/api/wishlist/handlers/update-wishlist.ts
Normal file
27
lib/bigcommerce/api/wishlist/handlers/update-wishlist.ts
Normal 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
|
152
lib/bigcommerce/api/wishlist/index.ts
Normal file
152
lib/bigcommerce/api/wishlist/index.ts
Normal 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, {})
|
47
lib/bigcommerce/schema.d.ts
vendored
47
lib/bigcommerce/schema.d.ts
vendored
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
46
lib/bigcommerce/wishlist/use-add-item.tsx
Normal file
46
lib/bigcommerce/wishlist/use-add-item.tsx
Normal 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)
|
51
lib/bigcommerce/wishlist/use-remove-item.tsx
Normal file
51
lib/bigcommerce/wishlist/use-remove-item.tsx
Normal 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)
|
11
lib/bigcommerce/wishlist/use-wishlist-actions.tsx
Normal file
11
lib/bigcommerce/wishlist/use-wishlist-actions.tsx
Normal 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 }
|
||||
}
|
41
lib/bigcommerce/wishlist/use-wishlist.tsx
Normal file
41
lib/bigcommerce/wishlist/use-wishlist.tsx
Normal 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)
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
40
pages/login.tsx
Normal 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
|
@ -149,7 +149,7 @@ export default function Search({
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Grid>
|
||||
<Grid layout="normal">
|
||||
{range(12).map(() => (
|
||||
<Skeleton
|
||||
className="w-full animate__animated animate__fadeIn"
|
||||
|
81
yarn.lock
81
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user