forked from crowetic/commerce
Merge branch 'master' into arzafran/tweak-banner
This commit is contained in:
commit
e6f411f671
@ -59,7 +59,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fit {
|
.fit {
|
||||||
min-height: calc(100vh - 300px);
|
min-height: calc(100vh - 88px - 41px);
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
|
@ -22,10 +22,6 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-black text-white">
|
<div className="bg-black text-white">
|
||||||
<hr
|
|
||||||
className="hidden md:block mt-4 border-gray-700"
|
|
||||||
style={{ flexBasis: '100%', height: 0 }}
|
|
||||||
/>
|
|
||||||
<footer className={rootClassName}>
|
<footer className={rootClassName}>
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a className="flex flex-initial items-center md:items-start font-bold md:mr-24">
|
<a className="flex flex-initial items-center md:items-start font-bold md:mr-24">
|
||||||
|
@ -20,7 +20,7 @@ const Navbar: FC<Props> = ({ className }) => {
|
|||||||
<Logo />
|
<Logo />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="space-x-4 ml-6 hidden md:block">
|
<nav className="space-x-4 ml-6 sm:hidden lg:block">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a className={s.link}>All</a>
|
<a className={s.link}>All</a>
|
||||||
</Link>
|
</Link>
|
||||||
@ -33,11 +33,11 @@ const Navbar: FC<Props> = ({ className }) => {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:flex flex-1 justify-center hidden">
|
<div className="lg:flex flex-1 justify-center sm:hidden">
|
||||||
<Searchbar />
|
<Searchbar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-initial md:flex-1 justify-end space-x-8">
|
<div className="flex flex-1 justify-end space-x-8">
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={theme === 'dark'}
|
checked={theme === 'dark'}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
@ -47,7 +47,8 @@ const Navbar: FC<Props> = ({ className }) => {
|
|||||||
<UserNav />
|
<UserNav />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="block flex pb-4 md:hidden px-4 md:px-6">
|
|
||||||
|
<div className="sm:flex pb-4 lg:px-6 lg:hidden">
|
||||||
<Searchbar />
|
<Searchbar />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,30 +1,24 @@
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import s from './ProductCard.module.css'
|
import s from './ProductCard.module.css'
|
||||||
import { FC, ReactNode, Component } from 'react'
|
import { FC, ReactNode, Component } from 'react'
|
||||||
|
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-all-products'
|
||||||
import { Heart } from '@components/icon'
|
import { Heart } from '@components/icon'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
children?: ReactNode[] | Component[] | any[]
|
children?: ReactNode[] | Component[] | any[]
|
||||||
node: ProductData
|
product: ProductNode
|
||||||
variant?: 'slim' | 'simple'
|
variant?: 'slim' | 'simple'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductData {
|
const ProductCard: FC<Props> = ({ className, product: p, variant }) => {
|
||||||
name: string
|
|
||||||
images: any
|
|
||||||
prices: any
|
|
||||||
path: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProductCard: FC<Props> = ({ className, node: p, variant }) => {
|
|
||||||
if (variant === 'slim') {
|
if (variant === 'slim') {
|
||||||
return (
|
return (
|
||||||
<div className="relative overflow-hidden box-border">
|
<div className="relative overflow-hidden box-border">
|
||||||
<img
|
<img
|
||||||
className="object-scale-down h-48"
|
className="object-scale-down h-48"
|
||||||
src={p.images.edges[0].node.urlSmall}
|
src={p.images.edges?.[0]?.node.urlSmall}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 flex items-center justify-end mr-8">
|
<div className="absolute inset-0 flex items-center justify-end mr-8">
|
||||||
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
|
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
|
||||||
@ -41,7 +35,7 @@ const ProductCard: FC<Props> = ({ className, node: p, variant }) => {
|
|||||||
<div className="absolute z-10 inset-0 flex items-center justify-center">
|
<div className="absolute z-10 inset-0 flex items-center justify-center">
|
||||||
<img
|
<img
|
||||||
className="w-full object-cover"
|
className="w-full object-cover"
|
||||||
src={p.images.edges[0].node.urlXL}
|
src={p.images.edges?.[0]?.node.urlXL}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(s.squareBg, { [s.gray]: variant === 'simple' })} />
|
<div className={cn(s.squareBg, { [s.gray]: variant === 'simple' })} />
|
||||||
@ -50,7 +44,7 @@ const ProductCard: FC<Props> = ({ className, node: p, variant }) => {
|
|||||||
<p className={s.productTitle}>
|
<p className={s.productTitle}>
|
||||||
<span>{p.name}</span>
|
<span>{p.name}</span>
|
||||||
</p>
|
</p>
|
||||||
<span className={s.productPrice}>${p.prices.price.value}</span>
|
<span className={s.productPrice}>${p.prices?.price.value}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={s.wishlistButton}>
|
<div className={s.wishlistButton}>
|
||||||
<Heart />
|
<Heart />
|
||||||
|
22
components/product/ProductSlider/ProductSlider.module.css
Normal file
22
components/product/ProductSlider/ProductSlider.module.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.root {
|
||||||
|
@apply relative w-full h-full;
|
||||||
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rootPanel {
|
||||||
|
@apply absolute flex flex-row inset-0 z-20 m-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftPanel {
|
||||||
|
@apply flex-1;
|
||||||
|
cursor: url('/cursor-left.png'), auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightPanel {
|
||||||
|
@apply flex-1;
|
||||||
|
cursor: url('/cursor-right.png'), auto;
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import { FC, useState } from 'react'
|
import React, { FC, useState } from 'react'
|
||||||
import React from 'react'
|
|
||||||
import SwipeableViews from 'react-swipeable-views'
|
import SwipeableViews from 'react-swipeable-views'
|
||||||
|
import s from './ProductSlider.module.css'
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: any
|
children?: any
|
||||||
}
|
}
|
||||||
@ -19,14 +18,19 @@ const ProductSlider: FC<Props> = ({ children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full">
|
<div className={s.root}>
|
||||||
<div className="absolute flex flex-row inset-0 z-10 opacity-0">
|
<SwipeableViews
|
||||||
<div className="flex-1 bg-cyan" onClick={goBack}></div>
|
index={idx}
|
||||||
<div className="flex-1 bg-pink" onClick={goNext}></div>
|
onChangeIndex={setIdx}
|
||||||
</div>
|
containerStyle={{ overflow: 'visible' }}
|
||||||
<SwipeableViews index={idx} onChangeIndex={setIdx}>
|
slideStyle={{ overflow: 'visible' }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</SwipeableViews>
|
</SwipeableViews>
|
||||||
|
<div className={s.rootPanel}>
|
||||||
|
<div className={s.leftPanel} onClick={goBack}></div>
|
||||||
|
<div className={s.rightPanel} onClick={goNext}></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,89 @@
|
|||||||
.button {
|
.root {
|
||||||
min-width: 300px;
|
@apply relative grid items-start gap-8 grid-cols-1;
|
||||||
|
|
||||||
|
@screen lg {
|
||||||
|
@apply grid-cols-12 pt-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.productDisplay {
|
||||||
|
@apply relative flex px-0 pb-0 relative box-border col-span-7;
|
||||||
|
margin-right: -2rem;
|
||||||
|
margin-left: -2rem;
|
||||||
|
min-height: 400px;
|
||||||
|
|
||||||
|
@screen md {
|
||||||
|
min-height: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen lg {
|
||||||
|
@apply mx-0;
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.squareBg {
|
.squareBg {
|
||||||
@apply absolute inset-24 z-0 bg-violet;
|
@apply absolute inset-0 bg-violet z-0;
|
||||||
|
max-height: 250px;
|
||||||
|
|
||||||
|
@screen md {
|
||||||
|
@apply inset-20;
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameBox {
|
||||||
|
@apply absolute top-6 left-0 z-50;
|
||||||
|
|
||||||
|
& .name {
|
||||||
|
@apply px-6 py-2 bg-primary text-primary font-bold text-3xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .price {
|
||||||
|
@apply px-6 py-2 pb-4 bg-primary text-primary font-bold inline-block tracking-wide;
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen md {
|
||||||
|
& .name,
|
||||||
|
& .price {
|
||||||
|
@apply bg-violet text-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
@apply flex flex-col col-span-5;
|
||||||
|
|
||||||
|
@screen lg {
|
||||||
|
padding-top: 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliderContainer {
|
||||||
|
@apply absolute z-10 inset-0 flex items-center justify-center overflow-x-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
@apply w-full h-full object-cover;
|
||||||
|
|
||||||
|
@screen md {
|
||||||
|
height: 100%;
|
||||||
|
margin-top: -2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen lg {
|
||||||
|
height: 150%;
|
||||||
|
margin-top: -10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen xl {
|
||||||
|
height: 170%;
|
||||||
|
margin-top: -19%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
min-width: 300px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,35 @@
|
|||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState, useEffect } from 'react'
|
||||||
import s from './ProductView.module.css'
|
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import { Button, Container } from '@components/ui'
|
import { Button, Container } from '@components/ui'
|
||||||
import { Swatch, ProductSlider } from '@components/product'
|
import { Swatch, ProductSlider } from '@components/product'
|
||||||
import useAddItem from '@lib/bigcommerce/cart/use-add-item'
|
import useAddItem from '@lib/bigcommerce/cart/use-add-item'
|
||||||
import type { Product } from '@lib/bigcommerce/api/operations/get-product'
|
|
||||||
import { getProductOptions } from '../helpers'
|
import { getProductOptions } from '../helpers'
|
||||||
|
import s from './ProductView.module.css'
|
||||||
|
import { isDesktop } from '@lib/browser'
|
||||||
|
import cn from 'classnames'
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
children?: any
|
children?: any
|
||||||
product: Product
|
product: ProductNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductView: FC<Props> = ({ product, className }) => {
|
const ProductView: FC<Props> = ({ product, className }) => {
|
||||||
const addItem = useAddItem()
|
const addItem = useAddItem()
|
||||||
const { openSidebar } = useUI()
|
const { openSidebar } = useUI()
|
||||||
const options = getProductOptions(product)
|
const options = getProductOptions(product)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [validMedia, setValidMedia] = useState(false)
|
||||||
|
|
||||||
const [choices, setChoices] = useState<Record<string, any>>({
|
const [choices, setChoices] = useState<Record<string, any>>({
|
||||||
size: null,
|
size: null,
|
||||||
color: null,
|
color: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
useEffect(() => {
|
||||||
|
setValidMedia(isDesktop())
|
||||||
|
}, [])
|
||||||
|
|
||||||
const addToCart = async () => {
|
const addToCart = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@ -59,39 +64,40 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
|||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="relative flex flex-row items-start fit my-12">
|
<div className={cn(s.root, 'fit')}>
|
||||||
<div className="absolute top-0 left-0 z-50">
|
<div className={cn(s.productDisplay, 'fit')}>
|
||||||
<h1 className="px-6 py-2 bg-violet text-white font-bold text-3xl">
|
<div className={s.squareBg}></div>
|
||||||
{product.name}
|
<div className={s.nameBox}>
|
||||||
</h1>
|
<h1 className={s.name}>{product.name}</h1>
|
||||||
<div className="px-6 py-2 pb-4 bg-violet text-white font-bold inline-block traking">
|
<div className={s.price}>
|
||||||
{product.prices?.price.value}
|
{product.prices?.price.value}
|
||||||
{` `}
|
{` `}
|
||||||
{product.prices?.price.currencyCode}
|
{product.prices?.price.currencyCode}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 px-24 pb-0 relative fit box-border">
|
<div className={s.sliderContainer}>
|
||||||
<div className="absolute z-10 inset-0 flex items-center justify-center">
|
|
||||||
<ProductSlider>
|
<ProductSlider>
|
||||||
{/** TODO: Change with Image Component */}
|
{/** TODO: Change with Image Component **/}
|
||||||
{product.images.edges?.map((image, i) => (
|
{product.images.edges?.map((image, i) => (
|
||||||
<img
|
<img
|
||||||
key={image?.node.urlSmall}
|
key={image?.node.urlSmall}
|
||||||
className="w-full object-cover"
|
className={s.img}
|
||||||
src={image?.node.urlXL}
|
src={image?.node.urlXL}
|
||||||
loading={i === 0 ? 'eager' : 'lazy'}
|
loading={i === 0 ? 'eager' : 'lazy'}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ProductSlider>
|
</ProductSlider>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute z-10 bottom-10 left-1/2 transform -translate-x-1/2 inline-block">
|
|
||||||
<img src="/slider-arrows.png" />
|
{!validMedia && (
|
||||||
</div>
|
<div className="absolute z-10 bottom-10 left-1/2 transform -translate-x-1/2 inline-block">
|
||||||
<div className={s.squareBg}></div>
|
<img src="/slider-arrows.png" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col pt-24">
|
<div className={s.sidebar}>
|
||||||
<section>
|
<section>
|
||||||
{options?.map((opt: any) => (
|
{options?.map((opt: any) => (
|
||||||
<div className="pb-4" key={opt.displayName}>
|
<div className="pb-4" key={opt.displayName}>
|
||||||
|
@ -1,13 +1,32 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply h-12 w-12 bg-primary text-primary rounded-full mr-3 inline-flex
|
@apply h-12 w-12 bg-primary text-primary rounded-full mr-3 inline-flex
|
||||||
items-center justify-center cursor-pointer transition duration-100 ease-in-out
|
items-center justify-center cursor-pointer transition duration-100 ease-in-out
|
||||||
p-0 shadow-none border-gray-200 border box-border text-black;
|
p-0 shadow-none border-gray-200 border box-border;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
@apply absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply transform scale-110 bg-hover;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.active.size {
|
.color {
|
||||||
@apply border-accents-9 border-2;
|
@apply text-black transition duration-100 ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply text-black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dark,
|
||||||
|
&.dark:hover {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.root:hover {
|
.active {
|
||||||
@apply transform scale-110 bg-hover;
|
&.size {
|
||||||
|
@apply border-accents-9 border-2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ interface Props {
|
|||||||
|
|
||||||
const Swatch: FC<Props & ButtonProps> = ({
|
const Swatch: FC<Props & ButtonProps> = ({
|
||||||
className,
|
className,
|
||||||
color,
|
color = '',
|
||||||
label,
|
label,
|
||||||
variant = 'size',
|
variant = 'size',
|
||||||
active,
|
active,
|
||||||
@ -23,13 +23,14 @@ const Swatch: FC<Props & ButtonProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
variant = variant?.toLowerCase()
|
variant = variant?.toLowerCase()
|
||||||
label = label?.toLowerCase()
|
label = label?.toLowerCase()
|
||||||
const isDarkBg = isDark(color)
|
|
||||||
|
|
||||||
const rootClassName = cn(
|
const rootClassName = cn(
|
||||||
s.root,
|
s.root,
|
||||||
{
|
{
|
||||||
[s.active]: active,
|
[s.active]: active,
|
||||||
[s.size]: variant === 'size',
|
[s.size]: variant === 'size',
|
||||||
|
[s.color]: color,
|
||||||
|
[s.dark]: color ? isDark(color) : false,
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
)
|
)
|
||||||
@ -41,11 +42,7 @@ const Swatch: FC<Props & ButtonProps> = ({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{variant === 'color' && active && (
|
{variant === 'color' && active && (
|
||||||
<span
|
<span>
|
||||||
className={cn('absolute', {
|
|
||||||
'text-white': isDarkBg,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Check />
|
<Check />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { Product } from '@lib/bigcommerce/api/operations/get-product'
|
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
||||||
|
|
||||||
export function getProductOptions(product: Product) {
|
export function getProductOptions(product: ProductNode) {
|
||||||
// console.log(product)
|
// console.log(product)
|
||||||
const options = product.productOptions.edges?.map(({ node }: any) => ({
|
const options = product.productOptions.edges?.map(({ node }: any) => ({
|
||||||
displayName: node.displayName.toLowerCase(),
|
displayName: node.displayName.toLowerCase(),
|
||||||
|
@ -52,7 +52,7 @@ const Sidebar: FC<Props> = ({ className, children, show = true, close }) => {
|
|||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-black bg-opacity-25 transition-opacity"
|
className="absolute inset-0 bg-black bg-opacity-50 transition-opacity"
|
||||||
// Close the sidebar when clicking on the backdrop
|
// Close the sidebar when clicking on the backdrop
|
||||||
onClick={close}
|
onClick={close}
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
import getAllProducts, {
|
import getAllProducts, { ProductEdge } from '../../operations/get-all-products'
|
||||||
Products,
|
|
||||||
Product,
|
|
||||||
} from '../../operations/get-all-products'
|
|
||||||
import type { ProductsHandlers } from '../products'
|
import type { ProductsHandlers } from '../products'
|
||||||
|
|
||||||
const SORT: { [key: string]: string | undefined } = {
|
const SORT: { [key: string]: string | undefined } = {
|
||||||
@ -52,16 +49,16 @@ const getProducts: ProductsHandlers['getProducts'] = async ({
|
|||||||
// We want the GraphQL version of each product
|
// We want the GraphQL version of each product
|
||||||
const graphqlData = await getAllProducts({
|
const graphqlData = await getAllProducts({
|
||||||
variables: { first: LIMIT, entityIds },
|
variables: { first: LIMIT, entityIds },
|
||||||
|
config,
|
||||||
})
|
})
|
||||||
// Put the products in an object that we can use to get them by id
|
// Put the products in an object that we can use to get them by id
|
||||||
const productsById = graphqlData.products.reduce<{ [k: number]: Product }>(
|
const productsById = graphqlData.products.reduce<{
|
||||||
(prods, p) => {
|
[k: number]: ProductEdge
|
||||||
prods[p.node.entityId] = p
|
}>((prods, p) => {
|
||||||
return prods
|
prods[p.node.entityId] = p
|
||||||
},
|
return prods
|
||||||
{}
|
}, {})
|
||||||
)
|
const products: ProductEdge[] = found ? [] : graphqlData.products
|
||||||
const products: Products = found ? [] : graphqlData.products
|
|
||||||
|
|
||||||
// Populate the products array with the graphql products, in the order
|
// Populate the products array with the graphql products, in the order
|
||||||
// assigned by the list of entity ids
|
// assigned by the list of entity ids
|
||||||
|
@ -4,11 +4,11 @@ import createApiHandler, {
|
|||||||
BigcommerceHandler,
|
BigcommerceHandler,
|
||||||
} from '../utils/create-api-handler'
|
} from '../utils/create-api-handler'
|
||||||
import { BigcommerceApiError } from '../utils/errors'
|
import { BigcommerceApiError } from '../utils/errors'
|
||||||
import type { Products } from '../operations/get-all-products'
|
import type { ProductEdge } from '../operations/get-all-products'
|
||||||
import getProducts from './handlers/get-products'
|
import getProducts from './handlers/get-products'
|
||||||
|
|
||||||
export type SearchProductsData = {
|
export type SearchProductsData = {
|
||||||
products: Products
|
products: ProductEdge[]
|
||||||
found: boolean
|
found: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
import createApiHandler, {
|
|
||||||
BigcommerceApiHandler,
|
|
||||||
BigcommerceHandler,
|
|
||||||
} from './utils/create-api-handler'
|
|
||||||
import isAllowedMethod from './utils/is-allowed-method'
|
|
||||||
import { BigcommerceApiError } from './utils/errors'
|
|
||||||
|
|
||||||
type Body<T> = Partial<T> | undefined
|
|
||||||
|
|
||||||
export type Customer = any
|
|
||||||
|
|
||||||
export type AddCustomerBody = { item: any }
|
|
||||||
|
|
||||||
export type CartHandlers = {
|
|
||||||
addItem: BigcommerceHandler<Customer, { cartId?: string } & Body<any>>
|
|
||||||
}
|
|
||||||
|
|
||||||
const METHODS = ['POST']
|
|
||||||
|
|
||||||
const customersApi: BigcommerceApiHandler<Customer> = async (
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
config
|
|
||||||
) => {
|
|
||||||
if (!isAllowedMethod(req, res, METHODS)) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
// let result = {} as any
|
|
||||||
// const
|
|
||||||
// result = await config.storeApiFetch('/v3/customers')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
|
||||||
error instanceof BigcommerceApiError
|
|
||||||
? 'An unexpected error ocurred with the Bigcommerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createCustomer: BigcommerceHandler<Customer> = ({
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
body,
|
|
||||||
config,
|
|
||||||
}) => {}
|
|
||||||
|
|
||||||
const handlers = {
|
|
||||||
createCustomer,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createApiHandler(customersApi, handlers, {})
|
|
67
lib/bigcommerce/api/customers/handlers/create-customer.ts
Normal file
67
lib/bigcommerce/api/customers/handlers/create-customer.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { BigcommerceApiError } from '../../utils/errors'
|
||||||
|
import login from '../../operations/login'
|
||||||
|
import type { CustomersHandlers } from '..'
|
||||||
|
|
||||||
|
const createCustomer: CustomersHandlers['createCustomer'] = async ({
|
||||||
|
res,
|
||||||
|
body: { firstName, lastName, email, password },
|
||||||
|
config,
|
||||||
|
}) => {
|
||||||
|
// TODO: Add proper validations with something like Ajv
|
||||||
|
if (!(firstName && lastName && email && password)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Invalid request' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// TODO: validate the password and email
|
||||||
|
// Passwords must be at least 7 characters and contain both alphabetic
|
||||||
|
// and numeric characters.
|
||||||
|
|
||||||
|
let result: { data?: any } = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await config.storeApiFetch('/v3/customers', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify([
|
||||||
|
{
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
email,
|
||||||
|
authentication: {
|
||||||
|
new_password: password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof BigcommerceApiError && error.status === 422) {
|
||||||
|
const hasEmailError = '0.email' in error.data?.errors
|
||||||
|
|
||||||
|
// If there's an error with the email, it most likely means it's duplicated
|
||||||
|
if (hasEmailError) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: 'The email is already in use',
|
||||||
|
code: 'duplicated_email',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('DATA', result.data)
|
||||||
|
|
||||||
|
const loginData = await login({ variables: { email, password }, config })
|
||||||
|
|
||||||
|
console.log('LOGIN DATA', loginData)
|
||||||
|
|
||||||
|
res.status(200).json({ data: result.data ?? null })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createCustomer
|
58
lib/bigcommerce/api/customers/index.ts
Normal file
58
lib/bigcommerce/api/customers/index.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import createApiHandler, {
|
||||||
|
BigcommerceApiHandler,
|
||||||
|
BigcommerceHandler,
|
||||||
|
} from '../utils/create-api-handler'
|
||||||
|
import isAllowedMethod from '../utils/is-allowed-method'
|
||||||
|
import { BigcommerceApiError } from '../utils/errors'
|
||||||
|
import createCustomer from './handlers/create-customer'
|
||||||
|
|
||||||
|
type Body<T> = Partial<T> | undefined
|
||||||
|
|
||||||
|
export type Customer = null
|
||||||
|
|
||||||
|
export type CreateCustomerBody = {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomersHandlers = {
|
||||||
|
createCustomer: BigcommerceHandler<
|
||||||
|
Customer,
|
||||||
|
{ cartId?: string } & Body<CreateCustomerBody>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
|
const METHODS = ['POST']
|
||||||
|
|
||||||
|
const customersApi: BigcommerceApiHandler<Customer, CustomersHandlers> = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
config
|
||||||
|
) => {
|
||||||
|
if (!isAllowedMethod(req, res, METHODS)) return
|
||||||
|
|
||||||
|
const { cookies } = req
|
||||||
|
const cartId = cookies[config.cartCookie]
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const body = { cartId, ...req.body }
|
||||||
|
return await handlers['createCustomer']({ req, res, config, body })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
const message =
|
||||||
|
error instanceof BigcommerceApiError
|
||||||
|
? 'An unexpected error ocurred with the Bigcommerce API'
|
||||||
|
: 'An unexpected error ocurred'
|
||||||
|
|
||||||
|
res.status(500).json({ data: null, errors: [{ message }] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = { createCustomer }
|
||||||
|
|
||||||
|
export default createApiHandler(customersApi, handlers, {})
|
@ -1,7 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
GetAllProductsQuery,
|
GetAllProductsQuery,
|
||||||
GetAllProductsQueryVariables,
|
GetAllProductsQueryVariables,
|
||||||
} from 'lib/bigcommerce/schema'
|
} from '@lib/bigcommerce/schema'
|
||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||||
import filterEdges from '../utils/filter-edges'
|
import filterEdges from '../utils/filter-edges'
|
||||||
import { productConnectionFragment } from '../fragments/product'
|
import { productConnectionFragment } from '../fragments/product'
|
||||||
@ -43,14 +43,16 @@ export const getAllProductsQuery = /* GraphQL */ `
|
|||||||
${productConnectionFragment}
|
${productConnectionFragment}
|
||||||
`
|
`
|
||||||
|
|
||||||
export type Product = NonNullable<
|
export type ProductEdge = NonNullable<
|
||||||
NonNullable<GetAllProductsQuery['site']['products']['edges']>[0]
|
NonNullable<GetAllProductsQuery['site']['products']['edges']>[0]
|
||||||
>
|
>
|
||||||
|
|
||||||
export type Products = Product[]
|
export type ProductNode = ProductEdge['node']
|
||||||
|
|
||||||
export type GetAllProductsResult<
|
export type GetAllProductsResult<
|
||||||
T extends Record<keyof GetAllProductsResult, any[]> = { products: Products }
|
T extends Record<keyof GetAllProductsResult, any[]> = {
|
||||||
|
products: ProductEdge[]
|
||||||
|
}
|
||||||
> = T
|
> = T
|
||||||
|
|
||||||
const FIELDS = [
|
const FIELDS = [
|
||||||
|
@ -33,13 +33,13 @@ export const getProductQuery = /* GraphQL */ `
|
|||||||
${productInfoFragment}
|
${productInfoFragment}
|
||||||
`
|
`
|
||||||
|
|
||||||
export type Product = Extract<
|
export type ProductNode = Extract<
|
||||||
GetProductQuery['site']['route']['node'],
|
GetProductQuery['site']['route']['node'],
|
||||||
{ __typename: 'Product' }
|
{ __typename: 'Product' }
|
||||||
>
|
>
|
||||||
|
|
||||||
export type GetProductResult<
|
export type GetProductResult<
|
||||||
T extends { product?: any } = { product?: Product }
|
T extends { product?: any } = { product?: ProductNode }
|
||||||
> = T
|
> = T
|
||||||
|
|
||||||
export type ProductVariables = Images &
|
export type ProductVariables = Images &
|
||||||
|
51
lib/bigcommerce/api/operations/login.ts
Normal file
51
lib/bigcommerce/api/operations/login.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import type {
|
||||||
|
LoginMutation,
|
||||||
|
LoginMutationVariables,
|
||||||
|
} from 'lib/bigcommerce/schema'
|
||||||
|
import type { RecursivePartial } from '../utils/types'
|
||||||
|
import { BigcommerceConfig, getConfig } from '..'
|
||||||
|
|
||||||
|
export const loginMutation = /* GraphQL */ `
|
||||||
|
mutation login($email: String!, $password: String!) {
|
||||||
|
login(email: $email, password: $password) {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export type LoginResult<T extends { result?: any } = { result?: string }> = T
|
||||||
|
|
||||||
|
export type LoginVariables = LoginMutationVariables
|
||||||
|
|
||||||
|
async function login(opts: {
|
||||||
|
variables: LoginVariables
|
||||||
|
config?: BigcommerceConfig
|
||||||
|
}): Promise<LoginResult>
|
||||||
|
|
||||||
|
async function login<T extends { result?: any }, V = any>(opts: {
|
||||||
|
query: string
|
||||||
|
variables: V
|
||||||
|
config?: BigcommerceConfig
|
||||||
|
}): Promise<LoginResult<T>>
|
||||||
|
|
||||||
|
async function login({
|
||||||
|
query = loginMutation,
|
||||||
|
variables,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
query?: string
|
||||||
|
variables: LoginVariables
|
||||||
|
config?: BigcommerceConfig
|
||||||
|
}): Promise<LoginResult> {
|
||||||
|
config = getConfig(config)
|
||||||
|
|
||||||
|
const data = await config.fetch<RecursivePartial<LoginMutation>>(query, {
|
||||||
|
variables,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: data.login?.result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default login
|
@ -27,7 +27,7 @@ export type BigcommerceHandlers<T = any> = {
|
|||||||
|
|
||||||
export type BigcommerceApiResponse<T> = {
|
export type BigcommerceApiResponse<T> = {
|
||||||
data: T | null
|
data: T | null
|
||||||
errors?: { message: string }[]
|
errors?: { message: string; code?: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createApiHandler<
|
export default function createApiHandler<
|
||||||
|
@ -4,12 +4,14 @@ export class BigcommerceGraphQLError extends Error {}
|
|||||||
export class BigcommerceApiError extends Error {
|
export class BigcommerceApiError extends Error {
|
||||||
status: number
|
status: number
|
||||||
res: Response
|
res: Response
|
||||||
|
data: any
|
||||||
|
|
||||||
constructor(msg: string, res: Response) {
|
constructor(msg: string, res: Response, data?: any) {
|
||||||
super(msg)
|
super(msg)
|
||||||
this.name = 'BigcommerceApiError'
|
this.name = 'BigcommerceApiError'
|
||||||
this.status = res.status
|
this.status = res.status
|
||||||
this.res = res
|
this.res = res
|
||||||
|
this.data = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ export default async function fetchGraphqlApi<Q, V = any>(
|
|||||||
query: string,
|
query: string,
|
||||||
{ variables, preview }: CommerceAPIFetchOptions<V> = {}
|
{ variables, preview }: CommerceAPIFetchOptions<V> = {}
|
||||||
): Promise<Q> {
|
): Promise<Q> {
|
||||||
log.warn(query)
|
// log.warn(query)
|
||||||
const config = getConfig()
|
const config = getConfig()
|
||||||
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
|
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -23,7 +23,7 @@ export default async function fetchGraphqlApi<Q, V = any>(
|
|||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
if (json.errors) {
|
if (json.errors) {
|
||||||
console.error(json.errors)
|
console.error(json.errors)
|
||||||
throw new Error('Failed to fetch API')
|
throw new Error('Failed to fetch BigCommerce API')
|
||||||
}
|
}
|
||||||
return json.data
|
return json.data
|
||||||
}
|
}
|
||||||
|
@ -24,13 +24,22 @@ export default async function fetchStoreApi<T>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentType = res.headers.get('Content-Type')
|
||||||
|
const isJSON = contentType?.includes('application/json')
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new BigcommerceApiError(await getErrorText(res), res)
|
const data = isJSON ? await res.json() : await getTextOrNull(res)
|
||||||
|
const headers = getRawHeaders(res)
|
||||||
|
const msg = `Big Commerce API error (${
|
||||||
|
res.status
|
||||||
|
}) \nHeaders: ${JSON.stringify(headers, null, 2)}\n${
|
||||||
|
typeof data === 'string' ? data : JSON.stringify(data, null, 2)
|
||||||
|
}`
|
||||||
|
|
||||||
|
throw new BigcommerceApiError(msg, res, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = res.headers.get('Content-Type')
|
if (!isJSON) {
|
||||||
|
|
||||||
if (!contentType?.includes('application/json')) {
|
|
||||||
throw new BigcommerceApiError(
|
throw new BigcommerceApiError(
|
||||||
`Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`,
|
`Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`,
|
||||||
res
|
res
|
||||||
@ -41,12 +50,6 @@ export default async function fetchStoreApi<T>(
|
|||||||
return res.status === 204 ? null : await res.json()
|
return res.status === 204 ? null : await res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getErrorText(res: Response) {
|
|
||||||
return `Big Commerce API error (${res.status}) \n${JSON.stringify(
|
|
||||||
getRawHeaders(res)
|
|
||||||
)}\n ${await getTextOrNull(res)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRawHeaders(res: Response) {
|
function getRawHeaders(res: Response) {
|
||||||
const headers: { [key: string]: string } = {}
|
const headers: { [key: string]: string } = {}
|
||||||
|
|
||||||
|
9
lib/bigcommerce/schema.d.ts
vendored
9
lib/bigcommerce/schema.d.ts
vendored
@ -1926,3 +1926,12 @@ export type GetSiteInfoQuery = { __typename?: 'Query' } & {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LoginMutationVariables = Exact<{
|
||||||
|
email: Scalars['String']
|
||||||
|
password: Scalars['String']
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type LoginMutation = { __typename?: 'Mutation' } & {
|
||||||
|
login: { __typename?: 'LoginResult' } & Pick<LoginResult, 'result'>
|
||||||
|
}
|
||||||
|
16
lib/browser.ts
Normal file
16
lib/browser.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import * as Bowser from 'bowser'
|
||||||
|
|
||||||
|
export function isDesktop(): boolean {
|
||||||
|
const browser = Bowser.getParser(window.navigator.userAgent)
|
||||||
|
return browser.getPlatform().type === 'desktop'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMobile(): boolean {
|
||||||
|
const browser = Bowser.getParser(window.navigator.userAgent)
|
||||||
|
return browser.getPlatform().type === 'mobile'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTablet(): boolean {
|
||||||
|
const browser = Bowser.getParser(window.navigator.userAgent)
|
||||||
|
return browser.getPlatform().type === 'tablet'
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { random } from 'lodash'
|
import random from 'lodash.random'
|
||||||
|
|
||||||
export function getRandomPairOfColors() {
|
export function getRandomPairOfColors() {
|
||||||
const colors = ['#37B679', '#DA3C3C', '#3291FF', '#7928CA', '#79FFE1']
|
const colors = ['#37B679', '#DA3C3C', '#3291FF', '#7928CA', '#79FFE1']
|
||||||
|
7
lib/range-map.ts
Normal file
7
lib/range-map.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default function rangeMap(n: number, fn: (i: number) => any) {
|
||||||
|
const arr = []
|
||||||
|
while (n > arr.length) {
|
||||||
|
arr.push(fn(arr.length))
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
15
package.json
15
package.json
@ -21,25 +21,23 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^0.2.0",
|
"@headlessui/react": "^0.2.0",
|
||||||
"@tailwindcss/ui": "^0.6.2",
|
"@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",
|
"animate.css": "^4.1.1",
|
||||||
|
"bowser": "^2.11.0",
|
||||||
"bunyan": "^1.8.14",
|
"bunyan": "^1.8.14",
|
||||||
"bunyan-prettystream": "^0.1.3",
|
"bunyan-prettystream": "^0.1.3",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"cookie": "^0.4.1",
|
"cookie": "^0.4.1",
|
||||||
"js-cookie": "^2.2.1",
|
"js-cookie": "^2.2.1",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lodash.random": "^3.2.0",
|
||||||
"next": "^9.5.6-canary.4",
|
"next": "^9.5.6-canary.4",
|
||||||
"next-seo": "^4.11.0",
|
"next-seo": "^4.11.0",
|
||||||
"next-themes": "^0.0.4",
|
"next-themes": "^0.0.4",
|
||||||
"nextjs-progressbar": "^0.0.6",
|
"nextjs-progressbar": "^0.0.6",
|
||||||
"postcss-nesting": "^7.0.1",
|
"postcss-nesting": "^7.0.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.14.0",
|
||||||
"react-aria": "^3.0.0",
|
"react-aria": "^3.0.0",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.14.0",
|
||||||
"react-icons": "^3.11.0",
|
"react-icons": "^3.11.0",
|
||||||
"react-merge-refs": "^1.1.0",
|
"react-merge-refs": "^1.1.0",
|
||||||
"react-swipeable-views": "^0.13.9",
|
"react-swipeable-views": "^0.13.9",
|
||||||
@ -53,11 +51,16 @@
|
|||||||
"@graphql-codegen/typescript": "^1.17.10",
|
"@graphql-codegen/typescript": "^1.17.10",
|
||||||
"@graphql-codegen/typescript-operations": "^1.17.8",
|
"@graphql-codegen/typescript-operations": "^1.17.8",
|
||||||
"@manifoldco/swagger-to-ts": "^2.1.0",
|
"@manifoldco/swagger-to-ts": "^2.1.0",
|
||||||
|
"@types/bunyan": "^1.8.6",
|
||||||
|
"@types/bunyan-prettystream": "^0.1.31",
|
||||||
|
"@types/classnames": "^2.2.10",
|
||||||
"@types/cookie": "^0.4.0",
|
"@types/cookie": "^0.4.0",
|
||||||
"@types/js-cookie": "^2.2.6",
|
"@types/js-cookie": "^2.2.6",
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
|
"@types/lodash.random": "^3.2.6",
|
||||||
"@types/node": "^14.11.2",
|
"@types/node": "^14.11.2",
|
||||||
"@types/react": "^16.9.49",
|
"@types/react": "^16.9.49",
|
||||||
|
"@types/react-swipeable-views": "^0.13.0",
|
||||||
"graphql": "^15.3.0",
|
"graphql": "^15.3.0",
|
||||||
"postcss-flexbugs-fixes": "^4.2.1",
|
"postcss-flexbugs-fixes": "^4.2.1",
|
||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-preset-env": "^6.7.0",
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import '@assets/global.css'
|
import '@assets/global.css'
|
||||||
import '@assets/tailwind.css'
|
import '@assets/tailwind.css'
|
||||||
import '@assets/utils.css'
|
import '@assets/utils.css'
|
||||||
|
|
||||||
|
// To be removed
|
||||||
import 'animate.css'
|
import 'animate.css'
|
||||||
|
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import type { AppProps } from 'next/app'
|
import type { AppProps } from 'next/app'
|
||||||
|
|
||||||
|
3
pages/api/bigcommerce/customers.ts
Normal file
3
pages/api/bigcommerce/customers.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import customersApi from '@lib/bigcommerce/api/customers'
|
||||||
|
|
||||||
|
export default customersApi()
|
@ -1,40 +1,77 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
import { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
import { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
||||||
import getAllProducts from '@lib/bigcommerce/api/operations/get-all-products'
|
import getAllProducts from '@lib/bigcommerce/api/operations/get-all-products'
|
||||||
|
import getSiteInfo from '@lib/bigcommerce/api/operations/get-site-info'
|
||||||
|
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
||||||
|
import rangeMap from '@lib/range-map'
|
||||||
import { Layout } from '@components/core'
|
import { Layout } from '@components/core'
|
||||||
import { Grid, Marquee, Hero } from '@components/ui'
|
import { Grid, Marquee, Hero } from '@components/ui'
|
||||||
import { ProductCard } from '@components/product'
|
import { ProductCard } from '@components/product'
|
||||||
import getSiteInfo from '@lib/bigcommerce/api/operations/get-site-info'
|
|
||||||
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
|
||||||
|
|
||||||
export async function getStaticProps({ preview }: GetStaticPropsContext) {
|
export async function getStaticProps({ preview }: GetStaticPropsContext) {
|
||||||
const { pages } = await getAllPages()
|
|
||||||
const { products } = await getAllProducts()
|
|
||||||
const { products: featuredProducts } = await getAllProducts({
|
const { products: featuredProducts } = await getAllProducts({
|
||||||
variables: { field: 'featuredProducts', first: 3 },
|
variables: { field: 'featuredProducts', first: 6 },
|
||||||
|
})
|
||||||
|
const { products: bestSellingProducts } = await getAllProducts({
|
||||||
|
variables: { field: 'bestSellingProducts', first: 6 },
|
||||||
|
})
|
||||||
|
const { products: newestProducts } = await getAllProducts({
|
||||||
|
variables: { field: 'newestProducts', first: 12 },
|
||||||
})
|
})
|
||||||
const { categories, brands } = await getSiteInfo()
|
const { categories, brands } = await getSiteInfo()
|
||||||
|
const { pages } = await getAllPages()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { pages, products, featuredProducts, categories, brands },
|
props: {
|
||||||
|
featuredProducts,
|
||||||
|
bestSellingProducts,
|
||||||
|
newestProducts,
|
||||||
|
categories,
|
||||||
|
brands,
|
||||||
|
pages,
|
||||||
|
},
|
||||||
|
revalidate: 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nonNullable = (v: any) => v
|
||||||
|
|
||||||
export default function Home({
|
export default function Home({
|
||||||
products,
|
|
||||||
featuredProducts,
|
featuredProducts,
|
||||||
|
bestSellingProducts,
|
||||||
|
newestProducts,
|
||||||
categories,
|
categories,
|
||||||
brands,
|
brands,
|
||||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
|
const { featured, bestSelling } = useMemo(() => {
|
||||||
|
// Create a copy of products that we can mutate
|
||||||
|
const products = [...newestProducts]
|
||||||
|
// If the lists of featured and best selling products don't have enough
|
||||||
|
// products, then fill them with products from the products list, this
|
||||||
|
// is useful for new commerce sites that don't have a lot of products
|
||||||
|
return {
|
||||||
|
featured: rangeMap(
|
||||||
|
6,
|
||||||
|
(i) => featuredProducts[i] ?? products.shift()
|
||||||
|
).filter(nonNullable),
|
||||||
|
bestSelling: rangeMap(
|
||||||
|
6,
|
||||||
|
(i) => bestSellingProducts[i] ?? products.shift()
|
||||||
|
).filter(nonNullable),
|
||||||
|
}
|
||||||
|
// Props from getStaticProps won't change
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<Grid>
|
<Grid>
|
||||||
{featuredProducts.slice(0, 3).map((p: any) => (
|
{featured.slice(0, 3).map(({ node }) => (
|
||||||
<ProductCard key={p.id} {...p} />
|
<ProductCard key={node.path} product={node} />
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Marquee variant="secondary">
|
<Marquee variant="secondary">
|
||||||
{products.slice(0, 3).map((p: any) => (
|
{bestSelling.slice(0, 3).map(({ node }) => (
|
||||||
<ProductCard key={p.id} {...p} variant="slim" />
|
<ProductCard key={node.path} product={node} variant="slim" />
|
||||||
))}
|
))}
|
||||||
</Marquee>
|
</Marquee>
|
||||||
<Hero
|
<Hero
|
||||||
@ -48,13 +85,13 @@ export default function Home({
|
|||||||
‘Natural’."
|
‘Natural’."
|
||||||
/>
|
/>
|
||||||
<Grid layout="B">
|
<Grid layout="B">
|
||||||
{featuredProducts.slice(3, 6).map((p: any) => (
|
{featured.slice(3, 6).map(({ node }) => (
|
||||||
<ProductCard key={p.id} {...p} />
|
<ProductCard key={node.path} product={node} />
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Marquee>
|
<Marquee>
|
||||||
{products.slice(3, 6).map((p: any) => (
|
{bestSelling.slice(3, 6).map(({ node }) => (
|
||||||
<ProductCard key={p.id} {...p} variant="slim" />
|
<ProductCard key={node.path} product={node} variant="slim" />
|
||||||
))}
|
))}
|
||||||
</Marquee>
|
</Marquee>
|
||||||
<div className="py-12 flex flex-row w-full px-12">
|
<div className="py-12 flex flex-row w-full px-12">
|
||||||
@ -84,8 +121,8 @@ export default function Home({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Grid layout="normal">
|
<Grid layout="normal">
|
||||||
{products.map((p: any) => (
|
{newestProducts.map(({ node }) => (
|
||||||
<ProductCard key={p.id} {...p} variant="simple" />
|
<ProductCard key={node.path} product={node} variant="simple" />
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { range } from 'lodash'
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
|
||||||
import { Layout } from '@components/core'
|
import { Layout } from '@components/core'
|
||||||
import { ProductCard } from '@components/product'
|
import { ProductCard } from '@components/product'
|
||||||
import { Container, Grid, Skeleton } from '@components/ui'
|
import { Container, Grid, Skeleton } from '@components/ui'
|
||||||
import getSlug from '@utils/get-slug'
|
|
||||||
import useSearch from '@lib/bigcommerce/products/use-search'
|
import useSearch from '@lib/bigcommerce/products/use-search'
|
||||||
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
||||||
import getSiteInfo from '@lib/bigcommerce/api/operations/get-site-info'
|
import getSiteInfo from '@lib/bigcommerce/api/operations/get-site-info'
|
||||||
|
import rangeMap from '@lib/range-map'
|
||||||
|
import getSlug from '@utils/get-slug'
|
||||||
import {
|
import {
|
||||||
filterQuery,
|
filterQuery,
|
||||||
getCategoryPath,
|
getCategoryPath,
|
||||||
@ -141,17 +141,19 @@ export default function Search({
|
|||||||
|
|
||||||
{data ? (
|
{data ? (
|
||||||
<Grid layout="normal">
|
<Grid layout="normal">
|
||||||
{data.products.map((p: any) => (
|
{data.products.map(({ node }) => (
|
||||||
<ProductCard
|
<ProductCard
|
||||||
|
key={node.path}
|
||||||
className="animate__animated animate__fadeIn"
|
className="animate__animated animate__fadeIn"
|
||||||
{...p}
|
product={node}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
) : (
|
) : (
|
||||||
<Grid layout="normal">
|
<Grid layout="normal">
|
||||||
{range(12).map(() => (
|
{rangeMap(12, (i) => (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
|
key={i}
|
||||||
className="w-full animate__animated animate__fadeIn"
|
className="w-full animate__animated animate__fadeIn"
|
||||||
height={325}
|
height={325}
|
||||||
/>
|
/>
|
||||||
|
BIN
public/cursor-left.png
Normal file
BIN
public/cursor-left.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
public/cursor-right.png
Normal file
BIN
public/cursor-right.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -3,11 +3,7 @@ module.exports = {
|
|||||||
removeDeprecatedGapUtilities: true,
|
removeDeprecatedGapUtilities: true,
|
||||||
purgeLayersByDefault: true,
|
purgeLayersByDefault: true,
|
||||||
},
|
},
|
||||||
purge: [
|
purge: ['./components/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}'],
|
||||||
'./components/**/*.{js,ts,jsx,tsx}',
|
|
||||||
'./pages/**/*.{js,ts,jsx,tsx}',
|
|
||||||
'./ui/**/*.{js,ts,jsx,tsx}',
|
|
||||||
],
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
backgroundOpacity: {
|
backgroundOpacity: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user