4
0
forked from crowetic/commerce

Merge branch 'master' into arzafran/ui-tweaks

This commit is contained in:
Franco Arza 2020-10-26 09:49:40 -03:00
commit dc6919e1da
94 changed files with 1857 additions and 839 deletions

View File

@ -1 +1 @@
# Commerce Example # Next.js Commerce

View File

@ -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;
}
}

View 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

View 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

View 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
View File

@ -0,0 +1,3 @@
export { default as LoginView } from './LoginView'
export { default as SignUpView } from './SignUpView'
export { default as ForgotPassword } from './ForgotPassword'

View File

@ -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;
} }

View File

@ -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} />

View File

@ -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'

View File

@ -1 +1,2 @@
export { default as CartSidebarView } from './CartSidebarView' export { default as CartSidebarView } from './CartSidebarView'
export { default as CartItem } from './CartItem'

View 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

View File

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

View File

@ -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>&copy; 2020 ACME, Inc. All rights reserved.</span> <span>&copy; 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)

View File

@ -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 {

View File

@ -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>
) )

View File

@ -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>
) )

View File

@ -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>
) )

View File

@ -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;
} }

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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'

View File

@ -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

View 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
View 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

View File

@ -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'

View File

@ -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}

View File

@ -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,

View File

@ -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;
} }
} }

View File

@ -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 */}

View File

@ -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 && (

View File

@ -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),

View File

@ -1 +0,0 @@
import { Colors } from '@components/ui/types'

View File

@ -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;
}

View File

@ -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,

View File

@ -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>

View 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;
}

View 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

View File

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

View File

@ -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}>

View File

@ -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;
}

View File

@ -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>
) )
} }

View 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;
}

View 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

View File

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

View 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;
}

View 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

View File

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

View File

@ -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} />
} }

View File

@ -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'

View File

@ -1 +0,0 @@
export type Colors = 'violet' | 'black' | 'pink' | 'white'

View File

@ -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 {

View File

@ -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, {})

View File

@ -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}
` `

View File

@ -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,

View File

@ -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

View File

@ -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>,
} }

View File

@ -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 {}

View File

@ -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,

View File

@ -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,

View File

@ -0,0 +1,3 @@
import zeitFetch from '@vercel/fetch'
export default zeitFetch()

View 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
}
}
}

View File

@ -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' } & {

View File

@ -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

View File

@ -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 [
{ {

View File

@ -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",

View File

@ -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
} }
} }

View File

@ -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'

View File

@ -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">

View File

@ -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 couldnt 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

View File

@ -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>

View File

@ -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
View 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

View File

@ -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
View 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

View File

@ -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}
/> />
))} ))}

View File

@ -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>

View File

@ -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: {

View File

@ -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}` : ''

642
yarn.lock

File diff suppressed because it is too large Load Diff