forked from crowetic/commerce
Merge branch 'master' into arzafran/ui-tweaks
This commit is contained in:
commit
dc6919e1da
@ -98,3 +98,37 @@ body {
|
|||||||
a {
|
a {
|
||||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.animated {
|
||||||
|
-webkit-animation-duration: 1s;
|
||||||
|
animation-duration: 1s;
|
||||||
|
-webkit-animation-duration: 1s;
|
||||||
|
animation-duration: 1s;
|
||||||
|
-webkit-animation-fill-mode: both;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeIn {
|
||||||
|
-webkit-animation-name: fadeIn;
|
||||||
|
animation-name: fadeIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
89
components/auth/ForgotPassword.tsx
Normal file
89
components/auth/ForgotPassword.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { FC, useEffect, useState, useCallback } from 'react'
|
||||||
|
import { validate } from 'email-validator'
|
||||||
|
import { Info } from'@components/icons'
|
||||||
|
import { useUI } from '@components/ui/context'
|
||||||
|
import { Logo, Button, Input } from '@components/ui'
|
||||||
|
import useSignup from '@lib/bigcommerce/use-signup'
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
const ForgotPassword: FC<Props> = () => {
|
||||||
|
// Form State
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
const [dirty, setDirty] = useState(false)
|
||||||
|
const [disabled, setDisabled] = useState(false)
|
||||||
|
|
||||||
|
const signup = useSignup()
|
||||||
|
const { setModalView, closeModal } = useUI()
|
||||||
|
|
||||||
|
const handleSignup = async () => {
|
||||||
|
if (!dirty && !disabled) {
|
||||||
|
setDirty(true)
|
||||||
|
handleValidation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// setLoading(true)
|
||||||
|
// setMessage('')
|
||||||
|
// await signup({
|
||||||
|
// email,
|
||||||
|
// })
|
||||||
|
// setLoading(false)
|
||||||
|
// closeModal()
|
||||||
|
// } catch ({ errors }) {
|
||||||
|
// setMessage(errors[0].message)
|
||||||
|
// setLoading(false)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleValidation = useCallback(() => {
|
||||||
|
// Unable to send form unless fields are valid.
|
||||||
|
if (dirty) {
|
||||||
|
setDisabled(!validate(email))
|
||||||
|
}
|
||||||
|
}, [email, dirty])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleValidation()
|
||||||
|
}, [handleValidation])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-80 flex flex-col justify-between p-3">
|
||||||
|
<div className="flex justify-center pb-12 ">
|
||||||
|
<Logo width="64px" height="64px" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
{message && (
|
||||||
|
<div className="text-red border border-red p-3">{message}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Input placeholder="Email" onChange={setEmail} />
|
||||||
|
<div className="pt-2 w-full flex flex-col">
|
||||||
|
<Button
|
||||||
|
variant="slim"
|
||||||
|
onClick={() => handleSignup()}
|
||||||
|
loading={loading}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
Recover Password
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="pt-3 text-center text-sm">
|
||||||
|
<span className="text-accents-7">Do you have an account?</span>
|
||||||
|
{` `}
|
||||||
|
<a
|
||||||
|
className="text-accent-9 font-bold hover:underline cursor-pointer"
|
||||||
|
onClick={() => setModalView('LOGIN_VIEW')}
|
||||||
|
>
|
||||||
|
Log In
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ForgotPassword
|
99
components/auth/LoginView.tsx
Normal file
99
components/auth/LoginView.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { FC, useEffect, useState, useCallback } from 'react'
|
||||||
|
import { Logo, Modal, Button, Input } from '@components/ui'
|
||||||
|
import useLogin from '@lib/bigcommerce/use-login'
|
||||||
|
import { useUI } from '@components/ui/context'
|
||||||
|
import { validate } from 'email-validator'
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
const LoginView: FC<Props> = () => {
|
||||||
|
// Form State
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
const [dirty, setDirty] = useState(false)
|
||||||
|
const [disabled, setDisabled] = useState(false)
|
||||||
|
const { setModalView, closeModal } = useUI()
|
||||||
|
|
||||||
|
const login = useLogin()
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!dirty && !disabled) {
|
||||||
|
setDirty(true)
|
||||||
|
handleValidation()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
setMessage('')
|
||||||
|
await login({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
setLoading(false)
|
||||||
|
closeModal()
|
||||||
|
} catch ({ errors }) {
|
||||||
|
setMessage(errors[0].message)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleValidation = useCallback(() => {
|
||||||
|
// Test for Alphanumeric password
|
||||||
|
const validPassword = /^(?=.*[a-zA-Z])(?=.*[0-9])/.test(password)
|
||||||
|
|
||||||
|
// Unable to send form unless fields are valid.
|
||||||
|
if (dirty) {
|
||||||
|
setDisabled(!validate(email) || password.length < 7 || !validPassword)
|
||||||
|
}
|
||||||
|
}, [email, password, dirty])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleValidation()
|
||||||
|
}, [handleValidation])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-80 flex flex-col justify-between p-3">
|
||||||
|
<div className="flex justify-center pb-12 ">
|
||||||
|
<Logo width="64px" height="64px" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-3">
|
||||||
|
{message && (
|
||||||
|
<div className="text-red border border-red p-3">
|
||||||
|
{message}. Did you {` `}
|
||||||
|
<a
|
||||||
|
className="text-accent-9 inline font-bold hover:underline cursor-pointer"
|
||||||
|
onClick={() => setModalView('FORGOT_VIEW')}
|
||||||
|
>
|
||||||
|
forgot your password?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Input placeholder="Email" onChange={setEmail} />
|
||||||
|
<Input placeholder="Password" onChange={setPassword} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="slim"
|
||||||
|
onClick={() => handleLogin()}
|
||||||
|
loading={loading}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
Log In
|
||||||
|
</Button>
|
||||||
|
<div className="pt-1 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"
|
||||||
|
onClick={() => setModalView('SIGNUP_VIEW')}
|
||||||
|
>
|
||||||
|
Sign Up
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginView
|
109
components/auth/SignUpView.tsx
Normal file
109
components/auth/SignUpView.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { FC, useEffect, useState, useCallback } from 'react'
|
||||||
|
import { validate } from 'email-validator'
|
||||||
|
import { Info } from '@components/icons'
|
||||||
|
import { useUI } from '@components/ui/context'
|
||||||
|
import { Logo, Button, Input } from '@components/ui'
|
||||||
|
import useSignup from '@lib/bigcommerce/use-signup'
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
const SignUpView: FC<Props> = () => {
|
||||||
|
// Form State
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [firstName, setFirstName] = useState('')
|
||||||
|
const [lastName, setLastName] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
const [dirty, setDirty] = useState(false)
|
||||||
|
const [disabled, setDisabled] = useState(false)
|
||||||
|
|
||||||
|
const signup = useSignup()
|
||||||
|
const { setModalView, closeModal } = useUI()
|
||||||
|
|
||||||
|
const handleSignup = async () => {
|
||||||
|
if (!dirty && !disabled) {
|
||||||
|
setDirty(true)
|
||||||
|
handleValidation()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
setMessage('')
|
||||||
|
await signup({
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
setLoading(false)
|
||||||
|
closeModal()
|
||||||
|
} catch ({ errors }) {
|
||||||
|
setMessage(errors[0].message)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleValidation = useCallback(() => {
|
||||||
|
// Test for Alphanumeric password
|
||||||
|
const validPassword = /^(?=.*[a-zA-Z])(?=.*[0-9])/.test(password)
|
||||||
|
|
||||||
|
// Unable to send form unless fields are valid.
|
||||||
|
if (dirty) {
|
||||||
|
setDisabled(!validate(email) || password.length < 7 || !validPassword)
|
||||||
|
}
|
||||||
|
}, [email, password, dirty])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleValidation()
|
||||||
|
}, [handleValidation])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-96 flex flex-col justify-between p-3">
|
||||||
|
<div className="flex justify-center pb-12 ">
|
||||||
|
<Logo width="64px" height="64px" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
{message && (
|
||||||
|
<div className="text-red border border-red p-3">{message}</div>
|
||||||
|
)}
|
||||||
|
<Input placeholder="First Name" onChange={setFirstName} />
|
||||||
|
<Input placeholder="Last Name" onChange={setLastName} />
|
||||||
|
<Input placeholder="Email" onChange={setEmail} />
|
||||||
|
<Input placeholder="Password" onChange={setPassword} />
|
||||||
|
<span className="text-accents-8">
|
||||||
|
<span className="inline-block align-middle ">
|
||||||
|
<Info width="15" height="15" />
|
||||||
|
</span>{' '}
|
||||||
|
<span className="leading-6 text-sm">
|
||||||
|
<strong>Info</strong>: Passwords must be longer than 7 chars and
|
||||||
|
include numbers.{' '}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div className="pt-2 w-full flex flex-col">
|
||||||
|
<Button
|
||||||
|
variant="slim"
|
||||||
|
onClick={() => handleSignup()}
|
||||||
|
loading={loading}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
Sign Up
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="pt-1 text-center text-sm">
|
||||||
|
<span className="text-accents-7">Do you have an account?</span>
|
||||||
|
{` `}
|
||||||
|
<a
|
||||||
|
className="text-accent-9 font-bold hover:underline cursor-pointer"
|
||||||
|
onClick={() => setModalView('LOGIN_VIEW')}
|
||||||
|
>
|
||||||
|
Log In
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignUpView
|
3
components/auth/index.ts
Normal file
3
components/auth/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as LoginView } from './LoginView'
|
||||||
|
export { default as SignUpView } from './SignUpView'
|
||||||
|
export { default as ForgotPassword } from './ForgotPassword'
|
@ -10,8 +10,9 @@
|
|||||||
|
|
||||||
.productImage {
|
.productImage {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
|
||||||
left: -10px;
|
|
||||||
top: 15px;
|
|
||||||
transform: scale(1.9);
|
transform: scale(1.9);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 30% !important;
|
||||||
|
top: 30% !important;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { ChangeEvent, useEffect, useState } from 'react'
|
import s from './CartItem.module.css'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { Trash, Plus, Minus } from '@components/icon'
|
import Link from 'next/link'
|
||||||
|
import { ChangeEvent, useEffect, useState } from 'react'
|
||||||
|
import { Trash, Plus, Minus } from '@components/icons'
|
||||||
import usePrice from '@lib/bigcommerce/use-price'
|
import usePrice from '@lib/bigcommerce/use-price'
|
||||||
import useUpdateItem from '@lib/bigcommerce/cart/use-update-item'
|
import useUpdateItem from '@lib/bigcommerce/cart/use-update-item'
|
||||||
import useRemoveItem from '@lib/bigcommerce/cart/use-remove-item'
|
import useRemoveItem from '@lib/bigcommerce/cart/use-remove-item'
|
||||||
import s from './CartItem.module.css'
|
|
||||||
|
|
||||||
const CartItem = ({
|
const CartItem = ({
|
||||||
item,
|
item,
|
||||||
@ -55,18 +56,26 @@ const CartItem = ({
|
|||||||
}, [item.quantity])
|
}, [item.quantity])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="flex flex-row space-x-8 py-6">
|
<li className="flex flex-row space-x-8 py-8">
|
||||||
<div className="w-12 h-12 bg-violet relative overflow-hidden">
|
<div className="w-16 h-16 bg-violet relative overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
|
className={s.productImage}
|
||||||
src={item.image_url}
|
src={item.image_url}
|
||||||
width={60}
|
width={150}
|
||||||
height={60}
|
height={150}
|
||||||
|
alt="Product Image"
|
||||||
// The cart item image is already optimized and very small in size
|
// The cart item image is already optimized and very small in size
|
||||||
unoptimized
|
unoptimized
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col justify-between text-base">
|
<div className="flex-1 flex flex-col text-base">
|
||||||
<span className="font-bold mb-3">{item.name}</span>
|
{/** TODO: Replace this. No `path` found at Cart */}
|
||||||
|
<Link href={`/product/${item.url.split('/')[3]}`}>
|
||||||
|
<span className="font-bold mb-5 text-lg cursor-pointer">
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button type="button" onClick={() => increaseQuantity(-1)}>
|
<button type="button" onClick={() => increaseQuantity(-1)}>
|
||||||
<Minus width={18} height={18} />
|
<Minus width={18} height={18} />
|
||||||
|
@ -2,7 +2,7 @@ import { FC } from 'react'
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { UserNav } from '@components/core'
|
import { UserNav } from '@components/core'
|
||||||
import { Button } from '@components/ui'
|
import { Button } from '@components/ui'
|
||||||
import { ArrowLeft, Bag, Cross, Check } from '@components/icon'
|
import { ArrowLeft, Bag, Cross, Check } from '@components/icons'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import useCart from '@lib/bigcommerce/cart/use-cart'
|
import useCart from '@lib/bigcommerce/cart/use-cart'
|
||||||
import usePrice from '@lib/bigcommerce/use-price'
|
import usePrice from '@lib/bigcommerce/use-price'
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export { default as CartSidebarView } from './CartSidebarView'
|
export { default as CartSidebarView } from './CartSidebarView'
|
||||||
|
export { default as CartItem } from './CartItem'
|
||||||
|
42
components/core/EnhancedImage/EnhancedImage.tsx
Normal file
42
components/core/EnhancedImage/EnhancedImage.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { useInView } from 'react-intersection-observer'
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
type Props = Omit<
|
||||||
|
JSX.IntrinsicElements['img'],
|
||||||
|
'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading'
|
||||||
|
> & {
|
||||||
|
src: string
|
||||||
|
quality?: string
|
||||||
|
priority?: boolean
|
||||||
|
loading?: readonly ['lazy', 'eager', undefined]
|
||||||
|
unoptimized?: boolean
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
width: number | string
|
||||||
|
height: number | string
|
||||||
|
unsized?: false
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
width?: number | string
|
||||||
|
height?: number | string
|
||||||
|
unsized: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const EnhancedImage: FC<Props & JSX.IntrinsicElements['img']> = ({
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [ref] = useInView({
|
||||||
|
triggerOnce: true,
|
||||||
|
rootMargin: '220px 0px',
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<Image {...props} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnhancedImage
|
1
components/core/EnhancedImage/index.ts
Normal file
1
components/core/EnhancedImage/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './EnhancedImage'
|
@ -1,11 +1,13 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import getSlug from '@utils/get-slug'
|
import { useRouter } from 'next/router'
|
||||||
import { Github } from '@components/icon'
|
|
||||||
import { Logo, Container } from '@components/ui'
|
|
||||||
import type { Page } from '@lib/bigcommerce/api/operations/get-all-pages'
|
import type { Page } from '@lib/bigcommerce/api/operations/get-all-pages'
|
||||||
|
import getSlug from '@utils/get-slug'
|
||||||
|
import { Github } from '@components/icons'
|
||||||
|
import { Logo, Container } from '@components/ui'
|
||||||
import { I18nWidget } from '@components/core'
|
import { I18nWidget } from '@components/core'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
children?: any
|
children?: any
|
||||||
@ -15,8 +17,8 @@ interface Props {
|
|||||||
const LEGAL_PAGES = ['terms-of-use', 'shipping-returns', 'privacy-policy']
|
const LEGAL_PAGES = ['terms-of-use', 'shipping-returns', 'privacy-policy']
|
||||||
|
|
||||||
const Footer: FC<Props> = ({ className, pages }) => {
|
const Footer: FC<Props> = ({ className, pages }) => {
|
||||||
|
const { sitePages, legalPages } = usePages(pages)
|
||||||
const rootClassName = cn(className)
|
const rootClassName = cn(className)
|
||||||
const { sitePages, legalPages } = getPages(pages)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className={rootClassName}>
|
<footer className={rootClassName}>
|
||||||
@ -36,21 +38,21 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
<ul className="flex flex-initial flex-col md:flex-1">
|
<ul className="flex flex-initial flex-col md:flex-1">
|
||||||
<li className="py-3 md:py-0 md:pb-4">
|
<li className="py-3 md:py-0 md:pb-4">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a className="text-gray-400 hover:text-white transition ease-in-out duration-150">
|
<a className="text-accent-3 hover:text-white transition ease-in-out duration-150">
|
||||||
Home
|
Home
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="py-3 md:py-0 md:pb-4">
|
<li className="py-3 md:py-0 md:pb-4">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a className="text-gray-400 hover:text-white transition ease-in-out duration-150">
|
<a className="text-accent-3 hover:text-white transition ease-in-out duration-150">
|
||||||
Careers
|
Careers
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="py-3 md:py-0 md:pb-4">
|
<li className="py-3 md:py-0 md:pb-4">
|
||||||
<Link href="/blog">
|
<Link href="/blog">
|
||||||
<a className="text-gray-400 hover:text-white transition ease-in-out duration-150">
|
<a className="text-accent-3 hover:text-white transition ease-in-out duration-150">
|
||||||
Blog
|
Blog
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
@ -58,7 +60,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
{sitePages.map((page) => (
|
{sitePages.map((page) => (
|
||||||
<li key={page.url} className="py-3 md:py-0 md:pb-4">
|
<li key={page.url} className="py-3 md:py-0 md:pb-4">
|
||||||
<Link href={page.url!}>
|
<Link href={page.url!}>
|
||||||
<a className="text-gray-400 hover:text-white transition ease-in-out duration-150">
|
<a className="text-accent-3 hover:text-white transition ease-in-out duration-150">
|
||||||
{page.name}
|
{page.name}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
@ -71,7 +73,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
{legalPages.map((page) => (
|
{legalPages.map((page) => (
|
||||||
<li key={page.url} className="py-3 md:py-0 md:pb-4">
|
<li key={page.url} className="py-3 md:py-0 md:pb-4">
|
||||||
<Link href={page.url!}>
|
<Link href={page.url!}>
|
||||||
<a className="text-gray-400 hover:text-white transition ease-in-out duration-150">
|
<a className="text-accent-3 hover:text-white transition ease-in-out duration-150">
|
||||||
{page.name}
|
{page.name}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
@ -90,9 +92,9 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
<div>
|
<div>
|
||||||
<span>© 2020 ACME, Inc. All rights reserved.</span>
|
<span>© 2020 ACME, Inc. All rights reserved.</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-accents-4">
|
<div className="flex items-center">
|
||||||
<span>Crafted by</span>
|
<span className="text-accent-3">Crafted by</span>
|
||||||
<a href="https://vercel.com">
|
<a href="https://vercel.com" aria-label="Vercel.com Link">
|
||||||
<img
|
<img
|
||||||
src="/vercel.png"
|
src="/vercel.png"
|
||||||
alt="Vercel.com Logo"
|
alt="Vercel.com Logo"
|
||||||
@ -106,18 +108,22 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPages(pages?: Page[]) {
|
function usePages(pages?: Page[]) {
|
||||||
|
const { locale } = useRouter()
|
||||||
const sitePages: Page[] = []
|
const sitePages: Page[] = []
|
||||||
const legalPages: Page[] = []
|
const legalPages: Page[] = []
|
||||||
|
|
||||||
if (pages) {
|
if (pages) {
|
||||||
pages.forEach((page) => {
|
pages.forEach((page) => {
|
||||||
if (page.url) {
|
const slug = page.url && getSlug(page.url)
|
||||||
if (LEGAL_PAGES.includes(getSlug(page.url))) {
|
|
||||||
legalPages.push(page)
|
if (!slug) return
|
||||||
} else {
|
if (locale && !slug.startsWith(`${locale}/`)) return
|
||||||
sitePages.push(page)
|
|
||||||
}
|
if (isLegalPage(slug, locale)) {
|
||||||
|
legalPages.push(page)
|
||||||
|
} else {
|
||||||
|
sitePages.push(page)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -128,6 +134,11 @@ function getPages(pages?: Page[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLegalPage = (slug: string, locale?: string) =>
|
||||||
|
locale
|
||||||
|
? LEGAL_PAGES.some((p) => `${locale}/${p}` === slug)
|
||||||
|
: LEGAL_PAGES.includes(slug)
|
||||||
|
|
||||||
// Sort pages by the sort order assigned in the BC dashboard
|
// Sort pages by the sort order assigned in the BC dashboard
|
||||||
function bySortOrder(a: Page, b: Page) {
|
function bySortOrder(a: Page, b: Page) {
|
||||||
return (a.sort_order ?? 0) - (b.sort_order ?? 0)
|
return (a.sort_order ?? 0) - (b.sort_order ?? 0)
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@apply h-10 px-2 rounded-md border border-accents-2 flex items-center space-x-2 justify-center;
|
@apply h-10 px-2 rounded-md border border-accents-2 flex items-center space-x-2 justify-center outline-none focus:outline-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdownMenu {
|
.dropdownMenu {
|
||||||
|
@ -1,35 +1,48 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import s from './I18nWidget.module.css'
|
|
||||||
import { Menu } from '@headlessui/react'
|
|
||||||
import { DoubleChevron } from '@components/icon'
|
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { Menu } from '@headlessui/react'
|
||||||
|
import { DoubleChevron } from '@components/icons'
|
||||||
|
import s from './I18nWidget.module.css'
|
||||||
|
|
||||||
|
const LOCALES_MAP: Record<string, string> = {
|
||||||
|
es: 'Español',
|
||||||
|
'en-US': 'English',
|
||||||
|
}
|
||||||
|
|
||||||
const I18nWidget: FC = () => {
|
const I18nWidget: FC = () => {
|
||||||
|
const { locale, locales, defaultLocale = 'en-US' } = useRouter()
|
||||||
|
const options = locales?.filter((val) => val !== locale)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={s.root}>
|
<nav className={s.root}>
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Button className={s.button}>
|
<Menu.Button className={s.button} aria-label="Language selector">
|
||||||
<img className="" src="/flag-us.png" />
|
<img className="" src="/flag-us.png" alt="US Flag" />
|
||||||
<span>English</span>
|
<span>{LOCALES_MAP[locale || defaultLocale]}</span>
|
||||||
<span className="">
|
{options && (
|
||||||
<DoubleChevron />
|
<span className="">
|
||||||
</span>
|
<DoubleChevron />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
<Menu.Items className={s.dropdownMenu}>
|
|
||||||
<Menu.Item>
|
{options?.length ? (
|
||||||
{({ active }) => (
|
<Menu.Items className={s.dropdownMenu}>
|
||||||
<a
|
{options.map((locale) => (
|
||||||
className={cn(s.item, { [s.active]: active })}
|
<Menu.Item key={locale}>
|
||||||
href="#"
|
{({ active }) => (
|
||||||
onClick={(e) => {
|
<Link href="/" locale={locale}>
|
||||||
e.preventDefault()
|
<a className={cn(s.item, { [s.active]: active })}>
|
||||||
}}
|
{LOCALES_MAP[locale]}
|
||||||
>
|
</a>
|
||||||
Español
|
</Link>
|
||||||
</a>
|
)}
|
||||||
)}
|
</Menu.Item>
|
||||||
</Menu.Item>
|
))}
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
|
) : null}
|
||||||
</Menu>
|
</Menu>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useCallback, useEffect, useState } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
import type { Page } from '@lib/bigcommerce/api/operations/get-all-pages'
|
import type { Page } from '@lib/bigcommerce/api/operations/get-all-pages'
|
||||||
import { CommerceProvider } from '@lib/bigcommerce'
|
import { CommerceProvider } from '@lib/bigcommerce'
|
||||||
import { Navbar, Featurebar, Footer } from '@components/core'
|
|
||||||
import { Container, Sidebar } from '@components/ui'
|
|
||||||
import Button from '@components/ui/Button'
|
|
||||||
import { CartSidebarView } from '@components/cart'
|
import { CartSidebarView } from '@components/cart'
|
||||||
|
import { Container, Sidebar, Button, Modal, Toast } from '@components/ui'
|
||||||
|
import { Navbar, Featurebar, Footer } from '@components/core'
|
||||||
|
import { LoginView, SignUpView, ForgotPassword } from '@components/auth'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import s from './Layout.module.css'
|
|
||||||
import { usePreventScroll } from '@react-aria/overlays'
|
import { usePreventScroll } from '@react-aria/overlays'
|
||||||
|
import s from './Layout.module.css'
|
||||||
|
import debounce from 'lodash.debounce'
|
||||||
interface Props {
|
interface Props {
|
||||||
pageProps: {
|
pageProps: {
|
||||||
pages?: Page[]
|
pages?: Page[]
|
||||||
@ -16,32 +18,42 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Layout: FC<Props> = ({ children, pageProps }) => {
|
const Layout: FC<Props> = ({ children, pageProps }) => {
|
||||||
const { displaySidebar, displayDropdown, closeSidebar } = useUI()
|
const {
|
||||||
|
displaySidebar,
|
||||||
|
displayModal,
|
||||||
|
closeSidebar,
|
||||||
|
closeModal,
|
||||||
|
modalView,
|
||||||
|
toastText,
|
||||||
|
closeToast,
|
||||||
|
displayToast,
|
||||||
|
} = useUI()
|
||||||
const [acceptedCookies, setAcceptedCookies] = useState(false)
|
const [acceptedCookies, setAcceptedCookies] = useState(false)
|
||||||
const [hasScrolled, setHasScrolled] = useState(false)
|
const [hasScrolled, setHasScrolled] = useState(false)
|
||||||
|
const { locale = 'en-US' } = useRouter()
|
||||||
|
|
||||||
// TODO: Update code, add throttle and more.
|
usePreventScroll({
|
||||||
useEffect(() => {
|
isDisabled: !(displaySidebar || displayModal),
|
||||||
const offset = 0
|
})
|
||||||
function handleScroll() {
|
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
debounce(() => {
|
||||||
|
const offset = 0
|
||||||
const { scrollTop } = document.documentElement
|
const { scrollTop } = document.documentElement
|
||||||
if (scrollTop > offset) setHasScrolled(true)
|
if (scrollTop > offset) setHasScrolled(true)
|
||||||
else setHasScrolled(false)
|
else setHasScrolled(false)
|
||||||
}
|
}, 1)
|
||||||
document.addEventListener('scroll', handleScroll)
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('scroll', handleScroll)
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('scroll', handleScroll)
|
document.removeEventListener('scroll', handleScroll)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [handleScroll])
|
||||||
|
|
||||||
console.log(displaySidebar, displayDropdown)
|
|
||||||
usePreventScroll({
|
|
||||||
isDisabled: !displaySidebar,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommerceProvider locale="en-us">
|
<CommerceProvider locale={locale}>
|
||||||
<div className={cn(s.root)}>
|
<div className={cn(s.root)}>
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -55,11 +67,15 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
|
|||||||
</header>
|
</header>
|
||||||
<main className="fit">{children}</main>
|
<main className="fit">{children}</main>
|
||||||
<Footer pages={pageProps.pages} />
|
<Footer pages={pageProps.pages} />
|
||||||
|
|
||||||
<Sidebar open={displaySidebar} onClose={closeSidebar}>
|
<Sidebar open={displaySidebar} onClose={closeSidebar}>
|
||||||
<CartSidebarView />
|
<CartSidebarView />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
||||||
|
<Modal open={displayModal} onClose={closeModal}>
|
||||||
|
{modalView === 'LOGIN_VIEW' && <LoginView />}
|
||||||
|
{modalView === 'SIGNUP_VIEW' && <SignUpView />}
|
||||||
|
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
|
||||||
|
</Modal>
|
||||||
<Featurebar
|
<Featurebar
|
||||||
title="This site uses cookies to improve your experience."
|
title="This site uses cookies to improve your experience."
|
||||||
description="By clicking, you agree to our Privacy Policy."
|
description="By clicking, you agree to our Privacy Policy."
|
||||||
@ -70,6 +86,9 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{/* <Toast open={displayToast} onClose={closeModal}>
|
||||||
|
{toastText}
|
||||||
|
</Toast> */}
|
||||||
</div>
|
</div>
|
||||||
</CommerceProvider>
|
</CommerceProvider>
|
||||||
)
|
)
|
||||||
|
@ -15,7 +15,7 @@ const Navbar: FC<Props> = ({ className }) => {
|
|||||||
<div className="flex justify-between align-center flex-row py-4 md:py-6 relative">
|
<div className="flex justify-between align-center flex-row py-4 md:py-6 relative">
|
||||||
<div className="flex flex-1 items-center">
|
<div className="flex flex-1 items-center">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a className="cursor-pointer">
|
<a className="cursor-pointer" aria-label="Logo">
|
||||||
<Logo />
|
<Logo />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
@ -42,7 +42,7 @@ const Navbar: FC<Props> = ({ className }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex pb-4 lg:px-6 lg:hidden">
|
<div className="flex pb-4 lg:px-6 lg:hidden">
|
||||||
<Searchbar />
|
<Searchbar id="mobileSearch" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -3,10 +3,6 @@
|
|||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input:focus {
|
|
||||||
@apply outline-none shadow-outline-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconContainer {
|
.iconContainer {
|
||||||
@apply absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none;
|
@apply absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none;
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,10 @@ import { useRouter } from 'next/router'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
|
id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Searchbar: FC<Props> = ({ className }) => {
|
const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -21,27 +22,30 @@ const Searchbar: FC<Props> = ({ className }) => {
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<input
|
<label htmlFor={id}>
|
||||||
className={s.input}
|
<input
|
||||||
placeholder="Search for products..."
|
id={id}
|
||||||
defaultValue={router.query.q}
|
className={s.input}
|
||||||
onKeyUp={(e) => {
|
placeholder="Search for products..."
|
||||||
e.preventDefault()
|
defaultValue={router.query.q}
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
const q = e.currentTarget.value
|
const q = e.currentTarget.value
|
||||||
|
|
||||||
router.push(
|
router.push(
|
||||||
{
|
{
|
||||||
pathname: `/search`,
|
pathname: `/search`,
|
||||||
query: q ? { q } : {},
|
query: q ? { q } : {},
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
{ shallow: true }
|
{ shallow: true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</label>
|
||||||
<div className={s.iconContainer}>
|
<div className={s.iconContainer}>
|
||||||
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
|
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path
|
<path
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { FC } from 'react'
|
import React, { FC } from 'react'
|
||||||
import { Switch } from '@headlessui/react'
|
import { Switch } from '@headlessui/react'
|
||||||
import { HiSun, HiMoon } from 'react-icons/hi'
|
import { Moon, Sun } from '@components/icons'
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
checked: boolean
|
checked: boolean
|
||||||
@ -35,7 +35,7 @@ const Toggle: FC<Props> = ({ className, checked, onChange }) => {
|
|||||||
: 'opacity-100 ease-in duration-150'
|
: 'opacity-100 ease-in duration-150'
|
||||||
} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`}
|
} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`}
|
||||||
>
|
>
|
||||||
<HiSun className="h-3 w-3 text-gray-400" />
|
<Sun className="h-3 w-3 text-accent-3" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`${
|
className={`${
|
||||||
@ -44,7 +44,7 @@ const Toggle: FC<Props> = ({ className, checked, onChange }) => {
|
|||||||
: 'opacity-0 ease-out duration-150'
|
: 'opacity-0 ease-out duration-150'
|
||||||
} opacity-0 ease-out duration-150 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`}
|
} opacity-0 ease-out duration-150 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`}
|
||||||
>
|
>
|
||||||
<HiMoon className="h-3 w-3 text-yellow-400" />
|
<Moon className="h-3 w-3 text-yellow-400" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -3,16 +3,31 @@ import Link from 'next/link'
|
|||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import s from './DropdownMenu.module.css'
|
import s from './DropdownMenu.module.css'
|
||||||
import { Moon, Sun } from '@components/icon'
|
import { Moon, Sun } from '@components/icons'
|
||||||
import { Menu, Transition } from '@headlessui/react'
|
import { Menu, Transition } from '@headlessui/react'
|
||||||
|
import useLogout from '@lib/bigcommerce/use-logout'
|
||||||
interface DropdownMenuProps {
|
interface DropdownMenuProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LINKS = [
|
||||||
|
{
|
||||||
|
name: 'My Orders',
|
||||||
|
href: '/orders',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'My Profile',
|
||||||
|
href: '/profile',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cart',
|
||||||
|
href: '/cart',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
|
const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
|
const logout = useLogout()
|
||||||
return (
|
return (
|
||||||
<Transition
|
<Transition
|
||||||
show={open}
|
show={open}
|
||||||
@ -24,39 +39,41 @@ const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
|
|||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className={s.dropdownMenu}>
|
<Menu.Items className={s.dropdownMenu}>
|
||||||
|
{LINKS.map(({ name, href }) => (
|
||||||
|
<Menu.Item key={href}>
|
||||||
|
{({ active }) => (
|
||||||
|
<Link href={href}>
|
||||||
|
<a className={cn(s.link, { [s.active]: active })}>{name}</a>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
{({ active }) => <a className={s.link}>My Purchases</a>}
|
<a
|
||||||
|
className={cn(s.link, 'justify-between')}
|
||||||
|
onClick={() =>
|
||||||
|
theme === 'dark' ? setTheme('light') : setTheme('dark')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Theme: <strong>{theme}</strong>{' '}
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
{theme == 'dark' ? (
|
||||||
|
<Moon width={20} height={20} />
|
||||||
|
) : (
|
||||||
|
<Sun width="20" height={20} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
{({ active }) => <a className={s.link}>My Account</a>}
|
<a
|
||||||
</Menu.Item>
|
className={cn(s.link, 'border-t border-accents-2 mt-4')}
|
||||||
<Menu.Item>
|
onClick={() => logout()}
|
||||||
{({ active }) => (
|
>
|
||||||
<a
|
Logout
|
||||||
className={cn(s.link, 'justify-between')}
|
</a>
|
||||||
onClick={() =>
|
|
||||||
theme === 'dark' ? setTheme('light') : setTheme('dark')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
Theme: <strong>{theme}</strong>{' '}
|
|
||||||
</div>
|
|
||||||
<div className="ml-3">
|
|
||||||
{theme == 'dark' ? (
|
|
||||||
<Moon width={20} height={20} />
|
|
||||||
) : (
|
|
||||||
<Sun width="20" height={20} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item>
|
|
||||||
{({ active }) => (
|
|
||||||
<a className={cn(s.link, 'border-t border-accents-2 mt-4')}>
|
|
||||||
Logout
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
@apply relative;
|
@apply relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainContainer {
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
@apply flex flex-row items-center justify-items-end h-full;
|
@apply flex flex-row items-center justify-items-end h-full;
|
||||||
}
|
}
|
||||||
@ -25,3 +22,11 @@
|
|||||||
@apply outline-none;
|
@apply outline-none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bagCount {
|
||||||
|
@apply border border-accents-1 bg-secondary text-secondary h-4 w-4 absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarButton {
|
||||||
|
@apply inline-flex justify-center rounded-full outline-none focus:outline-none;
|
||||||
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import s from './UserNav.module.css'
|
import s from './UserNav.module.css'
|
||||||
import { FC, useRef } from 'react'
|
import { FC } from 'react'
|
||||||
|
import { Heart, Bag } from '@components/icons'
|
||||||
import { Avatar } from '@components/core'
|
import { Avatar } from '@components/core'
|
||||||
import { Heart, Bag } from '@components/icon'
|
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
|
import { LoginView } from '@components/auth'
|
||||||
import DropdownMenu from './DropdownMenu'
|
import DropdownMenu from './DropdownMenu'
|
||||||
import { Menu } from '@headlessui/react'
|
import { Menu } from '@headlessui/react'
|
||||||
import useCart from '@lib/bigcommerce/cart/use-cart'
|
import useCart from '@lib/bigcommerce/cart/use-cart'
|
||||||
|
import useCustomer from '@lib/bigcommerce/use-customer'
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
@ -19,25 +20,20 @@ const countItems = (count: number, items: any[]) =>
|
|||||||
|
|
||||||
const UserNav: FC<Props> = ({ className, children, ...props }) => {
|
const UserNav: FC<Props> = ({ className, children, ...props }) => {
|
||||||
const { data } = useCart()
|
const { data } = useCart()
|
||||||
const { openSidebar, closeSidebar, displaySidebar } = useUI()
|
const { data: customer } = useCustomer()
|
||||||
|
|
||||||
|
const { openSidebar, closeSidebar, displaySidebar, openModal } = useUI()
|
||||||
const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0)
|
const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0)
|
||||||
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={cn(s.root, className)}>
|
<nav className={cn(s.root, className)}>
|
||||||
<div className={s.mainContainer}>
|
<div className={s.mainContainer}>
|
||||||
<ul className={s.list}>
|
<ul className={s.list}>
|
||||||
<li
|
<li
|
||||||
className={s.item}
|
className={s.item}
|
||||||
onClick={() => (displaySidebar ? closeSidebar() : openSidebar())}
|
onClick={(e) => (displaySidebar ? closeSidebar() : openSidebar())}
|
||||||
>
|
>
|
||||||
<Bag />
|
<Bag />
|
||||||
{itemsCount > 0 && (
|
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
||||||
<span className="border border-accent-1 bg-secondary text-secondary h-4 w-4 absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs">
|
|
||||||
{itemsCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</li>
|
</li>
|
||||||
<Link href="/wishlist">
|
<Link href="/wishlist">
|
||||||
<li className={s.item}>
|
<li className={s.item}>
|
||||||
@ -45,18 +41,26 @@ const UserNav: FC<Props> = ({ className, children, ...props }) => {
|
|||||||
</li>
|
</li>
|
||||||
</Link>
|
</Link>
|
||||||
<li className={s.item}>
|
<li className={s.item}>
|
||||||
<Menu>
|
{customer ? (
|
||||||
{({ open }) => {
|
<Menu>
|
||||||
return (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Menu.Button className="inline-flex justify-center rounded-full">
|
<Menu.Button className={s.avatarButton} aria-label="Menu">
|
||||||
<Avatar />
|
<Avatar />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
<DropdownMenu open={open} />
|
<DropdownMenu open={open} />
|
||||||
</>
|
</>
|
||||||
)
|
)}
|
||||||
}}
|
</Menu>
|
||||||
</Menu>
|
) : (
|
||||||
|
<button
|
||||||
|
className={s.avatarButton}
|
||||||
|
aria-label="Menu"
|
||||||
|
onClick={() => openModal()}
|
||||||
|
>
|
||||||
|
<Avatar />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,3 +9,4 @@ export { default as Toggle } from './Toggle'
|
|||||||
export { default as Head } from './Head'
|
export { default as Head } from './Head'
|
||||||
export { default as HTMLContent } from './HTMLContent'
|
export { default as HTMLContent } from './HTMLContent'
|
||||||
export { default as I18nWidget } from './I18nWidget'
|
export { default as I18nWidget } from './I18nWidget'
|
||||||
|
export { default as EnhancedImage } from './EnhancedImage'
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
const Cross = ({ ...props }) => {
|
|
||||||
return (
|
|
||||||
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" {...props}>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Cross
|
|
21
components/icons/Cross.tsx
Normal file
21
components/icons/Cross.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const Cross = ({ ...props }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="none"
|
||||||
|
shape-rendering="geometricPrecision"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M18 6L6 18" />
|
||||||
|
<path d="M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Cross
|
22
components/icons/Info.tsx
Normal file
22
components/icons/Info.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const Info = ({ ...props }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="none"
|
||||||
|
shape-rendering="geometricPrecision"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" fill="transparent" />
|
||||||
|
<path d="M12 8v4" stroke="currentColor" />
|
||||||
|
<path d="M12 16h.01" stroke="currentColor" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Info
|
@ -11,3 +11,4 @@ export { default as Moon } from './Moon'
|
|||||||
export { default as Github } from './Github'
|
export { default as Github } from './Github'
|
||||||
export { default as DoubleChevron } from './DoubleChevron'
|
export { default as DoubleChevron } from './DoubleChevron'
|
||||||
export { default as RightArrow } from './RightArrow'
|
export { default as RightArrow } from './RightArrow'
|
||||||
|
export { default as Info } from './Info'
|
@ -1,9 +1,10 @@
|
|||||||
import { FC, ReactNode, Component } from 'react'
|
import React, { FC, ReactNode, Component } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Image from 'next/image'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-all-products'
|
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-all-products'
|
||||||
import { Heart } from '@components/icon'
|
import usePrice from '@lib/bigcommerce/use-price'
|
||||||
|
import { Heart } from '@components/icons'
|
||||||
|
import { EnhancedImage } from '@components/core'
|
||||||
import s from './ProductCard.module.css'
|
import s from './ProductCard.module.css'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -25,6 +26,11 @@ const ProductCard: FC<Props> = ({
|
|||||||
priority,
|
priority,
|
||||||
}) => {
|
}) => {
|
||||||
const src = p.images.edges?.[0]?.node.urlOriginal!
|
const src = p.images.edges?.[0]?.node.urlOriginal!
|
||||||
|
const { price } = usePrice({
|
||||||
|
amount: p.prices?.price?.value,
|
||||||
|
baseAmount: p.prices?.retailPrice?.value,
|
||||||
|
currencyCode: p.prices?.price?.currencyCode!,
|
||||||
|
})
|
||||||
|
|
||||||
if (variant === 'slim') {
|
if (variant === 'slim') {
|
||||||
return (
|
return (
|
||||||
@ -34,8 +40,9 @@ const ProductCard: FC<Props> = ({
|
|||||||
{p.name}
|
{p.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Image
|
<EnhancedImage
|
||||||
src={src}
|
src={p.images.edges?.[0]?.node.urlOriginal!}
|
||||||
|
alt={p.name}
|
||||||
width={imgWidth}
|
width={imgWidth}
|
||||||
height={imgHeight}
|
height={imgHeight}
|
||||||
priority={priority}
|
priority={priority}
|
||||||
@ -56,14 +63,15 @@ const ProductCard: FC<Props> = ({
|
|||||||
<h3 className={s.productTitle}>
|
<h3 className={s.productTitle}>
|
||||||
<span>{p.name}</span>
|
<span>{p.name}</span>
|
||||||
</h3>
|
</h3>
|
||||||
<span className={s.productPrice}>${p.prices?.price.value}</span>
|
<span className={s.productPrice}>{price}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={s.wishlistButton}>
|
<div className={s.wishlistButton}>
|
||||||
<Heart />
|
<Heart />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(s.imageContainer)}>
|
<div className={cn(s.imageContainer)}>
|
||||||
<Image
|
<EnhancedImage
|
||||||
|
alt={p.name}
|
||||||
className={cn('w-full object-cover', s['product-image'])}
|
className={cn('w-full object-cover', s['product-image'])}
|
||||||
src={src}
|
src={src}
|
||||||
width={imgWidth}
|
width={imgWidth}
|
||||||
|
@ -19,10 +19,15 @@ const ProductSlider: FC = ({ children }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={s.root}>
|
<div className={s.root}>
|
||||||
<button className={cn(s.leftControl, s.control)} onClick={slider?.prev} />
|
<button
|
||||||
|
className={cn(s.leftControl, s.control)}
|
||||||
|
onClick={slider?.prev}
|
||||||
|
aria-label="Previous Product Image"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
className={cn(s.rightControl, s.control)}
|
className={cn(s.rightControl, s.control)}
|
||||||
onClick={slider?.next}
|
onClick={slider?.next}
|
||||||
|
aria-label="Next Product Image"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -50,6 +55,7 @@ const ProductSlider: FC = ({ children }) => {
|
|||||||
{[...Array(slider.details().size).keys()].map((idx) => {
|
{[...Array(slider.details().size).keys()].map((idx) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
aria-label="Position indicator"
|
||||||
key={idx}
|
key={idx}
|
||||||
className={cn(s.positionIndicator, {
|
className={cn(s.positionIndicator, {
|
||||||
[s.positionIndicatorActive]: currentSlide === idx,
|
[s.positionIndicatorActive]: currentSlide === idx,
|
||||||
|
@ -53,10 +53,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
@apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 w-full;
|
@apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 w-full h-full;
|
||||||
|
|
||||||
@screen lg {
|
@screen lg {
|
||||||
@apply col-span-6 pt-20;
|
@apply col-span-6 py-24 justify-between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,10 +4,11 @@ import Image from 'next/image'
|
|||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
|
|
||||||
import s from './ProductView.module.css'
|
import s from './ProductView.module.css'
|
||||||
import { Heart } from '@components/icon'
|
import { Heart } from '@components/icons'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import { Button, Container } from '@components/ui'
|
|
||||||
import { Swatch, ProductSlider } from '@components/product'
|
import { Swatch, ProductSlider } from '@components/product'
|
||||||
|
import { Button, Container } from '@components/ui'
|
||||||
|
import { HTMLContent } from '@components/core'
|
||||||
|
|
||||||
import useAddItem from '@lib/bigcommerce/cart/use-add-item'
|
import useAddItem from '@lib/bigcommerce/cart/use-add-item'
|
||||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
||||||
@ -77,10 +78,11 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
|||||||
<div className={s.sliderContainer}>
|
<div className={s.sliderContainer}>
|
||||||
<ProductSlider>
|
<ProductSlider>
|
||||||
{product.images.edges?.map((image, i) => (
|
{product.images.edges?.map((image, i) => (
|
||||||
<div key={image?.node.urlXL} className={s.imageContainer}>
|
<div key={image?.node.urlOriginal} className={s.imageContainer}>
|
||||||
<Image
|
<Image
|
||||||
|
alt={product.name}
|
||||||
className={s.img}
|
className={s.img}
|
||||||
src={image?.node.urlXL!}
|
src={image?.node.urlOriginal!}
|
||||||
width={1050}
|
width={1050}
|
||||||
height={1050}
|
height={1050}
|
||||||
priority={i === 0}
|
priority={i === 0}
|
||||||
@ -110,7 +112,6 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
|||||||
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,
|
||||||
@ -123,21 +124,22 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="pb-12">
|
|
||||||
<div
|
<div className="pb-14 break-words w-full max-w-xl">
|
||||||
className="pb-14 break-words w-full"
|
<HTMLContent html={product.description} />
|
||||||
dangerouslySetInnerHTML={{ __html: product.description }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className={s.button}
|
|
||||||
onClick={addToCart}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
Add to Cart
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
aria-label="Add to Cart"
|
||||||
|
type="button"
|
||||||
|
className={s.button}
|
||||||
|
onClick={addToCart}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
Add to Cart
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TODO make it work */}
|
{/* TODO make it work */}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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 { Check } from '@components/icon'
|
import { Check } from '@components/icons'
|
||||||
import Button, { ButtonProps } from '@components/ui/Button'
|
import Button, { ButtonProps } from '@components/ui/Button'
|
||||||
import { isDark } from '@lib/colors'
|
import { isDark } from '@lib/colors'
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -39,6 +39,7 @@ const Swatch: FC<Props & ButtonProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
className={rootClassName}
|
className={rootClassName}
|
||||||
style={color ? { backgroundColor: color } : {}}
|
style={color ? { backgroundColor: color } : {}}
|
||||||
|
aria-label="Variant Swatch"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{variant === 'color' && active && (
|
{variant === 'color' && active && (
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
||||||
|
|
||||||
export function getProductOptions(product: ProductNode) {
|
export function getProductOptions(product: ProductNode) {
|
||||||
// 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(),
|
||||||
values: node.values.edges?.map(({ node }: any) => node),
|
values: node.values.edges?.map(({ node }: any) => node),
|
||||||
|
@ -1 +0,0 @@
|
|||||||
import { Colors } from '@components/ui/types'
|
|
@ -24,3 +24,12 @@
|
|||||||
.slim {
|
.slim {
|
||||||
@apply py-2 transform-none normal-case;
|
@apply py-2 transform-none normal-case;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled,
|
||||||
|
.disabled:hover {
|
||||||
|
@apply text-accents-4 border-accents-2 bg-accents-1 cursor-not-allowed;
|
||||||
|
filter: grayscale(1);
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
-webkit-perspective: 1000;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
@ -19,6 +19,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
Component?: string | JSXElementConstructor<any>
|
Component?: string | JSXElementConstructor<any>
|
||||||
width?: string | number
|
width?: string | number
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
||||||
@ -28,10 +29,10 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
|||||||
children,
|
children,
|
||||||
active,
|
active,
|
||||||
onClick,
|
onClick,
|
||||||
disabled,
|
|
||||||
width,
|
width,
|
||||||
Component = 'button',
|
Component = 'button',
|
||||||
loading = false,
|
loading = false,
|
||||||
|
disabled = false,
|
||||||
style = {},
|
style = {},
|
||||||
...rest
|
...rest
|
||||||
} = props
|
} = props
|
||||||
@ -52,6 +53,7 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
|||||||
{
|
{
|
||||||
[s.slim]: variant === 'slim',
|
[s.slim]: variant === 'slim',
|
||||||
[s.loading]: loading,
|
[s.loading]: loading,
|
||||||
|
[s.disabled]: disabled,
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
)
|
)
|
||||||
@ -64,6 +66,7 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
|||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
data-active={isPressed ? '' : undefined}
|
data-active={isPressed ? '' : undefined}
|
||||||
className={rootClassName}
|
className={rootClassName}
|
||||||
|
disabled={disabled}
|
||||||
style={{
|
style={{
|
||||||
width,
|
width,
|
||||||
...style,
|
...style,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { FC } from 'react'
|
import React, { FC } from 'react'
|
||||||
import { Container } from '@components/ui'
|
import { Container } from '@components/ui'
|
||||||
import { RightArrow } from '@components/icon'
|
import { RightArrow } from '@components/icons'
|
||||||
import s from './Hero.module.css'
|
import s from './Hero.module.css'
|
||||||
|
import Link from 'next/link'
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
headline: string
|
headline: string
|
||||||
@ -21,10 +21,12 @@ const Hero: FC<Props> = ({ headline, description }) => {
|
|||||||
<p className="mt-5 text-xl leading-7 text-accent-2 text-white">
|
<p className="mt-5 text-xl leading-7 text-accent-2 text-white">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
<a className="text-white pt-3 font-bold hover:underline flex flex-row cursor-pointer w-max-content">
|
<Link href="/blog">
|
||||||
<span>Read it here</span>
|
<a className="text-white pt-3 font-bold hover:underline flex flex-row cursor-pointer w-max-content">
|
||||||
<RightArrow width="20" heigh="20" className="ml-1" />
|
Read it here
|
||||||
</a>
|
<RightArrow width="20" heigh="20" className="ml-1" />
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
5
components/ui/Input/Input.module.css
Normal file
5
components/ui/Input/Input.module.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.root {
|
||||||
|
@apply focus:outline-none bg-primary focus:shadow-outline-gray py-2
|
||||||
|
px-6 w-full appearance-none transition duration-150 ease-in-out
|
||||||
|
placeholder-accents-5 pr-10 border border-accents-3 text-accents-6;
|
||||||
|
}
|
35
components/ui/Input/Input.tsx
Normal file
35
components/ui/Input/Input.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import cn from 'classnames'
|
||||||
|
import s from './Input.module.css'
|
||||||
|
import React, { InputHTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
className?: string
|
||||||
|
onChange?: (...args: any[]) => any
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input: React.FC<Props> = (props) => {
|
||||||
|
const { className, children, onChange, ...rest } = props
|
||||||
|
|
||||||
|
const rootClassName = cn(s.root, {}, className)
|
||||||
|
|
||||||
|
const handleOnChange = (e: any) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(e.target.value)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
className={rootClassName}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Input
|
1
components/ui/Input/index.ts
Normal file
1
components/ui/Input/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './Input'
|
@ -19,16 +19,6 @@ const M: FC<Props> = ({ className = '', children, variant = 'primary' }) => {
|
|||||||
className
|
className
|
||||||
)
|
)
|
||||||
|
|
||||||
// return (
|
|
||||||
// <div className={rootClassName}>
|
|
||||||
// <div className={s.container}>
|
|
||||||
// {items.map((p: any) => (
|
|
||||||
// <Component {...p} />
|
|
||||||
// ))}
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// )
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={rootClassName}>
|
<div className={rootClassName}>
|
||||||
<Ticker offset={80}>
|
<Ticker offset={80}>
|
||||||
|
@ -6,3 +6,7 @@
|
|||||||
.modal {
|
.modal {
|
||||||
@apply bg-primary p-12 border border-accents-2;
|
@apply bg-primary p-12 border border-accents-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal:focus {
|
||||||
|
@apply outline-none;
|
||||||
|
}
|
||||||
|
@ -2,43 +2,75 @@ import cn from 'classnames'
|
|||||||
import { FC, useRef } from 'react'
|
import { FC, useRef } from 'react'
|
||||||
import s from './Modal.module.css'
|
import s from './Modal.module.css'
|
||||||
import { useDialog } from '@react-aria/dialog'
|
import { useDialog } from '@react-aria/dialog'
|
||||||
import { useOverlay, useModal } from '@react-aria/overlays'
|
|
||||||
import { FocusScope } from '@react-aria/focus'
|
import { FocusScope } from '@react-aria/focus'
|
||||||
|
import { Transition } from '@headlessui/react'
|
||||||
|
import { useOverlay, useModal, OverlayContainer } from '@react-aria/overlays'
|
||||||
|
import { Cross } from '@components/icons'
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
children?: any
|
children?: any
|
||||||
show?: boolean
|
open?: boolean
|
||||||
close: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal: FC<Props> = ({
|
const Modal: FC<Props> = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
show = true,
|
open = false,
|
||||||
close,
|
onClose,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const rootClassName = cn(s.root, className)
|
const rootClassName = cn(s.root, className)
|
||||||
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
|
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
|
||||||
let { modalProps } = useModal()
|
let { modalProps } = useModal()
|
||||||
let { overlayProps } = useOverlay(props, ref)
|
let { dialogProps } = useDialog({}, ref)
|
||||||
let { dialogProps } = useDialog(props, ref)
|
let { overlayProps } = useOverlay(
|
||||||
|
{
|
||||||
|
isOpen: open,
|
||||||
|
isDismissable: false,
|
||||||
|
onClose: onClose,
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={rootClassName}>
|
<Transition show={open}>
|
||||||
<FocusScope contain restoreFocus autoFocus>
|
<OverlayContainer>
|
||||||
<div
|
<FocusScope contain restoreFocus autoFocus>
|
||||||
{...overlayProps}
|
<div className={rootClassName}>
|
||||||
{...dialogProps}
|
<Transition.Child
|
||||||
{...modalProps}
|
enter="transition-opacity ease-linear duration-300"
|
||||||
ref={ref}
|
enterFrom="opacity-0"
|
||||||
className={s.modal}
|
enterTo="opacity-100"
|
||||||
>
|
leave="transition-opacity ease-linear duration-300"
|
||||||
{children}
|
leaveFrom="opacity-100"
|
||||||
</div>
|
leaveTo="opacity-0"
|
||||||
</FocusScope>
|
>
|
||||||
</div>
|
<div
|
||||||
|
className={s.modal}
|
||||||
|
{...overlayProps}
|
||||||
|
{...dialogProps}
|
||||||
|
{...modalProps}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<div className="h-7 flex items-center justify-end w-full">
|
||||||
|
<button
|
||||||
|
onClick={() => onClose()}
|
||||||
|
aria-label="Close panel"
|
||||||
|
className="hover:text-gray-500 transition ease-in-out duration-150 focus:outline-none"
|
||||||
|
>
|
||||||
|
<Cross className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</FocusScope>
|
||||||
|
</OverlayContainer>
|
||||||
|
</Transition>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
15
components/ui/Text/Text.module.css
Normal file
15
components/ui/Text/Text.module.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.body {
|
||||||
|
@apply text-lg leading-7 font-medium max-w-6xl mx-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
@apply text-5xl mb-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageHeading {
|
||||||
|
@apply pt-1 pb-4 text-2xl leading-7 font-bold text-base tracking-wide;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeading {
|
||||||
|
@apply pt-1 pb-2 text-base font-semibold leading-7 text-base tracking-wider uppercase border-b border-accents-2 mb-3;
|
||||||
|
}
|
58
components/ui/Text/Text.tsx
Normal file
58
components/ui/Text/Text.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import React, {
|
||||||
|
FunctionComponent,
|
||||||
|
JSXElementConstructor,
|
||||||
|
CSSProperties,
|
||||||
|
} from 'react'
|
||||||
|
import cn from 'classnames'
|
||||||
|
import s from './Text.module.css'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
variant?: Variant
|
||||||
|
className?: string
|
||||||
|
style?: CSSProperties
|
||||||
|
children: React.ReactNode | any
|
||||||
|
}
|
||||||
|
|
||||||
|
type Variant = 'heading' | 'body' | 'pageHeading' | 'sectionHeading'
|
||||||
|
|
||||||
|
const Text: FunctionComponent<Props> = ({
|
||||||
|
style,
|
||||||
|
className = '',
|
||||||
|
variant = 'body',
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const componentsMap: {
|
||||||
|
[P in Variant]: React.ComponentType<any> | string
|
||||||
|
} = {
|
||||||
|
body: 'p',
|
||||||
|
heading: 'h1',
|
||||||
|
pageHeading: 'h1',
|
||||||
|
sectionHeading: 'h2',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component:
|
||||||
|
| JSXElementConstructor<any>
|
||||||
|
| React.ReactElement<any>
|
||||||
|
| React.ComponentType<any>
|
||||||
|
| string = componentsMap![variant!]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
className={cn(
|
||||||
|
s.root,
|
||||||
|
{
|
||||||
|
[s.body]: variant === 'body',
|
||||||
|
[s.heading]: variant === 'heading',
|
||||||
|
[s.pageHeading]: variant === 'pageHeading',
|
||||||
|
[s.sectionHeading]: variant === 'sectionHeading',
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Text
|
1
components/ui/Text/index.ts
Normal file
1
components/ui/Text/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './Text'
|
9
components/ui/Toast/Toast.module.css
Normal file
9
components/ui/Toast/Toast.module.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.root {
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
@apply absolute bg-primary text-primary flex items-center border border-accents-1
|
||||||
|
rounded-md z-50 shadow-2xl top-0 right-0 p-6 my-6 mx-3;
|
||||||
|
width: 420px;
|
||||||
|
z-index: 20000;
|
||||||
|
}
|
73
components/ui/Toast/Toast.tsx
Normal file
73
components/ui/Toast/Toast.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import cn from 'classnames'
|
||||||
|
import { FC, useRef, useEffect, useCallback } from 'react'
|
||||||
|
import s from './Toast.module.css'
|
||||||
|
import { useDialog } from '@react-aria/dialog'
|
||||||
|
import { FocusScope } from '@react-aria/focus'
|
||||||
|
import { Transition } from '@headlessui/react'
|
||||||
|
import { useOverlay, useModal, OverlayContainer } from '@react-aria/overlays'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string
|
||||||
|
children?: any
|
||||||
|
open?: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Toast: FC<Props> = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
open = false,
|
||||||
|
onClose,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const rootClassName = cn(s.root, className)
|
||||||
|
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
|
||||||
|
let { modalProps } = useModal()
|
||||||
|
let { dialogProps } = useDialog({}, ref)
|
||||||
|
let { overlayProps } = useOverlay(
|
||||||
|
{
|
||||||
|
isOpen: open,
|
||||||
|
isDismissable: true,
|
||||||
|
onClose: onClose,
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// setTimeout(() => {
|
||||||
|
// useCallback(onClose, [])
|
||||||
|
// }, 400)
|
||||||
|
// })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition show={open}>
|
||||||
|
<OverlayContainer>
|
||||||
|
<FocusScope contain restoreFocus autoFocus>
|
||||||
|
<div className={rootClassName}>
|
||||||
|
<Transition.Child
|
||||||
|
enter="transition-opacity ease-linear duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="transition-opacity ease-linear duration-300"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={s.toast}
|
||||||
|
{...overlayProps}
|
||||||
|
{...dialogProps}
|
||||||
|
{...modalProps}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</FocusScope>
|
||||||
|
</OverlayContainer>
|
||||||
|
</Transition>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Toast
|
1
components/ui/Toast/index.ts
Normal file
1
components/ui/Toast/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './Toast'
|
@ -5,11 +5,19 @@ import { SSRProvider, OverlayProvider } from 'react-aria'
|
|||||||
export interface State {
|
export interface State {
|
||||||
displaySidebar: boolean
|
displaySidebar: boolean
|
||||||
displayDropdown: boolean
|
displayDropdown: boolean
|
||||||
|
displayModal: boolean
|
||||||
|
displayToast: boolean
|
||||||
|
modalView: string
|
||||||
|
toastText: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
displaySidebar: false,
|
displaySidebar: false,
|
||||||
displayDropdown: false,
|
displayDropdown: false,
|
||||||
|
displayModal: false,
|
||||||
|
modalView: 'LOGIN_VIEW',
|
||||||
|
displayToast: false,
|
||||||
|
toastText: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
@ -19,12 +27,39 @@ type Action =
|
|||||||
| {
|
| {
|
||||||
type: 'CLOSE_SIDEBAR'
|
type: 'CLOSE_SIDEBAR'
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'OPEN_TOAST'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'CLOSE_TOAST'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'SET_TOAST_TEXT'
|
||||||
|
text: ToastText
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'OPEN_DROPDOWN'
|
type: 'OPEN_DROPDOWN'
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'CLOSE_DROPDOWN'
|
type: 'CLOSE_DROPDOWN'
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'OPEN_MODAL'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'CLOSE_MODAL'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'SET_MODAL_VIEW'
|
||||||
|
view: 'LOGIN_VIEW'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'SET_MODAL_VIEW'
|
||||||
|
view: 'SIGNUP_VIEW'
|
||||||
|
}
|
||||||
|
|
||||||
|
type MODAL_VIEWS = 'SIGNUP_VIEW' | 'LOGIN_VIEW' | 'FORGOT_VIEW'
|
||||||
|
type ToastText = string
|
||||||
|
|
||||||
export const UIContext = React.createContext<State | any>(initialState)
|
export const UIContext = React.createContext<State | any>(initialState)
|
||||||
|
|
||||||
@ -56,6 +91,42 @@ function uiReducer(state: State, action: Action) {
|
|||||||
displayDropdown: false,
|
displayDropdown: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'OPEN_MODAL': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
displayModal: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'CLOSE_MODAL': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
displayModal: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'OPEN_TOAST': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
displayToast: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'CLOSE_TOAST': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
displayToast: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'SET_MODAL_VIEW': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
modalView: action.view,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'SET_TOAST_TEXT': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toastText: action.text,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,14 +139,32 @@ export const UIProvider: FC = (props) => {
|
|||||||
const openDropdown = () => dispatch({ type: 'OPEN_DROPDOWN' })
|
const openDropdown = () => dispatch({ type: 'OPEN_DROPDOWN' })
|
||||||
const closeDropdown = () => dispatch({ type: 'CLOSE_DROPDOWN' })
|
const closeDropdown = () => dispatch({ type: 'CLOSE_DROPDOWN' })
|
||||||
|
|
||||||
|
const openModal = () => dispatch({ type: 'OPEN_MODAL' })
|
||||||
|
const closeModal = () => dispatch({ type: 'CLOSE_MODAL' })
|
||||||
|
|
||||||
|
const openToast = () => dispatch({ type: 'OPEN_TOAST' })
|
||||||
|
const closeToast = () => dispatch({ type: 'CLOSE_TOAST' })
|
||||||
|
|
||||||
|
const setModalView = (view: MODAL_VIEWS) =>
|
||||||
|
dispatch({ type: 'SET_MODAL_VIEW', view })
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
...state,
|
...state,
|
||||||
openSidebar,
|
openSidebar,
|
||||||
closeSidebar,
|
closeSidebar,
|
||||||
openDropdown,
|
openDropdown,
|
||||||
closeDropdown,
|
closeDropdown,
|
||||||
|
openModal,
|
||||||
|
closeModal,
|
||||||
|
setModalView,
|
||||||
|
openToast,
|
||||||
|
closeToast,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
openToast()
|
||||||
|
}, 200)
|
||||||
|
|
||||||
return <UIContext.Provider value={value} {...props} />
|
return <UIContext.Provider value={value} {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,3 +8,6 @@ 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'
|
export { default as Modal } from './Modal'
|
||||||
|
export { default as Text } from './Text'
|
||||||
|
export { default as Input } from './Input'
|
||||||
|
export { default as Toast } from './Toast'
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export type Colors = 'violet' | 'black' | 'pink' | 'white'
|
|
@ -1,5 +1,5 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { Trash } from '@components/icon'
|
import { Trash } from '@components/icons'
|
||||||
import s from './WishlistCard.module.css'
|
import s from './WishlistCard.module.css'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -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 productApi: BigcommerceApiHandler<
|
const productsApi: BigcommerceApiHandler<
|
||||||
SearchProductsData,
|
SearchProductsData,
|
||||||
ProductsHandlers
|
ProductsHandlers
|
||||||
> = async (req, res, config, handlers) => {
|
> = async (req, res, config, handlers) => {
|
||||||
@ -45,4 +45,4 @@ const productApi: BigcommerceApiHandler<
|
|||||||
|
|
||||||
export const handlers = { getProducts }
|
export const handlers = { getProducts }
|
||||||
|
|
||||||
export default createApiHandler(productApi, handlers, {})
|
export default createApiHandler(productsApi, handlers, {})
|
||||||
|
@ -1,15 +1,3 @@
|
|||||||
export const responsiveImageFragment = /* GraphQL */ `
|
|
||||||
fragment responsiveImage on Image {
|
|
||||||
urlSmall: url(width: $imgSmallWidth, height: $imgSmallHeight)
|
|
||||||
urlMedium: url(width: $imgMediumWidth, height: $imgMediumHeight)
|
|
||||||
urlLarge: url(width: $imgLargeWidth, height: $imgLargeHeight)
|
|
||||||
urlXL: url(width: $imgXLWidth, height: $imgXLHeight)
|
|
||||||
urlOriginal
|
|
||||||
altText
|
|
||||||
isDefault
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const swatchOptionFragment = /* GraphQL */ `
|
export const swatchOptionFragment = /* GraphQL */ `
|
||||||
fragment swatchOption on SwatchOptionValue {
|
fragment swatchOption on SwatchOptionValue {
|
||||||
isDefault
|
isDefault
|
||||||
@ -51,11 +39,17 @@ export const productInfoFragment = /* GraphQL */ `
|
|||||||
value
|
value
|
||||||
currencyCode
|
currencyCode
|
||||||
}
|
}
|
||||||
|
retailPrice {
|
||||||
|
value
|
||||||
|
currencyCode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
images {
|
images {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
...responsiveImage
|
urlOriginal
|
||||||
|
altText
|
||||||
|
isDefault
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,7 +58,9 @@ export const productInfoFragment = /* GraphQL */ `
|
|||||||
node {
|
node {
|
||||||
entityId
|
entityId
|
||||||
defaultImage {
|
defaultImage {
|
||||||
...responsiveImage
|
urlOriginal
|
||||||
|
altText
|
||||||
|
isDefault
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,9 +74,17 @@ export const productInfoFragment = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
localeMeta: metafields(namespace: $locale, keys: ["name", "description"])
|
||||||
|
@include(if: $hasLocale) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
${responsiveImageFragment}
|
|
||||||
${multipleChoiceOptionFragment}
|
${multipleChoiceOptionFragment}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -1,44 +1,18 @@
|
|||||||
import { CommerceAPIConfig } from 'lib/commerce/api'
|
import type { RequestInit } from '@vercel/fetch'
|
||||||
import { GetAllProductsQueryVariables } from '../schema'
|
import type { CommerceAPIConfig } from 'lib/commerce/api'
|
||||||
import fetchGraphqlApi from './utils/fetch-graphql-api'
|
import fetchGraphqlApi from './utils/fetch-graphql-api'
|
||||||
import fetchStoreApi from './utils/fetch-store-api'
|
import fetchStoreApi from './utils/fetch-store-api'
|
||||||
|
|
||||||
export interface Images {
|
export interface BigcommerceConfig extends CommerceAPIConfig {
|
||||||
small?: ImageOptions
|
// Indicates if the returned metadata with translations should be applied to the
|
||||||
medium?: ImageOptions
|
// data or returned as it is
|
||||||
large?: ImageOptions
|
applyLocale?: boolean
|
||||||
xl?: ImageOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ImageOptions {
|
|
||||||
width: number
|
|
||||||
height?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProductImageVariables = Pick<
|
|
||||||
GetAllProductsQueryVariables,
|
|
||||||
| 'imgSmallWidth'
|
|
||||||
| 'imgSmallHeight'
|
|
||||||
| 'imgMediumWidth'
|
|
||||||
| 'imgMediumHeight'
|
|
||||||
| 'imgLargeWidth'
|
|
||||||
| 'imgLargeHeight'
|
|
||||||
| 'imgXLWidth'
|
|
||||||
| 'imgXLHeight'
|
|
||||||
>
|
|
||||||
|
|
||||||
export interface BigcommerceConfigOptions extends CommerceAPIConfig {
|
|
||||||
images?: Images
|
|
||||||
storeApiUrl: string
|
storeApiUrl: string
|
||||||
storeApiToken: string
|
storeApiToken: string
|
||||||
storeApiClientId: string
|
storeApiClientId: string
|
||||||
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
|
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BigcommerceConfig extends BigcommerceConfigOptions {
|
|
||||||
readonly imageVariables?: ProductImageVariables
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_URL = process.env.BIGCOMMERCE_STOREFRONT_API_URL
|
const API_URL = process.env.BIGCOMMERCE_STOREFRONT_API_URL
|
||||||
const API_TOKEN = process.env.BIGCOMMERCE_STOREFRONT_API_TOKEN
|
const API_TOKEN = process.env.BIGCOMMERCE_STOREFRONT_API_TOKEN
|
||||||
const STORE_API_URL = process.env.BIGCOMMERCE_STORE_API_URL
|
const STORE_API_URL = process.env.BIGCOMMERCE_STORE_API_URL
|
||||||
@ -66,39 +40,20 @@ if (!(STORE_API_URL && STORE_API_TOKEN && STORE_API_CLIENT_ID)) {
|
|||||||
export class Config {
|
export class Config {
|
||||||
private config: BigcommerceConfig
|
private config: BigcommerceConfig
|
||||||
|
|
||||||
constructor(config: Omit<BigcommerceConfigOptions, 'customerCookie'>) {
|
constructor(config: Omit<BigcommerceConfig, 'customerCookie'>) {
|
||||||
this.config = {
|
this.config = {
|
||||||
...config,
|
...config,
|
||||||
// The customerCookie is not customizable for now, BC sets the cookie and it's
|
// The customerCookie is not customizable for now, BC sets the cookie and it's
|
||||||
// not important to rename it
|
// not important to rename it
|
||||||
customerCookie: 'SHOP_TOKEN',
|
customerCookie: 'SHOP_TOKEN',
|
||||||
imageVariables: this.getImageVariables(config.images),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getImageVariables(images?: Images) {
|
|
||||||
return images
|
|
||||||
? {
|
|
||||||
imgSmallWidth: images.small?.width,
|
|
||||||
imgSmallHeight: images.small?.height,
|
|
||||||
imgMediumWidth: images.medium?.height,
|
|
||||||
imgMediumHeight: images.medium?.height,
|
|
||||||
imgLargeWidth: images.large?.height,
|
|
||||||
imgLargeHeight: images.large?.height,
|
|
||||||
imgXLWidth: images.xl?.height,
|
|
||||||
imgXLHeight: images.xl?.height,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
getConfig(userConfig: Partial<BigcommerceConfig> = {}) {
|
getConfig(userConfig: Partial<BigcommerceConfig> = {}) {
|
||||||
const { images: configImages, ...config } = this.config
|
return Object.entries(userConfig).reduce<BigcommerceConfig>(
|
||||||
const images = { ...configImages, ...userConfig.images }
|
(cfg, [key, value]) => Object.assign(cfg, { [key]: value }),
|
||||||
|
{ ...this.config }
|
||||||
return Object.assign(config, userConfig, {
|
)
|
||||||
images,
|
|
||||||
imageVariables: this.getImageVariables(images),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfig(newConfig: Partial<BigcommerceConfig>) {
|
setConfig(newConfig: Partial<BigcommerceConfig>) {
|
||||||
@ -113,6 +68,7 @@ const config = new Config({
|
|||||||
cartCookie: process.env.BIGCOMMERCE_CART_COOKIE ?? 'bc_cartId',
|
cartCookie: process.env.BIGCOMMERCE_CART_COOKIE ?? 'bc_cartId',
|
||||||
cartCookieMaxAge: ONE_DAY * 30,
|
cartCookieMaxAge: ONE_DAY * 30,
|
||||||
fetch: fetchGraphqlApi,
|
fetch: fetchGraphqlApi,
|
||||||
|
applyLocale: true,
|
||||||
// REST API only
|
// REST API only
|
||||||
storeApiUrl: STORE_API_URL,
|
storeApiUrl: STORE_API_URL,
|
||||||
storeApiToken: STORE_API_TOKEN,
|
storeApiToken: STORE_API_TOKEN,
|
||||||
|
@ -4,21 +4,16 @@ 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 setProductLocaleMeta from '../utils/set-product-locale-meta'
|
||||||
import { productConnectionFragment } from '../fragments/product'
|
import { productConnectionFragment } from '../fragments/product'
|
||||||
import { BigcommerceConfig, getConfig, Images, ProductImageVariables } from '..'
|
import { BigcommerceConfig, getConfig } from '..'
|
||||||
|
|
||||||
export const getAllProductsQuery = /* GraphQL */ `
|
export const getAllProductsQuery = /* GraphQL */ `
|
||||||
query getAllProducts(
|
query getAllProducts(
|
||||||
|
$hasLocale: Boolean = false
|
||||||
|
$locale: String = "null"
|
||||||
$entityIds: [Int!]
|
$entityIds: [Int!]
|
||||||
$first: Int = 10
|
$first: Int = 10
|
||||||
$imgSmallWidth: Int = 320
|
|
||||||
$imgSmallHeight: Int
|
|
||||||
$imgMediumWidth: Int = 640
|
|
||||||
$imgMediumHeight: Int
|
|
||||||
$imgLargeWidth: Int = 960
|
|
||||||
$imgLargeHeight: Int
|
|
||||||
$imgXLWidth: Int = 1280
|
|
||||||
$imgXLHeight: Int
|
|
||||||
$products: Boolean = false
|
$products: Boolean = false
|
||||||
$featuredProducts: Boolean = false
|
$featuredProducts: Boolean = false
|
||||||
$bestSellingProducts: Boolean = false
|
$bestSellingProducts: Boolean = false
|
||||||
@ -68,8 +63,10 @@ export type ProductTypes =
|
|||||||
| 'bestSellingProducts'
|
| 'bestSellingProducts'
|
||||||
| 'newestProducts'
|
| 'newestProducts'
|
||||||
|
|
||||||
export type ProductVariables = { field?: ProductTypes } & Images &
|
export type ProductVariables = { field?: ProductTypes } & Omit<
|
||||||
Omit<GetAllProductsQueryVariables, ProductTypes | keyof ProductImageVariables>
|
GetAllProductsQueryVariables,
|
||||||
|
ProductTypes | 'hasLocale'
|
||||||
|
>
|
||||||
|
|
||||||
async function getAllProducts(opts?: {
|
async function getAllProducts(opts?: {
|
||||||
variables?: ProductVariables
|
variables?: ProductVariables
|
||||||
@ -96,9 +93,11 @@ async function getAllProducts({
|
|||||||
} = {}): Promise<GetAllProductsResult> {
|
} = {}): Promise<GetAllProductsResult> {
|
||||||
config = getConfig(config)
|
config = getConfig(config)
|
||||||
|
|
||||||
|
const locale = vars.locale || config.locale
|
||||||
const variables: GetAllProductsQueryVariables = {
|
const variables: GetAllProductsQueryVariables = {
|
||||||
...config.imageVariables,
|
|
||||||
...vars,
|
...vars,
|
||||||
|
locale,
|
||||||
|
hasLocale: !!locale,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!FIELDS.includes(field)) {
|
if (!FIELDS.includes(field)) {
|
||||||
@ -115,11 +114,16 @@ async function getAllProducts({
|
|||||||
query,
|
query,
|
||||||
{ variables }
|
{ variables }
|
||||||
)
|
)
|
||||||
const products = data.site?.[field]?.edges
|
const edges = data.site?.[field]?.edges
|
||||||
|
const products = filterEdges(edges as RecursiveRequired<typeof edges>)
|
||||||
|
|
||||||
return {
|
if (locale && config.applyLocale) {
|
||||||
products: filterEdges(products as RecursiveRequired<typeof products>),
|
products.forEach((product: RecursivePartial<ProductEdge>) => {
|
||||||
|
if (product.node) setProductLocaleMeta(product.node)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { products }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getAllProducts
|
export default getAllProducts
|
||||||
|
@ -3,20 +3,15 @@ import type {
|
|||||||
GetProductQueryVariables,
|
GetProductQueryVariables,
|
||||||
} from 'lib/bigcommerce/schema'
|
} from 'lib/bigcommerce/schema'
|
||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||||
|
import setProductLocaleMeta from '../utils/set-product-locale-meta'
|
||||||
import { productInfoFragment } from '../fragments/product'
|
import { productInfoFragment } from '../fragments/product'
|
||||||
import { BigcommerceConfig, getConfig, Images } from '..'
|
import { BigcommerceConfig, getConfig } from '..'
|
||||||
|
|
||||||
export const getProductQuery = /* GraphQL */ `
|
export const getProductQuery = /* GraphQL */ `
|
||||||
query getProduct(
|
query getProduct(
|
||||||
|
$hasLocale: Boolean = false
|
||||||
|
$locale: String = "null"
|
||||||
$path: String!
|
$path: String!
|
||||||
$imgSmallWidth: Int = 320
|
|
||||||
$imgSmallHeight: Int
|
|
||||||
$imgMediumWidth: Int = 640
|
|
||||||
$imgMediumHeight: Int
|
|
||||||
$imgLargeWidth: Int = 960
|
|
||||||
$imgLargeHeight: Int
|
|
||||||
$imgXLWidth: Int = 1280
|
|
||||||
$imgXLHeight: Int
|
|
||||||
) {
|
) {
|
||||||
site {
|
site {
|
||||||
route(path: $path) {
|
route(path: $path) {
|
||||||
@ -42,8 +37,10 @@ export type GetProductResult<
|
|||||||
T extends { product?: any } = { product?: ProductNode }
|
T extends { product?: any } = { product?: ProductNode }
|
||||||
> = T
|
> = T
|
||||||
|
|
||||||
export type ProductVariables = Images &
|
export type ProductVariables = { locale?: string } & (
|
||||||
({ path: string; slug?: never } | { path?: never; slug: string })
|
| { path: string; slug?: never }
|
||||||
|
| { path?: never; slug: string }
|
||||||
|
)
|
||||||
|
|
||||||
async function getProduct(opts: {
|
async function getProduct(opts: {
|
||||||
variables: ProductVariables
|
variables: ProductVariables
|
||||||
@ -66,9 +63,12 @@ async function getProduct({
|
|||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
}): Promise<GetProductResult> {
|
}): Promise<GetProductResult> {
|
||||||
config = getConfig(config)
|
config = getConfig(config)
|
||||||
|
|
||||||
|
const locale = vars.locale || config.locale
|
||||||
const variables: GetProductQueryVariables = {
|
const variables: GetProductQueryVariables = {
|
||||||
...config.imageVariables,
|
|
||||||
...vars,
|
...vars,
|
||||||
|
locale,
|
||||||
|
hasLocale: !!locale,
|
||||||
path: slug ? `/${slug}/` : vars.path!,
|
path: slug ? `/${slug}/` : vars.path!,
|
||||||
}
|
}
|
||||||
const { data } = await config.fetch<RecursivePartial<GetProductQuery>>(
|
const { data } = await config.fetch<RecursivePartial<GetProductQuery>>(
|
||||||
@ -78,6 +78,10 @@ async function getProduct({
|
|||||||
const product = data.site?.route?.node
|
const product = data.site?.route?.node
|
||||||
|
|
||||||
if (product?.__typename === 'Product') {
|
if (product?.__typename === 'Product') {
|
||||||
|
if (locale && config.applyLocale) {
|
||||||
|
setProductLocaleMeta(product)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
product: product as RecursiveRequired<typeof product>,
|
product: product as RecursiveRequired<typeof product>,
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import type { Response } from '@vercel/fetch'
|
||||||
|
|
||||||
// Used for GraphQL errors
|
// Used for GraphQL errors
|
||||||
export class BigcommerceGraphQLError extends Error {}
|
export class BigcommerceGraphQLError extends Error {}
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { FetcherError } from '@lib/commerce/utils/errors'
|
import { FetcherError } from '@lib/commerce/utils/errors'
|
||||||
import type { GraphQLFetcher } from 'lib/commerce/api'
|
import type { GraphQLFetcher } from '@lib/commerce/api'
|
||||||
import { getConfig } from '..'
|
import { getConfig } from '..'
|
||||||
import log from '@lib/logger'
|
import log from '@lib/logger'
|
||||||
|
import fetch from './fetch'
|
||||||
|
|
||||||
const fetchGraphqlApi: GraphQLFetcher = async (
|
const fetchGraphqlApi: GraphQLFetcher = async (
|
||||||
query: string,
|
query: string,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
import type { RequestInit, Response } from '@vercel/fetch'
|
||||||
import { getConfig } from '..'
|
import { getConfig } from '..'
|
||||||
import { BigcommerceApiError, BigcommerceNetworkError } from './errors'
|
import { BigcommerceApiError, BigcommerceNetworkError } from './errors'
|
||||||
|
import fetch from './fetch'
|
||||||
|
|
||||||
export default async function fetchStoreApi<T>(
|
export default async function fetchStoreApi<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
|
3
lib/bigcommerce/api/utils/fetch.ts
Normal file
3
lib/bigcommerce/api/utils/fetch.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import zeitFetch from '@vercel/fetch'
|
||||||
|
|
||||||
|
export default zeitFetch()
|
21
lib/bigcommerce/api/utils/set-product-locale-meta.ts
Normal file
21
lib/bigcommerce/api/utils/set-product-locale-meta.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { ProductNode } from '../operations/get-all-products'
|
||||||
|
import type { RecursivePartial } from './types'
|
||||||
|
|
||||||
|
export default function setProductLocaleMeta(
|
||||||
|
node: RecursivePartial<ProductNode>
|
||||||
|
) {
|
||||||
|
if (node.localeMeta?.edges) {
|
||||||
|
node.localeMeta.edges = node.localeMeta.edges.filter((edge) => {
|
||||||
|
const { key, value } = edge?.node ?? {}
|
||||||
|
if (key && key in node) {
|
||||||
|
;(node as any)[key] = value
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!node.localeMeta.edges.length) {
|
||||||
|
delete node.localeMeta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
57
lib/bigcommerce/schema.d.ts
vendored
57
lib/bigcommerce/schema.d.ts
vendored
@ -1684,16 +1684,6 @@ export type CategoryTreeItemFragment = {
|
|||||||
'entityId' | 'name' | 'path' | 'description' | 'productCount'
|
'entityId' | 'name' | 'path' | 'description' | 'productCount'
|
||||||
>
|
>
|
||||||
|
|
||||||
export type ResponsiveImageFragment = { __typename?: 'Image' } & Pick<
|
|
||||||
Image,
|
|
||||||
'urlOriginal' | 'altText' | 'isDefault'
|
|
||||||
> & {
|
|
||||||
urlSmall: Image['url']
|
|
||||||
urlMedium: Image['url']
|
|
||||||
urlLarge: Image['url']
|
|
||||||
urlXL: Image['url']
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SwatchOptionFragment = { __typename?: 'SwatchOptionValue' } & Pick<
|
export type SwatchOptionFragment = { __typename?: 'SwatchOptionValue' } & Pick<
|
||||||
SwatchOptionValue,
|
SwatchOptionValue,
|
||||||
'isDefault' | 'hexColors'
|
'isDefault' | 'hexColors'
|
||||||
@ -1739,6 +1729,9 @@ export type ProductInfoFragment = { __typename?: 'Product' } & Pick<
|
|||||||
salePrice?: Maybe<
|
salePrice?: Maybe<
|
||||||
{ __typename?: 'Money' } & Pick<Money, 'value' | 'currencyCode'>
|
{ __typename?: 'Money' } & Pick<Money, 'value' | 'currencyCode'>
|
||||||
>
|
>
|
||||||
|
retailPrice?: Maybe<
|
||||||
|
{ __typename?: 'Money' } & Pick<Money, 'value' | 'currencyCode'>
|
||||||
|
>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
images: { __typename?: 'ImageConnection' } & {
|
images: { __typename?: 'ImageConnection' } & {
|
||||||
@ -1746,7 +1739,10 @@ export type ProductInfoFragment = { __typename?: 'Product' } & Pick<
|
|||||||
Array<
|
Array<
|
||||||
Maybe<
|
Maybe<
|
||||||
{ __typename?: 'ImageEdge' } & {
|
{ __typename?: 'ImageEdge' } & {
|
||||||
node: { __typename?: 'Image' } & ResponsiveImageFragment
|
node: { __typename?: 'Image' } & Pick<
|
||||||
|
Image,
|
||||||
|
'urlOriginal' | 'altText' | 'isDefault'
|
||||||
|
>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
>
|
>
|
||||||
@ -1759,7 +1755,10 @@ export type ProductInfoFragment = { __typename?: 'Product' } & Pick<
|
|||||||
{ __typename?: 'VariantEdge' } & {
|
{ __typename?: 'VariantEdge' } & {
|
||||||
node: { __typename?: 'Variant' } & Pick<Variant, 'entityId'> & {
|
node: { __typename?: 'Variant' } & Pick<Variant, 'entityId'> & {
|
||||||
defaultImage?: Maybe<
|
defaultImage?: Maybe<
|
||||||
{ __typename?: 'Image' } & ResponsiveImageFragment
|
{ __typename?: 'Image' } & Pick<
|
||||||
|
Image,
|
||||||
|
'urlOriginal' | 'altText' | 'isDefault'
|
||||||
|
>
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1807,6 +1806,20 @@ export type ProductInfoFragment = { __typename?: 'Product' } & Pick<
|
|||||||
>
|
>
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
localeMeta: { __typename?: 'MetafieldConnection' } & {
|
||||||
|
edges?: Maybe<
|
||||||
|
Array<
|
||||||
|
Maybe<
|
||||||
|
{ __typename?: 'MetafieldEdge' } & {
|
||||||
|
node: { __typename?: 'Metafields' } & Pick<
|
||||||
|
Metafields,
|
||||||
|
'key' | 'value'
|
||||||
|
>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProductConnnectionFragment = {
|
export type ProductConnnectionFragment = {
|
||||||
@ -1848,16 +1861,10 @@ export type GetAllProductPathsQuery = { __typename?: 'Query' } & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type GetAllProductsQueryVariables = Exact<{
|
export type GetAllProductsQueryVariables = Exact<{
|
||||||
|
hasLocale?: Maybe<Scalars['Boolean']>
|
||||||
|
locale?: Maybe<Scalars['String']>
|
||||||
entityIds?: Maybe<Array<Scalars['Int']>>
|
entityIds?: Maybe<Array<Scalars['Int']>>
|
||||||
first?: Maybe<Scalars['Int']>
|
first?: Maybe<Scalars['Int']>
|
||||||
imgSmallWidth?: Maybe<Scalars['Int']>
|
|
||||||
imgSmallHeight?: Maybe<Scalars['Int']>
|
|
||||||
imgMediumWidth?: Maybe<Scalars['Int']>
|
|
||||||
imgMediumHeight?: Maybe<Scalars['Int']>
|
|
||||||
imgLargeWidth?: Maybe<Scalars['Int']>
|
|
||||||
imgLargeHeight?: Maybe<Scalars['Int']>
|
|
||||||
imgXLWidth?: Maybe<Scalars['Int']>
|
|
||||||
imgXLHeight?: Maybe<Scalars['Int']>
|
|
||||||
products?: Maybe<Scalars['Boolean']>
|
products?: Maybe<Scalars['Boolean']>
|
||||||
featuredProducts?: Maybe<Scalars['Boolean']>
|
featuredProducts?: Maybe<Scalars['Boolean']>
|
||||||
bestSellingProducts?: Maybe<Scalars['Boolean']>
|
bestSellingProducts?: Maybe<Scalars['Boolean']>
|
||||||
@ -1880,15 +1887,9 @@ export type GetAllProductsQuery = { __typename?: 'Query' } & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type GetProductQueryVariables = Exact<{
|
export type GetProductQueryVariables = Exact<{
|
||||||
|
hasLocale?: Maybe<Scalars['Boolean']>
|
||||||
|
locale?: Maybe<Scalars['String']>
|
||||||
path: Scalars['String']
|
path: Scalars['String']
|
||||||
imgSmallWidth?: Maybe<Scalars['Int']>
|
|
||||||
imgSmallHeight?: Maybe<Scalars['Int']>
|
|
||||||
imgMediumWidth?: Maybe<Scalars['Int']>
|
|
||||||
imgMediumHeight?: Maybe<Scalars['Int']>
|
|
||||||
imgLargeWidth?: Maybe<Scalars['Int']>
|
|
||||||
imgLargeHeight?: Maybe<Scalars['Int']>
|
|
||||||
imgXLWidth?: Maybe<Scalars['Int']>
|
|
||||||
imgXLHeight?: Maybe<Scalars['Int']>
|
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export type GetProductQuery = { __typename?: 'Query' } & {
|
export type GetProductQuery = { __typename?: 'Query' } & {
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
import type { RequestInit, Response } from '@vercel/fetch'
|
||||||
|
|
||||||
export interface CommerceAPIConfig {
|
export interface CommerceAPIConfig {
|
||||||
|
locale?: string
|
||||||
commerceUrl: string
|
commerceUrl: string
|
||||||
apiToken: string
|
apiToken: string
|
||||||
cartCookie: string
|
cartCookie: string
|
||||||
|
@ -3,6 +3,12 @@ module.exports = {
|
|||||||
sizes: [320, 480, 820, 1200, 1600],
|
sizes: [320, 480, 820, 1200, 1600],
|
||||||
domains: ['cdn11.bigcommerce.com'],
|
domains: ['cdn11.bigcommerce.com'],
|
||||||
},
|
},
|
||||||
|
experimental: {
|
||||||
|
i18n: {
|
||||||
|
locales: ['en-US', 'es'],
|
||||||
|
defaultLocale: 'en-US',
|
||||||
|
},
|
||||||
|
},
|
||||||
rewrites() {
|
rewrites() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
15
package.json
15
package.json
@ -22,29 +22,26 @@
|
|||||||
"@headlessui/react": "^0.2.0",
|
"@headlessui/react": "^0.2.0",
|
||||||
"@react-aria/overlays": "^3.4.0",
|
"@react-aria/overlays": "^3.4.0",
|
||||||
"@tailwindcss/ui": "^0.6.2",
|
"@tailwindcss/ui": "^0.6.2",
|
||||||
"animate.css": "^4.1.1",
|
"@vercel/fetch": "^6.1.0",
|
||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
"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",
|
||||||
|
"email-validator": "^2.0.4",
|
||||||
|
"intersection-observer": "^0.11.0",
|
||||||
"js-cookie": "^2.2.1",
|
"js-cookie": "^2.2.1",
|
||||||
"keen-slider": "^5.2.4",
|
"keen-slider": "^5.2.4",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.random": "^3.2.0",
|
"lodash.random": "^3.2.0",
|
||||||
"next": "^9.5.6-canary.12",
|
"next": "^9.5.6-canary.14",
|
||||||
"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",
|
|
||||||
"postcss-import": "^13.0.0",
|
"postcss-import": "^13.0.0",
|
||||||
"postcss-nesting": "^7.0.1",
|
"postcss-nesting": "^7.0.1",
|
||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
"react-aria": "^3.0.0",
|
"react-aria": "^3.0.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"react-icons": "^3.11.0",
|
"react-intersection-observer": "^8.29.1",
|
||||||
"react-merge-refs": "^1.1.0",
|
"react-merge-refs": "^1.1.0",
|
||||||
"react-swipeable-views": "^0.13.9",
|
|
||||||
"react-swipeable-views-utils": "^0.14.0-alpha.0",
|
|
||||||
"react-ticker": "^1.2.2",
|
"react-ticker": "^1.2.2",
|
||||||
"swr": "^0.3.3",
|
"swr": "^0.3.3",
|
||||||
"tailwindcss": "^1.9"
|
"tailwindcss": "^1.9"
|
||||||
@ -65,6 +62,8 @@
|
|||||||
"@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",
|
"@types/react-swipeable-views": "^0.13.0",
|
||||||
|
"bunyan": "^1.8.14",
|
||||||
|
"bunyan-prettystream": "^0.1.3",
|
||||||
"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",
|
||||||
|
@ -7,19 +7,24 @@ import { Layout, HTMLContent } from '@components/core'
|
|||||||
export async function getStaticProps({
|
export async function getStaticProps({
|
||||||
preview,
|
preview,
|
||||||
params,
|
params,
|
||||||
|
locale,
|
||||||
}: GetStaticPropsContext<{ pages: string[] }>) {
|
}: GetStaticPropsContext<{ pages: string[] }>) {
|
||||||
const { pages } = await getAllPages()
|
const { pages } = await getAllPages()
|
||||||
const slug = params?.pages.join('/')
|
const path = params?.pages.join('/')
|
||||||
|
const slug = locale ? `${locale}/${path}` : path
|
||||||
|
|
||||||
const pageItem = pages.find((p) => (p.url ? getSlug(p.url) === slug : false))
|
const pageItem = pages.find((p) => (p.url ? getSlug(p.url) === slug : false))
|
||||||
const data = pageItem && (await getPage({ variables: { id: pageItem.id! } }))
|
const data = pageItem && (await getPage({ variables: { id: pageItem.id! } }))
|
||||||
const page = data?.page
|
const page = data?.page
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
|
// We throw to make sure this fails at build time as this is never expected to happen
|
||||||
throw new Error(`Page with slug '${slug}' not found`)
|
throw new Error(`Page with slug '${slug}' not found`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { pages, page },
|
props: { pages, page },
|
||||||
|
revalidate: 60 * 60, // Every hour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import '@assets/main.css'
|
import '@assets/main.css'
|
||||||
import 'keen-slider/keen-slider.min.css'
|
import 'keen-slider/keen-slider.min.css'
|
||||||
|
|
||||||
// To be removed
|
|
||||||
import 'animate.css'
|
|
||||||
|
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import type { AppProps } from 'next/app'
|
import type { AppProps } from 'next/app'
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ export default function Blog({}: InferGetStaticPropsType<
|
|||||||
<img
|
<img
|
||||||
className="h-12 w-12 rounded-full"
|
className="h-12 w-12 rounded-full"
|
||||||
src="https://vercel.com/api/www/avatar/61182a9f6bda512b4d9263c9c8a60aabe0402f4c?s=204"
|
src="https://vercel.com/api/www/avatar/61182a9f6bda512b4d9263c9c8a60aabe0402f4c?s=204"
|
||||||
alt=""
|
alt="Avatar"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
@ -51,7 +51,7 @@ export default function Blog({}: InferGetStaticPropsType<
|
|||||||
</div>
|
</div>
|
||||||
<Container>
|
<Container>
|
||||||
<div className="-mt-96 mx-auto">
|
<div className="-mt-96 mx-auto">
|
||||||
<img src="/jacket.png" />
|
<img src="/jacket.png" alt="Jacket" />
|
||||||
</div>
|
</div>
|
||||||
{/** Replace by HTML Content */}
|
{/** Replace by HTML Content */}
|
||||||
<div className="text-lg leading-7 font-medium py-6 text-justify max-w-6xl mx-auto">
|
<div className="text-lg leading-7 font-medium py-6 text-justify max-w-6xl mx-auto">
|
||||||
|
127
pages/cart.tsx
127
pages/cart.tsx
@ -1,7 +1,12 @@
|
|||||||
import { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
import { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
||||||
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
||||||
import { Layout } from '@components/core'
|
import { Layout } from '@components/core'
|
||||||
import { Container } from '@components/ui'
|
import { Button } from '@components/ui'
|
||||||
|
import { Bag, Cross, Check } from '@components/icons'
|
||||||
|
import useCart from '@lib/bigcommerce/cart/use-cart'
|
||||||
|
import usePrice from '@lib/bigcommerce/use-price'
|
||||||
|
import { CartItem } from '@components/cart'
|
||||||
|
import { Text } from '@components/ui'
|
||||||
|
|
||||||
export async function getStaticProps({ preview }: GetStaticPropsContext) {
|
export async function getStaticProps({ preview }: GetStaticPropsContext) {
|
||||||
const { pages } = await getAllPages()
|
const { pages } = await getAllPages()
|
||||||
@ -10,16 +15,122 @@ export async function getStaticProps({ preview }: GetStaticPropsContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home({}: InferGetStaticPropsType<
|
export default function Cart({}: InferGetStaticPropsType<
|
||||||
typeof getStaticProps
|
typeof getStaticProps
|
||||||
>) {
|
>) {
|
||||||
|
const { data, isEmpty } = useCart()
|
||||||
|
const { price: subTotal } = usePrice(
|
||||||
|
data && {
|
||||||
|
amount: data.base_amount,
|
||||||
|
currencyCode: data.currency.code,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const { price: total } = usePrice(
|
||||||
|
data && {
|
||||||
|
amount: data.cart_amount,
|
||||||
|
currencyCode: data.currency.code,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const items = data?.line_items.physical_items ?? []
|
||||||
|
|
||||||
|
const error = null
|
||||||
|
const success = null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<div className="grid lg:grid-cols-12">
|
||||||
<h2 className="pt-1 pb-4 text-2xl leading-7 font-bold text-base tracking-wide">
|
<div className="lg:col-span-8">
|
||||||
My Cart
|
{isEmpty ? (
|
||||||
</h2>
|
<div className="flex-1 px-4 flex flex-col justify-center items-center ">
|
||||||
</Container>
|
<span className="border border-dashed border-white rounded-full flex items-center justify-center w-16 h-16 bg-black p-12 rounded-lg text-white">
|
||||||
|
<Bag className="absolute" />
|
||||||
|
</span>
|
||||||
|
<h2 className="pt-6 text-2xl font-bold tracking-wide text-center">
|
||||||
|
Your cart is empty
|
||||||
|
</h2>
|
||||||
|
<p className="text-accents-2 px-10 text-center pt-2">
|
||||||
|
Biscuit oat cake wafer icing ice cream tiramisu pudding cupcake.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="flex-1 px-4 flex flex-col justify-center items-center">
|
||||||
|
<span className="border border-white rounded-full flex items-center justify-center w-16 h-16">
|
||||||
|
<Cross width={24} height={24} />
|
||||||
|
</span>
|
||||||
|
<h2 className="pt-6 text-xl font-light text-center">
|
||||||
|
We couldn’t process the purchase. Please check your card
|
||||||
|
information and try again.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
) : success ? (
|
||||||
|
<div className="flex-1 px-4 flex flex-col justify-center items-center">
|
||||||
|
<span className="border border-white rounded-full flex items-center justify-center w-16 h-16">
|
||||||
|
<Check />
|
||||||
|
</span>
|
||||||
|
<h2 className="pt-6 text-xl font-light text-center">
|
||||||
|
Thank you for your order.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="px-4 sm:px-6 flex-1">
|
||||||
|
<Text variant="pageHeading">My Cart</Text>
|
||||||
|
<Text variant="sectionHeading">Review your Order</Text>
|
||||||
|
<ul className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accents-2 border-b border-accents-2">
|
||||||
|
{items.map((item) => (
|
||||||
|
<CartItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
currencyCode={data?.currency.code!}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="my-6">
|
||||||
|
<Text>
|
||||||
|
Before you leave, take a look at these items. We picked them
|
||||||
|
just for you
|
||||||
|
</Text>
|
||||||
|
<div className="flex py-6 space-x-6">
|
||||||
|
{[1, 2, 3, 4, 5, 6].map((x) => (
|
||||||
|
<div className="border border-accents-3 w-full h-24 bg-accents-2 bg-opacity-50 transform cursor-pointer hover:scale-110 duration-75" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="lg:col-span-4">
|
||||||
|
<div className="flex-shrink-0 px-4 py-24 sm:px-6">
|
||||||
|
<div className="border-t border-accents-2">
|
||||||
|
<ul className="py-3">
|
||||||
|
<li className="flex justify-between py-1">
|
||||||
|
<span>Subtotal</span>
|
||||||
|
<span>{subTotal}</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex justify-between py-1">
|
||||||
|
<span>Taxes</span>
|
||||||
|
<span>Calculated at checkout</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex justify-between py-1">
|
||||||
|
<span>Estimated Shipping</span>
|
||||||
|
<span className="font-bold tracking-wide">FREE</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div className="flex justify-between border-t border-accents-2 py-3 font-bold mb-10">
|
||||||
|
<span>Total</span>
|
||||||
|
<span>{total}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-end">
|
||||||
|
<div className="w-full lg:w-72">
|
||||||
|
<Button href="/checkout" Component="a" width="100%">
|
||||||
|
Confirm Purchase
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Home.Layout = Layout
|
Cart.Layout = Layout
|
||||||
|
@ -1,25 +1,36 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
import { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
||||||
|
import { getConfig } from '@lib/bigcommerce/api'
|
||||||
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 getSiteInfo from '@lib/bigcommerce/api/operations/get-site-info'
|
||||||
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
||||||
import rangeMap from '@lib/range-map'
|
import rangeMap from '@lib/range-map'
|
||||||
|
import { getCategoryPath, getDesignerPath } from '@utils/search'
|
||||||
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 Link from 'next/link'
|
||||||
|
|
||||||
|
export async function getStaticProps({
|
||||||
|
preview,
|
||||||
|
locale,
|
||||||
|
}: GetStaticPropsContext) {
|
||||||
|
const config = getConfig({ locale })
|
||||||
|
|
||||||
export async function getStaticProps({ preview }: GetStaticPropsContext) {
|
|
||||||
const { products: featuredProducts } = await getAllProducts({
|
const { products: featuredProducts } = await getAllProducts({
|
||||||
variables: { field: 'featuredProducts', first: 6 },
|
variables: { field: 'featuredProducts', first: 6 },
|
||||||
|
config,
|
||||||
})
|
})
|
||||||
const { products: bestSellingProducts } = await getAllProducts({
|
const { products: bestSellingProducts } = await getAllProducts({
|
||||||
variables: { field: 'bestSellingProducts', first: 6 },
|
variables: { field: 'bestSellingProducts', first: 6 },
|
||||||
|
config,
|
||||||
})
|
})
|
||||||
const { products: newestProducts } = await getAllProducts({
|
const { products: newestProducts } = await getAllProducts({
|
||||||
variables: { field: 'newestProducts', first: 12 },
|
variables: { field: 'newestProducts', first: 12 },
|
||||||
|
config,
|
||||||
})
|
})
|
||||||
const { categories, brands } = await getSiteInfo()
|
const { categories, brands } = await getSiteInfo({ config })
|
||||||
const { pages } = await getAllPages()
|
const { pages } = await getAllPages({ config })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
@ -59,8 +70,7 @@ export default function Home({
|
|||||||
(i) => bestSellingProducts[i] ?? products.shift()
|
(i) => bestSellingProducts[i] ?? products.shift()
|
||||||
).filter(nonNullable),
|
).filter(nonNullable),
|
||||||
}
|
}
|
||||||
// Props from getStaticProps won't change
|
}, [newestProducts, featuredProducts, bestSellingProducts])
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -124,21 +134,29 @@ export default function Home({
|
|||||||
<div className="sticky top-32">
|
<div className="sticky top-32">
|
||||||
<ul className="mb-10">
|
<ul className="mb-10">
|
||||||
<li className="py-1 text-base font-bold tracking-wide">
|
<li className="py-1 text-base font-bold tracking-wide">
|
||||||
All Categories
|
<Link href={getCategoryPath('')}>
|
||||||
|
<a>All Categories</a>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{categories.map((cat) => (
|
{categories.map((cat) => (
|
||||||
<li key={cat.path} className="py-1 text-accents-8">
|
<li key={cat.path} className="py-1 text-accents-8">
|
||||||
<a href="#">{cat.name}</a>
|
<Link href={getCategoryPath(cat.path)}>
|
||||||
|
<a>{cat.name}</a>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<ul className="">
|
<ul className="">
|
||||||
<li className="py-1 text-base font-bold tracking-wide">
|
<li className="py-1 text-base font-bold tracking-wide">
|
||||||
All Designers
|
<Link href={getDesignerPath('')}>
|
||||||
|
<a>All Designers</a>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{brands.flatMap(({ node }) => (
|
{brands.flatMap(({ node }) => (
|
||||||
<li key={node.path} className="py-1 text-accents-8">
|
<li key={node.path} className="py-1 text-accents-8">
|
||||||
<a href="#">{node.name}</a>
|
<Link href={getDesignerPath(node.path)}>
|
||||||
|
<a>{node.name}</a>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,91 +0,0 @@
|
|||||||
import useSignup from '@lib/bigcommerce/use-signup'
|
|
||||||
import { Layout } from '@components/core'
|
|
||||||
import { Logo, Modal, Button } from '@components/ui'
|
|
||||||
import useLogin from '@lib/bigcommerce/use-login'
|
|
||||||
import useLogout from '@lib/bigcommerce/use-logout'
|
|
||||||
import useCustomer from '@lib/bigcommerce/use-customer'
|
|
||||||
|
|
||||||
export default function Login() {
|
|
||||||
const signup = useSignup()
|
|
||||||
const login = useLogin()
|
|
||||||
const logout = useLogout()
|
|
||||||
// Data about the currently logged in customer, it will update
|
|
||||||
// automatically after a signup/login/logout
|
|
||||||
const { data } = useCustomer()
|
|
||||||
// TODO: use this method. It can take more than 5 seconds to do a signup
|
|
||||||
const handleSignup = async () => {
|
|
||||||
// TODO: validate the password and email before calling the signup
|
|
||||||
// Passwords must be at least 7 characters and contain both alphabetic
|
|
||||||
// and numeric characters.
|
|
||||||
try {
|
|
||||||
await signup({
|
|
||||||
// This account already exists, so it will throw the "duplicated_email" error
|
|
||||||
email: 'luis@vercel.com',
|
|
||||||
firstName: 'Luis',
|
|
||||||
lastName: 'Alvarez',
|
|
||||||
password: 'luis123',
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'duplicated_email') {
|
|
||||||
// TODO: handle duplicated email
|
|
||||||
}
|
|
||||||
// Show a generic error saying that something bad happened, try again later
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
|
||||||
// TODO: validate the password and email before calling the signup
|
|
||||||
// Passwords must be at least 7 characters and contain both alphabetic
|
|
||||||
// and numeric characters.
|
|
||||||
try {
|
|
||||||
await login({
|
|
||||||
email: 'luis@vercel.com',
|
|
||||||
// This is an invalid password so it will throw the "invalid_credentials" error
|
|
||||||
password: 'luis1234', // will work with `luis123`
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'invalid_credentials') {
|
|
||||||
// The email and password didn't match an existing account
|
|
||||||
}
|
|
||||||
// Show a generic error saying that something bad happened, try again later
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 bg-primary 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="bg-primary 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" onClick={handleSignup}>
|
|
||||||
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
|
|
12
pages/orders.tsx
Normal file
12
pages/orders.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Layout } from '@components/core'
|
||||||
|
import { Container, Text } from '@components/ui'
|
||||||
|
|
||||||
|
export default function Orders() {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Text variant="pageHeading">My Orders</Text>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Orders.Layout = Layout
|
@ -1,5 +1,10 @@
|
|||||||
import { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
import {
|
||||||
|
GetStaticPathsContext,
|
||||||
|
GetStaticPropsContext,
|
||||||
|
InferGetStaticPropsType,
|
||||||
|
} from 'next'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
import { getConfig } from '@lib/bigcommerce/api'
|
||||||
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
||||||
import getProduct from '@lib/bigcommerce/api/operations/get-product'
|
import getProduct from '@lib/bigcommerce/api/operations/get-product'
|
||||||
import { Layout } from '@components/core'
|
import { Layout } from '@components/core'
|
||||||
@ -8,9 +13,15 @@ import getAllProductPaths from '@lib/bigcommerce/api/operations/get-all-product-
|
|||||||
|
|
||||||
export async function getStaticProps({
|
export async function getStaticProps({
|
||||||
params,
|
params,
|
||||||
|
locale,
|
||||||
}: GetStaticPropsContext<{ slug: string }>) {
|
}: GetStaticPropsContext<{ slug: string }>) {
|
||||||
const { pages } = await getAllPages()
|
const config = getConfig({ locale })
|
||||||
const { product } = await getProduct({ variables: { slug: params!.slug } })
|
|
||||||
|
const { pages } = await getAllPages({ config })
|
||||||
|
const { product } = await getProduct({
|
||||||
|
variables: { slug: params!.slug },
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
throw new Error(`Product with slug '${params!.slug}' not found`)
|
throw new Error(`Product with slug '${params!.slug}' not found`)
|
||||||
@ -22,11 +33,19 @@ export async function getStaticProps({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths({ locales }: GetStaticPathsContext) {
|
||||||
const { products } = await getAllProductPaths()
|
const { products } = await getAllProductPaths()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paths: products.map((product) => `/product${product.node.path}`),
|
paths: locales
|
||||||
|
? locales.reduce<string[]>((arr, locale) => {
|
||||||
|
// Add a product path for every locale
|
||||||
|
products.forEach((product) => {
|
||||||
|
arr.push(`/${locale}/product${product.node.path}`)
|
||||||
|
})
|
||||||
|
return arr
|
||||||
|
}, [])
|
||||||
|
: products.map((product) => `/product${product.node.path}`),
|
||||||
// If your store has tons of products, enable fallback mode to improve build times!
|
// If your store has tons of products, enable fallback mode to improve build times!
|
||||||
fallback: false,
|
fallback: false,
|
||||||
}
|
}
|
||||||
|
26
pages/profile.tsx
Normal file
26
pages/profile.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Layout } from '@components/core'
|
||||||
|
import { Container, Text } from '@components/ui'
|
||||||
|
import useCustomer from '@lib/bigcommerce/use-customer'
|
||||||
|
export default function Profile() {
|
||||||
|
const { data } = useCustomer()
|
||||||
|
console.log(data)
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Text variant="pageHeading">My Profile</Text>
|
||||||
|
<div className="max-w-2xl flex flex-col space-y-5">
|
||||||
|
<div>
|
||||||
|
<Text variant="sectionHeading">Full Name</Text>
|
||||||
|
<span>
|
||||||
|
{data.firstName} {data.lastName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text variant="sectionHeading">Email</Text>
|
||||||
|
<span>{data.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile.Layout = Layout
|
@ -40,7 +40,10 @@ export default function Search({
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { asPath } = router
|
const { asPath } = router
|
||||||
const { q, sort } = router.query
|
const { q, sort } = router.query
|
||||||
const query = filterQuery({ q, sort })
|
// `q` can be included but because categories and designers can't be searched
|
||||||
|
// in the same way of products, it's better to ignore the search input if one
|
||||||
|
// of those is selected
|
||||||
|
const query = filterQuery({ sort })
|
||||||
|
|
||||||
const { pathname, category, brand } = useSearchMeta(asPath)
|
const { pathname, category, brand } = useSearchMeta(asPath)
|
||||||
const activeCategory = categories.find(
|
const activeCategory = categories.find(
|
||||||
@ -76,7 +79,7 @@ export default function Search({
|
|||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={{
|
href={{
|
||||||
pathname: getCategoryPath(getSlug(cat.path), brand),
|
pathname: getCategoryPath(cat.path, brand),
|
||||||
query,
|
query,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -100,7 +103,7 @@ export default function Search({
|
|||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={{
|
href={{
|
||||||
pathname: getDesignerPath(getSlug(node.path), category),
|
pathname: getDesignerPath(node.path, category),
|
||||||
query,
|
query,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -111,33 +114,50 @@ export default function Search({
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-8">
|
<div className="col-span-8">
|
||||||
<div className="mb-12 animate__animated animate__fadeIn">
|
{(q || activeCategory || activeBrand) && (
|
||||||
{data ? (
|
<div className="mb-12 transition ease-in duration-75">
|
||||||
<>
|
{data ? (
|
||||||
<span
|
<>
|
||||||
className={cn('animate__animated', {
|
<span
|
||||||
animate__fadeIn: data.found,
|
className={cn('animated', {
|
||||||
hidden: !data.found,
|
fadeIn: data.found,
|
||||||
})}
|
hidden: !data.found,
|
||||||
>
|
})}
|
||||||
Showing {data.products.length} results for "
|
>
|
||||||
<strong>{q}</strong>"
|
Showing {data.products.length} results{' '}
|
||||||
</span>
|
{q && (
|
||||||
<span
|
<>
|
||||||
className={cn('animate__animated', {
|
for "<strong>{q}</strong>"
|
||||||
animate__fadeIn: !data.found,
|
</>
|
||||||
hidden: data.found,
|
)}
|
||||||
})}
|
</span>
|
||||||
>
|
<span
|
||||||
There are no products that match "<strong>{q}</strong>"
|
className={cn('animated', {
|
||||||
</span>
|
fadeIn: !data.found,
|
||||||
</>
|
hidden: data.found,
|
||||||
) : (
|
})}
|
||||||
<>
|
>
|
||||||
Searching for: "<strong>{q}</strong>"
|
{q ? (
|
||||||
</>
|
<>
|
||||||
)}
|
There are no products that match "<strong>{q}</strong>"
|
||||||
</div>
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
There are no products that match the selected category &
|
||||||
|
designer
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : q ? (
|
||||||
|
<>
|
||||||
|
Searching for: "<strong>{q}</strong>"
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>Searching...</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{data ? (
|
{data ? (
|
||||||
<Grid layout="normal">
|
<Grid layout="normal">
|
||||||
@ -145,7 +165,7 @@ export default function Search({
|
|||||||
<ProductCard
|
<ProductCard
|
||||||
variant="simple"
|
variant="simple"
|
||||||
key={node.path}
|
key={node.path}
|
||||||
className="animate__animated animate__fadeIn"
|
className="animated fadeIn"
|
||||||
product={node}
|
product={node}
|
||||||
imgWidth={480}
|
imgWidth={480}
|
||||||
imgHeight={480}
|
imgHeight={480}
|
||||||
@ -157,7 +177,7 @@ export default function Search({
|
|||||||
{rangeMap(12, (i) => (
|
{rangeMap(12, (i) => (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
key={i}
|
key={i}
|
||||||
className="w-full animate__animated animate__fadeIn"
|
className="w-full animated fadeIn"
|
||||||
height={325}
|
height={325}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
import { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
||||||
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
||||||
import { Layout } from '@components/core'
|
import { Layout } from '@components/core'
|
||||||
import { Container } from '@components/ui'
|
import { Container, Text } from '@components/ui'
|
||||||
import { WishlistCard } from '@components/wishlist'
|
import { WishlistCard } from '@components/wishlist'
|
||||||
|
|
||||||
import getSiteInfo from '@lib/bigcommerce/api/operations/get-site-info'
|
import getSiteInfo from '@lib/bigcommerce/api/operations/get-site-info'
|
||||||
@ -35,12 +35,10 @@ export default function Home({
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-8">
|
<div className="col-span-8">
|
||||||
<h2 className="pt-1 px-3 pb-4 text-2xl leading-7 font-bold text-base tracking-wide">
|
<Text variant="pageHeading">My Wishlist</Text>
|
||||||
My Wishlist
|
|
||||||
</h2>
|
|
||||||
<div className="group flex flex-col">
|
<div className="group flex flex-col">
|
||||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
<WishlistCard />
|
<WishlistCard key={i} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,12 @@ module.exports = {
|
|||||||
removeDeprecatedGapUtilities: true,
|
removeDeprecatedGapUtilities: true,
|
||||||
purgeLayersByDefault: true,
|
purgeLayersByDefault: true,
|
||||||
},
|
},
|
||||||
purge: ['./components/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}'],
|
purge: {
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{js,ts,jsx,tsx}',
|
||||||
|
'./components/**/*.{js,ts,jsx,tsx}',
|
||||||
|
],
|
||||||
|
},
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
maxWidth: {
|
maxWidth: {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import getSlug from './get-slug'
|
||||||
|
|
||||||
export function useSearchMeta(asPath: string) {
|
export function useSearchMeta(asPath: string) {
|
||||||
const [pathname, setPathname] = useState<string>('/search')
|
const [pathname, setPathname] = useState<string>('/search')
|
||||||
@ -34,11 +35,16 @@ export const filterQuery = (query: any) =>
|
|||||||
return obj
|
return obj
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
export const getCategoryPath = (slug: string, brand?: string) =>
|
export const getCategoryPath = (path: string, brand?: string) => {
|
||||||
`/search${brand ? `/designers/${brand}` : ''}${slug ? `/${slug}` : ''}`
|
const category = getSlug(path)
|
||||||
|
|
||||||
export const getDesignerPath = (slug: string, category?: string) => {
|
return `/search${brand ? `/designers/${brand}` : ''}${
|
||||||
const designer = slug.replace(/^brands/, 'designers')
|
category ? `/${category}` : ''
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDesignerPath = (path: string, category?: string) => {
|
||||||
|
const designer = getSlug(path).replace(/^brands/, 'designers')
|
||||||
|
|
||||||
return `/search${designer ? `/${designer}` : ''}${
|
return `/search${designer ? `/${designer}` : ''}${
|
||||||
category ? `/${category}` : ''
|
category ? `/${category}` : ''
|
||||||
|
Loading…
x
Reference in New Issue
Block a user