forked from crowetic/commerce
Merge branch 'master' into arzafran/tweak-banner
This commit is contained in:
commit
0a270e7d23
@ -22,16 +22,16 @@
|
|||||||
--violet: #7928ca;
|
--violet: #7928ca;
|
||||||
--blue: #0070f3;
|
--blue: #0070f3;
|
||||||
|
|
||||||
--accents-0: #fff;
|
--accents-0: #f8f9fa;
|
||||||
--accents-1: #fafafa;
|
--accents-1: #f1f3f5;
|
||||||
--accents-2: #eaeaea;
|
--accents-2: #e9ecef;
|
||||||
--accents-3: #999999;
|
--accents-3: #dee2e6;
|
||||||
--accents-4: #888888;
|
--accents-4: #ced4da;
|
||||||
--accents-5: #666666;
|
--accents-5: #adb5bd;
|
||||||
--accents-6: #444444;
|
--accents-6: #868e96;
|
||||||
--accents-7: #333333;
|
--accents-7: #495057;
|
||||||
--accents-8: #111111;
|
--accents-8: #343a40;
|
||||||
--accents-9: #000;
|
--accents-9: #212529;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='dark'] {
|
[data-theme='dark'] {
|
||||||
@ -46,16 +46,16 @@
|
|||||||
--text-primary: white;
|
--text-primary: white;
|
||||||
--text-secondary: black;
|
--text-secondary: black;
|
||||||
|
|
||||||
--accents-0: #000;
|
--accents-0: #212529;
|
||||||
--accents-1: #111111;
|
--accents-1: #343a40;
|
||||||
--accents-2: #333333;
|
--accents-2: #495057;
|
||||||
--accents-3: #444444;
|
--accents-3: #868e96;
|
||||||
--accents-4: #666666;
|
--accents-4: #adb5bd;
|
||||||
--accents-5: #888888;
|
--accents-5: #ced4da;
|
||||||
--accents-6: #999999;
|
--accents-6: #dee2e6;
|
||||||
--accents-7: #eaeaea;
|
--accents-7: #e9ecef;
|
||||||
--accents-8: #fafafa;
|
--accents-8: #f1f3f5;
|
||||||
--accents-9: #fff;
|
--accents-9: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fit {
|
.fit {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.input {
|
.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;
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ const Searchbar: FC<Props> = ({ className }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -2,12 +2,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
@apply flex flex-row items-center;
|
@apply flex flex-row items-center h-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.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 {
|
&:hover {
|
||||||
@apply text-accents-8;
|
@apply text-accents-8;
|
||||||
}
|
}
|
||||||
|
@ -10,15 +10,15 @@ const ArrowLeft = ({ ...props }) => {
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M19 12H5"
|
d="M19 12H5"
|
||||||
stroke-width="1.5"
|
strokeWidth="1.5"
|
||||||
stroke-linecap="round"
|
strokeLinecap="round"
|
||||||
stroke-linejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M12 19L5 12L12 5"
|
d="M12 19L5 12L12 5"
|
||||||
stroke-width="1.5"
|
strokeWidth="1.5"
|
||||||
stroke-linecap="round"
|
strokeLinecap="round"
|
||||||
stroke-linejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
@ -10,9 +10,9 @@ const Check = ({ ...props }) => {
|
|||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M20 6L9 17L4 12"
|
d="M20 6L9 17L4 12"
|
||||||
stroke-width="2"
|
strokeWidth="2"
|
||||||
stroke-linecap="round"
|
strokeLinecap="round"
|
||||||
stroke-linejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
@ -4,9 +4,9 @@ const Minus = ({ ...props }) => {
|
|||||||
<path
|
<path
|
||||||
d="M5 12H19"
|
d="M5 12H19"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="1.5"
|
strokeWidth="1.5"
|
||||||
stroke-linecap="round"
|
strokeLinecap="round"
|
||||||
stroke-linejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
@ -4,16 +4,16 @@ const Plus = ({ ...props }) => {
|
|||||||
<path
|
<path
|
||||||
d="M12 5V19"
|
d="M12 5V19"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="1.5"
|
strokeWidth="1.5"
|
||||||
stroke-linecap="round"
|
strokeLinecap="round"
|
||||||
stroke-linejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M5 12H19"
|
d="M5 12H19"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="1.5"
|
strokeWidth="1.5"
|
||||||
stroke-linecap="round"
|
strokeLinecap="round"
|
||||||
stroke-linejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.squareBg,
|
.squareBg,
|
||||||
.productTitle,
|
.productTitle > span,
|
||||||
.productPrice,
|
.productPrice,
|
||||||
.wishlistButton {
|
.wishlistButton {
|
||||||
@apply transition ease-in-out duration-300;
|
@apply transition ease-in-out duration-300;
|
||||||
@ -65,9 +65,13 @@
|
|||||||
@apply transform absolute inset-0 z-0 bg-secondary;
|
@apply transform absolute inset-0 z-0 bg-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.squareBg.gray {
|
||||||
|
@apply bg-gray-300 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.productTitle {
|
.productTitle {
|
||||||
line-height: 51px;
|
line-height: 40px;
|
||||||
width: 200px;
|
width: 18vw;
|
||||||
|
|
||||||
& span {
|
& span {
|
||||||
@apply inline text-2xl leading-6 p-4 bg-primary text-primary font-bold;
|
@apply inline text-2xl leading-6 p-4 bg-primary text-primary font-bold;
|
||||||
|
@ -8,7 +8,7 @@ interface Props {
|
|||||||
className?: string
|
className?: string
|
||||||
children?: ReactNode[] | Component[] | any[]
|
children?: ReactNode[] | Component[] | any[]
|
||||||
node: ProductData
|
node: ProductData
|
||||||
variant?: 'slim'
|
variant?: 'slim' | 'simple'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductData {
|
interface ProductData {
|
||||||
@ -23,7 +23,7 @@ const ProductCard: FC<Props> = ({ className, node: p, variant }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="relative overflow-hidden box-border">
|
<div className="relative overflow-hidden box-border">
|
||||||
<img
|
<img
|
||||||
className="object-scale-down h-24"
|
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">
|
||||||
@ -44,12 +44,12 @@ const ProductCard: FC<Props> = ({ className, node: p, variant }) => {
|
|||||||
src={p.images.edges[0].node.urlXL}
|
src={p.images.edges[0].node.urlXL}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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="flex flex-row justify-between box-border w-full z-10 relative">
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className={s.productTitle}>
|
<p className={s.productTitle}>
|
||||||
<span>{p.name}</span>
|
<span>{p.name}</span>
|
||||||
</div>
|
</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}>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import s from './ProductView.module.css'
|
import s from './ProductView.module.css'
|
||||||
import { Colors } from '@components/ui/types'
|
|
||||||
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'
|
||||||
@ -15,19 +14,12 @@ interface Props {
|
|||||||
product: Product
|
product: Product
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Choices {
|
|
||||||
size?: string | null
|
|
||||||
color?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProductView: FC<Props> = ({ product, className }) => {
|
const ProductView: FC<Props> = ({ product, className }) => {
|
||||||
const options = getProductOptions(product)
|
|
||||||
// console.log(options)
|
|
||||||
|
|
||||||
const addItem = useAddItem()
|
const addItem = useAddItem()
|
||||||
const { openSidebar } = useUI()
|
const { openSidebar } = useUI()
|
||||||
|
const options = getProductOptions(product)
|
||||||
|
|
||||||
const [choices, setChoices] = useState<Choices>({
|
const [choices, setChoices] = useState<Record<string, any>>({
|
||||||
size: null,
|
size: null,
|
||||||
color: null,
|
color: null,
|
||||||
})
|
})
|
||||||
@ -48,9 +40,6 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeSize = choices.size
|
|
||||||
const activeColor = choices.color
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<NextSeo
|
<NextSeo
|
||||||
@ -88,6 +77,7 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
|||||||
{/** 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}
|
||||||
className="w-full object-cover"
|
className="w-full object-cover"
|
||||||
src={image?.node.urlXL}
|
src={image?.node.urlXL}
|
||||||
loading={i === 0 ? 'eager' : 'lazy'}
|
loading={i === 0 ? 'eager' : 'lazy'}
|
||||||
@ -104,25 +94,28 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
|||||||
<div className="flex-1 flex flex-col pt-24">
|
<div className="flex-1 flex flex-col pt-24">
|
||||||
<section>
|
<section>
|
||||||
{options?.map((opt: any) => (
|
{options?.map((opt: any) => (
|
||||||
<div className="pb-4">
|
<div className="pb-4" key={opt.displayName}>
|
||||||
<h2 className="uppercase font-medium">{opt.displayName}</h2>
|
<h2 className="uppercase font-medium">{opt.displayName}</h2>
|
||||||
<div className="flex flex-row py-4">
|
<div className="flex flex-row py-4">
|
||||||
{opt.values.map((v: any) => {
|
{opt.values.map((v: any) => {
|
||||||
|
const active = choices[opt.displayName]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Swatch
|
<Swatch
|
||||||
key={v.entityId}
|
key={v.entityId}
|
||||||
active={v.label === activeColor}
|
active={v.label === active}
|
||||||
variant={opt.displayName}
|
variant={opt.displayName}
|
||||||
color={v.hexColors ? v.hexColors[0] : ''}
|
color={v.hexColors ? v.hexColors[0] : ''}
|
||||||
label={v.label}
|
label={v.label}
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
setChoices((choices) => {
|
setChoices((choices) => {
|
||||||
|
console.log(choices)
|
||||||
return {
|
return {
|
||||||
...choices,
|
...choices,
|
||||||
[opt.displayName]: v.label,
|
[opt.displayName]: v.label,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
.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-75 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;
|
p-0 shadow-none border-gray-200 border box-border text-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.active.size {
|
.active.size {
|
||||||
@apply border-accents-2 border-2;
|
@apply border-accents-9 border-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root:hover {
|
.root:hover {
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import s from './Swatch.module.css'
|
import s from './Swatch.module.css'
|
||||||
import { Colors } from '@components/ui/types'
|
|
||||||
import { Check } from '@components/icon'
|
import { Check } from '@components/icon'
|
||||||
import Button, { ButtonProps } from '@components/ui/Button'
|
import Button, { ButtonProps } from '@components/ui/Button'
|
||||||
|
import { isDark } from '@lib/colors'
|
||||||
interface Props {
|
interface Props {
|
||||||
active?: boolean
|
active?: boolean
|
||||||
children?: any
|
children?: any
|
||||||
@ -24,7 +23,8 @@ const Swatch: FC<Props & ButtonProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
variant = variant?.toLowerCase()
|
variant = variant?.toLowerCase()
|
||||||
label = label?.toLowerCase()
|
label = label?.toLowerCase()
|
||||||
// console.log(variant)
|
const isDarkBg = isDark(color)
|
||||||
|
|
||||||
const rootClassName = cn(
|
const rootClassName = cn(
|
||||||
s.root,
|
s.root,
|
||||||
{
|
{
|
||||||
@ -38,12 +38,12 @@ const Swatch: FC<Props & ButtonProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
className={rootClassName}
|
className={rootClassName}
|
||||||
style={color ? { backgroundColor: color } : {}}
|
style={color ? { backgroundColor: color } : {}}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
{variant === 'color' && active && (
|
{variant === 'color' && active && (
|
||||||
<span
|
<span
|
||||||
className={cn('absolute', {
|
className={cn('absolute', {
|
||||||
'text-white': label !== 'white',
|
'text-white': isDarkBg,
|
||||||
'text-black': label === 'white',
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Check />
|
<Check />
|
||||||
|
@ -20,3 +20,7 @@
|
|||||||
.loading {
|
.loading {
|
||||||
@apply bg-accents-1 text-accents-3 border-accents-2 cursor-not-allowed;
|
@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> {
|
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
href?: string
|
href?: string
|
||||||
className?: string
|
className?: string
|
||||||
variant?: 'filled' | 'outlined' | 'flat' | 'none'
|
variant?: 'flat' | 'slim'
|
||||||
active?: boolean
|
active?: boolean
|
||||||
type?: 'submit' | 'reset' | 'button'
|
type?: 'submit' | 'reset' | 'button'
|
||||||
Component?: string | JSXElementConstructor<any>
|
Component?: string | JSXElementConstructor<any>
|
||||||
@ -24,7 +24,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
variant = 'filled',
|
variant = 'flat',
|
||||||
children,
|
children,
|
||||||
active,
|
active,
|
||||||
onClick,
|
onClick,
|
||||||
@ -50,6 +50,7 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
|||||||
const rootClassName = cn(
|
const rootClassName = cn(
|
||||||
s.root,
|
s.root,
|
||||||
{
|
{
|
||||||
|
[s.slim]: variant === 'slim',
|
||||||
[s.loading]: loading,
|
[s.loading]: loading,
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
@ -57,16 +58,16 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
className={rootClassName}
|
|
||||||
aria-pressed={active}
|
aria-pressed={active}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
ref={mergeRefs([ref, buttonRef])}
|
ref={mergeRefs([ref, buttonRef])}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
|
data-active={isPressed ? '' : undefined}
|
||||||
|
className={rootClassName}
|
||||||
style={{
|
style={{
|
||||||
width,
|
width,
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
data-active={isPressed ? '' : undefined}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{loading && (
|
{loading && (
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
const Logo = () => (
|
const Logo = ({ className = '', ...props }) => (
|
||||||
<svg
|
<svg
|
||||||
width="32"
|
width="32"
|
||||||
height="32"
|
height="32"
|
||||||
viewBox="0 0 32 32"
|
viewBox="0 0 32 32"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
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
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
@apply flex-1 px-16 py-4;
|
@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 Container } from './Container'
|
||||||
export { default as LoadingDots } from './LoadingDots'
|
export { default as LoadingDots } from './LoadingDots'
|
||||||
export { default as Skeleton } from './Skeleton'
|
export { default as Skeleton } from './Skeleton'
|
||||||
|
export { default as Modal } from './Modal'
|
||||||
|
@ -22,7 +22,7 @@ export type ProductsHandlers = {
|
|||||||
const METHODS = ['GET']
|
const METHODS = ['GET']
|
||||||
|
|
||||||
// TODO: a complete implementation should have schema validation for `req.body`
|
// TODO: a complete implementation should have schema validation for `req.body`
|
||||||
const cartApi: BigcommerceApiHandler<
|
const productApi: BigcommerceApiHandler<
|
||||||
SearchProductsData,
|
SearchProductsData,
|
||||||
ProductsHandlers
|
ProductsHandlers
|
||||||
> = async (req, res, config, handlers) => {
|
> = async (req, res, config, handlers) => {
|
||||||
@ -45,4 +45,4 @@ const cartApi: BigcommerceApiHandler<
|
|||||||
|
|
||||||
export const handlers = { getProducts }
|
export const handlers = { getProducts }
|
||||||
|
|
||||||
export default createApiHandler(cartApi, handlers, {})
|
export default createApiHandler(productApi, handlers, {})
|
||||||
|
@ -9,12 +9,15 @@ export const responsiveImageFragment = /* GraphQL */ `
|
|||||||
isDefault
|
isDefault
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
export const multipleChoiceFragment = /* GraphQL */ `
|
|
||||||
|
export const swatchOptionFragment = /* GraphQL */ `
|
||||||
fragment swatchOption on SwatchOptionValue {
|
fragment swatchOption on SwatchOptionValue {
|
||||||
isDefault
|
isDefault
|
||||||
hexColors
|
hexColors
|
||||||
}
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const multipleChoiceOptionFragment = /* GraphQL */ `
|
||||||
fragment multipleChoiceOption on MultipleChoiceOption {
|
fragment multipleChoiceOption on MultipleChoiceOption {
|
||||||
entityId
|
entityId
|
||||||
values {
|
values {
|
||||||
@ -26,6 +29,8 @@ export const multipleChoiceFragment = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${swatchOptionFragment}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const productInfoFragment = /* GraphQL */ `
|
export const productInfoFragment = /* GraphQL */ `
|
||||||
@ -76,5 +81,22 @@ export const productInfoFragment = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
|
|
||||||
${responsiveImageFragment}
|
${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'
|
} 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 { productInfoFragment } from '../fragments/product'
|
import { productConnectionFragment } from '../fragments/product'
|
||||||
import { BigcommerceConfig, getConfig, Images, ProductImageVariables } from '..'
|
import { BigcommerceConfig, getConfig, Images, ProductImageVariables } from '..'
|
||||||
|
|
||||||
export const getAllProductsQuery = /* GraphQL */ `
|
export const getAllProductsQuery = /* GraphQL */ `
|
||||||
@ -19,24 +19,28 @@ export const getAllProductsQuery = /* GraphQL */ `
|
|||||||
$imgLargeHeight: Int
|
$imgLargeHeight: Int
|
||||||
$imgXLWidth: Int = 1280
|
$imgXLWidth: Int = 1280
|
||||||
$imgXLHeight: Int
|
$imgXLHeight: Int
|
||||||
|
$products: Boolean = false
|
||||||
|
$featuredProducts: Boolean = false
|
||||||
|
$bestSellingProducts: Boolean = false
|
||||||
|
$newestProducts: Boolean = false
|
||||||
) {
|
) {
|
||||||
site {
|
site {
|
||||||
products(first: $first, entityIds: $entityIds) {
|
products(first: $first, entityIds: $entityIds) @include(if: $products) {
|
||||||
pageInfo {
|
...productConnnection
|
||||||
startCursor
|
}
|
||||||
endCursor
|
featuredProducts(first: $first) @include(if: $featuredProducts) {
|
||||||
}
|
...productConnnection
|
||||||
edges {
|
}
|
||||||
cursor
|
bestSellingProducts(first: $first) @include(if: $bestSellingProducts) {
|
||||||
node {
|
...productConnnection
|
||||||
...productInfo
|
}
|
||||||
}
|
newestProducts(first: $first) @include(if: $newestProducts) {
|
||||||
}
|
...productConnnection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
${productInfoFragment}
|
${productConnectionFragment}
|
||||||
`
|
`
|
||||||
|
|
||||||
export type Product = NonNullable<
|
export type Product = NonNullable<
|
||||||
@ -46,18 +50,34 @@ export type Product = NonNullable<
|
|||||||
export type Products = Product[]
|
export type Products = Product[]
|
||||||
|
|
||||||
export type GetAllProductsResult<
|
export type GetAllProductsResult<
|
||||||
T extends { products: any[] } = { products: Products }
|
T extends Record<keyof GetAllProductsResult, any[]> = { products: Products }
|
||||||
> = T
|
> = T
|
||||||
|
|
||||||
export type ProductVariables = Images &
|
const FIELDS = [
|
||||||
Omit<GetAllProductsQueryVariables, keyof ProductImageVariables>
|
'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?: {
|
async function getAllProducts(opts?: {
|
||||||
variables?: ProductVariables
|
variables?: ProductVariables
|
||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
}): Promise<GetAllProductsResult>
|
}): 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
|
query: string
|
||||||
variables?: V
|
variables?: V
|
||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
@ -65,7 +85,7 @@ async function getAllProducts<T extends { products: any[] }, V = any>(opts: {
|
|||||||
|
|
||||||
async function getAllProducts({
|
async function getAllProducts({
|
||||||
query = getAllProductsQuery,
|
query = getAllProductsQuery,
|
||||||
variables: vars,
|
variables: { field = 'products', ...vars } = {},
|
||||||
config,
|
config,
|
||||||
}: {
|
}: {
|
||||||
query?: string
|
query?: string
|
||||||
@ -73,17 +93,27 @@ async function getAllProducts({
|
|||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
} = {}): Promise<GetAllProductsResult> {
|
} = {}): Promise<GetAllProductsResult> {
|
||||||
config = getConfig(config)
|
config = getConfig(config)
|
||||||
|
|
||||||
const variables: GetAllProductsQueryVariables = {
|
const variables: GetAllProductsQueryVariables = {
|
||||||
...config.imageVariables,
|
...config.imageVariables,
|
||||||
...vars,
|
...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
|
// RecursivePartial forces the method to check for every prop in the data, which is
|
||||||
// required in case there's a custom `query`
|
// required in case there's a custom `query`
|
||||||
const data = await config.fetch<RecursivePartial<GetAllProductsQuery>>(
|
const data = await config.fetch<RecursivePartial<GetAllProductsQuery>>(
|
||||||
query,
|
query,
|
||||||
{ variables }
|
{ variables }
|
||||||
)
|
)
|
||||||
const products = data.site?.products?.edges
|
const products = data.site?.[field]?.edges
|
||||||
|
|
||||||
return {
|
return {
|
||||||
products: filterEdges(products as RecursiveRequired<typeof products>),
|
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 GetAllProductPathsQueryVariables = Exact<{ [key: string]: never }>
|
||||||
|
|
||||||
export type GetAllProductPathsQuery = { __typename?: 'Query' } & {
|
export type GetAllProductPathsQuery = { __typename?: 'Query' } & {
|
||||||
@ -1814,25 +1832,24 @@ export type GetAllProductsQueryVariables = Exact<{
|
|||||||
imgLargeHeight?: Maybe<Scalars['Int']>
|
imgLargeHeight?: Maybe<Scalars['Int']>
|
||||||
imgXLWidth?: Maybe<Scalars['Int']>
|
imgXLWidth?: Maybe<Scalars['Int']>
|
||||||
imgXLHeight?: 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' } & {
|
export type GetAllProductsQuery = { __typename?: 'Query' } & {
|
||||||
site: { __typename?: 'Site' } & {
|
site: { __typename?: 'Site' } & {
|
||||||
products: { __typename?: 'ProductConnection' } & {
|
products: { __typename?: 'ProductConnection' } & ProductConnnectionFragment
|
||||||
pageInfo: { __typename?: 'PageInfo' } & Pick<
|
featuredProducts: {
|
||||||
PageInfo,
|
__typename?: 'ProductConnection'
|
||||||
'startCursor' | 'endCursor'
|
} & ProductConnnectionFragment
|
||||||
>
|
bestSellingProducts: {
|
||||||
edges?: Maybe<
|
__typename?: 'ProductConnection'
|
||||||
Array<
|
} & ProductConnnectionFragment
|
||||||
Maybe<
|
newestProducts: {
|
||||||
{ __typename?: 'ProductEdge' } & Pick<ProductEdge, 'cursor'> & {
|
__typename?: 'ProductConnection'
|
||||||
node: { __typename?: 'Product' } & ProductInfoFragment
|
} & 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
|
// Returns a pair of colors
|
||||||
return [colors[idx], colors[idx2]]
|
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>
|
swrOptions?: ConfigInterface<T | null>
|
||||||
) {
|
) {
|
||||||
const { cartCookie } = useCommerce()
|
const { cartCookie } = useCommerce()
|
||||||
|
|
||||||
const fetcher: typeof fetcherFn = (options, input, fetch) => {
|
const fetcher: typeof fetcherFn = (options, input, fetch) => {
|
||||||
input.cartId = Cookies.get(cartCookie)
|
input.cartId = Cookies.get(cartCookie)
|
||||||
return fetcherFn(options, input, fetch)
|
return fetcherFn(options, input, fetch)
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = useData(options, input, fetcher, swrOptions)
|
const response = useData(options, input, fetcher, swrOptions)
|
||||||
|
|
||||||
return Object.assign(response, { isEmpty: true }) as CartResponse<T>
|
return Object.assign(response, { isEmpty: true }) as CartResponse<T>
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { Fetcher } from './utils/types'
|
import { Fetcher } from './utils/types'
|
||||||
|
|
||||||
const Commerce = createContext<CommerceContextValue | null>(null)
|
const Commerce = createContext<CommerceContextValue | {}>({})
|
||||||
|
|
||||||
export type CommerceProps = {
|
export type CommerceProps = {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
|
@ -1,5 +1,18 @@
|
|||||||
import bunyan from 'bunyan'
|
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
|
export default log
|
||||||
|
@ -22,15 +22,17 @@
|
|||||||
"@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": "^1.8.6",
|
||||||
|
"@types/bunyan-prettystream": "^0.1.31",
|
||||||
"@types/classnames": "^2.2.10",
|
"@types/classnames": "^2.2.10",
|
||||||
"@types/react-swipeable-views": "^0.13.0",
|
"@types/react-swipeable-views": "^0.13.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"bunyan": "^1.8.14",
|
"bunyan": "^1.8.14",
|
||||||
|
"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",
|
||||||
"next": "^9.5.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",
|
||||||
|
@ -9,22 +9,26 @@ 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 { pages } = await getAllPages()
|
||||||
const { products } = await getAllProducts()
|
const { products } = await getAllProducts()
|
||||||
|
const { products: featuredProducts } = await getAllProducts({
|
||||||
|
variables: { field: 'featuredProducts', first: 3 },
|
||||||
|
})
|
||||||
const { categories, brands } = await getSiteInfo()
|
const { categories, brands } = await getSiteInfo()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { pages, products, categories, brands },
|
props: { pages, products, featuredProducts, categories, brands },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({
|
export default function Home({
|
||||||
products,
|
products,
|
||||||
|
featuredProducts,
|
||||||
categories,
|
categories,
|
||||||
brands,
|
brands,
|
||||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<Grid>
|
<Grid>
|
||||||
{products.slice(0, 3).map((p: any) => (
|
{featuredProducts.slice(0, 3).map((p: any) => (
|
||||||
<ProductCard key={p.id} {...p} />
|
<ProductCard key={p.id} {...p} />
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -44,42 +48,44 @@ export default function Home({
|
|||||||
‘Natural’."
|
‘Natural’."
|
||||||
/>
|
/>
|
||||||
<Grid layout="B">
|
<Grid layout="B">
|
||||||
{products.slice(3, 6).map((p: any) => (
|
{featuredProducts.slice(3, 6).map((p: any) => (
|
||||||
<ProductCard key={p.id} {...p} />
|
<ProductCard key={p.id} {...p} />
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Marquee>
|
<Marquee>
|
||||||
{products.slice(0, 3).map((p: any) => (
|
{products.slice(3, 6).map((p: any) => (
|
||||||
<ProductCard key={p.id} {...p} variant="slim" />
|
<ProductCard key={p.id} {...p} 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">
|
||||||
<div className="pr-3 w-48">
|
<div className="pr-3 w-48 relative">
|
||||||
<ul className="mb-10">
|
<div className="sticky top-2">
|
||||||
<li className="py-1 text-base font-bold tracking-wide">
|
<ul className="mb-10">
|
||||||
All Categories
|
<li className="py-1 text-base font-bold tracking-wide">
|
||||||
</li>
|
All Categories
|
||||||
{categories.map((cat) => (
|
|
||||||
<li key={cat.path} className="py-1 text-accents-8">
|
|
||||||
<a href="#">{cat.name}</a>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
{categories.map((cat) => (
|
||||||
</ul>
|
<li key={cat.path} className="py-1 text-accents-8">
|
||||||
<ul className="">
|
<a href="#">{cat.name}</a>
|
||||||
<li className="py-1 text-base font-bold tracking-wide">
|
</li>
|
||||||
All Designers
|
))}
|
||||||
</li>
|
</ul>
|
||||||
{brands.flatMap(({ node }) => (
|
<ul className="">
|
||||||
<li key={node.path} className="py-1 text-accents-8">
|
<li className="py-1 text-base font-bold tracking-wide">
|
||||||
<a href="#">{node.name}</a>
|
All Designers
|
||||||
</li>
|
</li>
|
||||||
))}
|
{brands.flatMap(({ node }) => (
|
||||||
</ul>
|
<li key={node.path} className="py-1 text-accents-8">
|
||||||
|
<a href="#">{node.name}</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Grid layout="normal">
|
<Grid layout="normal">
|
||||||
{products.map((p: any) => (
|
{products.map((p: any) => (
|
||||||
<ProductCard key={p.id} {...p} />
|
<ProductCard key={p.id} {...p} variant="simple" />
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</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>
|
<Grid layout="normal">
|
||||||
{range(12).map(() => (
|
{range(12).map(() => (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
className="w-full animate__animated animate__fadeIn"
|
className="w-full animate__animated animate__fadeIn"
|
||||||
|
81
yarn.lock
81
yarn.lock
@ -1397,6 +1397,26 @@
|
|||||||
is-promise "4.0.0"
|
is-promise "4.0.0"
|
||||||
tslib "~2.0.1"
|
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":
|
"@headlessui/react@^0.2.0":
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-0.2.0.tgz#a31f90892d736243ba91c1474f534b3256d0c538"
|
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-0.2.0.tgz#a31f90892d736243ba91c1474f534b3256d0c538"
|
||||||
@ -1412,20 +1432,20 @@
|
|||||||
meow "^7.0.0"
|
meow "^7.0.0"
|
||||||
prettier "^2.0.5"
|
prettier "^2.0.5"
|
||||||
|
|
||||||
"@next/env@9.5.4":
|
"@next/env@9.5.6-canary.4":
|
||||||
version "9.5.4"
|
version "9.5.6-canary.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-9.5.4.tgz#950f3370151a940ecac6e7e19cf125e6113e101e"
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-9.5.6-canary.4.tgz#785956d1e2d26e25377a2dcafbd3fe785cd88c35"
|
||||||
integrity sha512-uGnUO68/u9C8bqHj5obIvyGRDqe/jh1dFSLx03mJmlESjcCmV4umXYJOnt3XzU1VhVntSE+jUZtnS5bjYmmLfQ==
|
integrity sha512-c+JrotEtdgcaF6m4xSZ6D7eY4NWSDBsqcaLwkKAXh+pSEHfH2eKnZv7cRIaR3s7Ll37gE/RNfXGVC2l43vCcuw==
|
||||||
|
|
||||||
"@next/polyfill-module@9.5.4":
|
"@next/polyfill-module@9.5.6-canary.4":
|
||||||
version "9.5.4"
|
version "9.5.6-canary.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-9.5.4.tgz#35ea31ce5f6bbf0ac31aac483b60d4ba17a79861"
|
resolved "https://registry.yarnpkg.com/@next/polyfill-module/-/polyfill-module-9.5.6-canary.4.tgz#6144ee81ae42aa44905f6c2b864ff5be19c67c38"
|
||||||
integrity sha512-GA2sW7gs33s7RGPFqkMiT9asYpaV/Hhw9+XM9/UlPrkNdTaxZWaPa2iHgmqJ7k6OHiOmy+CBLFrUBgzqKNhs3Q==
|
integrity sha512-r+kAXpF8AjVZGanJBwveCR5GCqnNIjA0WBsotat4MP+KKQwrptocRYsP+eZZo7gyhHEwSMdMag2NWahqjW+Vlg==
|
||||||
|
|
||||||
"@next/react-dev-overlay@9.5.4":
|
"@next/react-dev-overlay@9.5.6-canary.4":
|
||||||
version "9.5.4"
|
version "9.5.6-canary.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-9.5.4.tgz#7d88a710d23021020cca213bc77106df18950b2b"
|
resolved "https://registry.yarnpkg.com/@next/react-dev-overlay/-/react-dev-overlay-9.5.6-canary.4.tgz#2218f9a9d5874e838d24178f657ec6dd8e1aa961"
|
||||||
integrity sha512-tYvNmOQ0inykSvcimkTiONMv4ZyFB2G2clsy9FKLLRZ2OA+Jiov6T7Pq6YpKbBwTLu/BQGVc7Qn4BZ5CDHR8ig==
|
integrity sha512-zYKkkdWi2rSyWYnOn7BEAQeZwOP2O6wHC4A/nB2YdRBDvD9p3qLawmribuiQ7vspPmAIgv2JgQ8DauC5YQU7DA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "7.10.4"
|
"@babel/code-frame" "7.10.4"
|
||||||
ally.js "1.4.1"
|
ally.js "1.4.1"
|
||||||
@ -1438,10 +1458,10 @@
|
|||||||
stacktrace-parser "0.1.10"
|
stacktrace-parser "0.1.10"
|
||||||
strip-ansi "6.0.0"
|
strip-ansi "6.0.0"
|
||||||
|
|
||||||
"@next/react-refresh-utils@9.5.4":
|
"@next/react-refresh-utils@9.5.6-canary.4":
|
||||||
version "9.5.4"
|
version "9.5.6-canary.4"
|
||||||
resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-9.5.4.tgz#3bfe067f0cfc717f079482d956211708c9e81126"
|
resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-9.5.6-canary.4.tgz#098a55714d906248ef46e9a1c077a1dc6e231331"
|
||||||
integrity sha512-TPhEiYxK5YlEuzVuTzgZiDN7SDh4drvUAqsO9Yccd8WLcfYqOLRN2fCALremW5mNLAZQZW3iFgW8PW8Gckq4EQ==
|
integrity sha512-hxSF3/lWX/a+6vPLViklLK6DNDzRzd8T/EUJpkUgLdwdHnABKFB6p9m1qvdGuqBQug2RNDcLQy7oKucy42tpVQ==
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.3":
|
"@nodelib/fs.scandir@2.1.3":
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
@ -2095,6 +2115,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||||
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
|
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":
|
"@types/bunyan@^1.8.6":
|
||||||
version "1.8.6"
|
version "1.8.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/bunyan/-/bunyan-1.8.6.tgz#6527641cca30bedec5feb9ab527b7803b8000582"
|
resolved "https://registry.yarnpkg.com/@types/bunyan/-/bunyan-1.8.6.tgz#6527641cca30bedec5feb9ab527b7803b8000582"
|
||||||
@ -2977,6 +3004,11 @@ bufferutil@^4.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
node-gyp-build "~3.7.0"
|
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:
|
bunyan@^1.8.14:
|
||||||
version "1.8.14"
|
version "1.8.14"
|
||||||
resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.14.tgz#3d8c1afea7de158a5238c7cb8a66ab6b38dd45b4"
|
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"
|
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
|
||||||
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
|
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
|
||||||
|
|
||||||
next@^9.5.4:
|
next@^9.5.6-canary.4:
|
||||||
version "9.5.4"
|
version "9.5.6-canary.4"
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-9.5.4.tgz#3c6aa3fd38ff1711e956ea2b6833475e0262ec35"
|
resolved "https://registry.yarnpkg.com/next/-/next-9.5.6-canary.4.tgz#af3ed55845f6005ac155f7c5d9d21b7493be7141"
|
||||||
integrity sha512-dicsJSxiUFcRjeZ/rNMAO3HS5ttFFuRHhdAn5g7lHnWUZ3MnEX4ggBIihaoUr6qu2So9KoqUPXpS91MuSXUmBw==
|
integrity sha512-sd29wpQer1Ha9EWpvrBvN6XXmLtJnR5JWAyYnlBmrA835BolN0cQw6++sOGcdTJZV5+ZWbhTEzBVOUTKsSLd5w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ampproject/toolbox-optimizer" "2.6.0"
|
"@ampproject/toolbox-optimizer" "2.6.0"
|
||||||
"@babel/code-frame" "7.10.4"
|
"@babel/code-frame" "7.10.4"
|
||||||
@ -5910,10 +5942,11 @@ next@^9.5.4:
|
|||||||
"@babel/preset-typescript" "7.10.4"
|
"@babel/preset-typescript" "7.10.4"
|
||||||
"@babel/runtime" "7.11.2"
|
"@babel/runtime" "7.11.2"
|
||||||
"@babel/types" "7.11.5"
|
"@babel/types" "7.11.5"
|
||||||
"@next/env" "9.5.4"
|
"@hapi/accept" "5.0.1"
|
||||||
"@next/polyfill-module" "9.5.4"
|
"@next/env" "9.5.6-canary.4"
|
||||||
"@next/react-dev-overlay" "9.5.4"
|
"@next/polyfill-module" "9.5.6-canary.4"
|
||||||
"@next/react-refresh-utils" "9.5.4"
|
"@next/react-dev-overlay" "9.5.6-canary.4"
|
||||||
|
"@next/react-refresh-utils" "9.5.6-canary.4"
|
||||||
ast-types "0.13.2"
|
ast-types "0.13.2"
|
||||||
babel-plugin-transform-define "2.0.0"
|
babel-plugin-transform-define "2.0.0"
|
||||||
babel-plugin-transform-react-remove-prop-types "0.4.24"
|
babel-plugin-transform-react-remove-prop-types "0.4.24"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user