Merge branch 'master' into mobile-sorting-filtering

This commit is contained in:
B 2020-12-02 11:20:59 -03:00 committed by GitHub
commit 60d0a5c289
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1867 additions and 11750 deletions

23
.editorconfig Normal file
View File

@ -0,0 +1,23 @@
root = true
[*]
indent_style = space
indent_size = 2
tab_width = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[*.js]
quote_type = single
[{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}]
curly_bracket_next_line = false
spaces_around_operators = true
spaces_around_brackets = outside
# close enough to 1TB
indent_brace_style = K&R

View File

@ -17,7 +17,7 @@ This project is currently <b>under development</b>.
- Responsive - Responsive
- UI Components - UI Components
- Theming - Theming
- Standarized Data Hooks - Standardized Data Hooks
- Integrations - Integrate seamlessly with the most common ecommerce platforms. - Integrations - Integrate seamlessly with the most common ecommerce platforms.
- Dark Mode Support - Dark Mode Support
@ -66,6 +66,15 @@ After Email confirmation, Checkout should be manually enabled through BigCommerc
BigCommerce team has been notified and they plan to add more detailed about this subject. BigCommerce team has been notified and they plan to add more detailed about this subject.
</details> </details>
<details>
<summary>I have issues with BigCommerce data hooks</summary>
<br>
Report issue with Data Hooks here: https://github.com/bigcommerce/storefront-data-hooks
</details>
## Contribute ## Contribute
Our commitment to Open Source can be found [here](https://vercel.com/oss). Our commitment to Open Source can be found [here](https://vercel.com/oss).

View File

@ -97,15 +97,17 @@ const CartItem = ({
<button type="button" onClick={() => increaseQuantity(-1)}> <button type="button" onClick={() => increaseQuantity(-1)}>
<Minus width={18} height={18} /> <Minus width={18} height={18} />
</button> </button>
<input <label>
type="number" <input
max={99} type="number"
min={0} max={99}
className={s.quantity} min={0}
value={quantity} className={s.quantity}
onChange={handleQuantity} value={quantity}
onBlur={handleBlur} onChange={handleQuantity}
/> onBlur={handleBlur}
/>
</label>
<button type="button" onClick={() => increaseQuantity(1)}> <button type="button" onClick={() => increaseQuantity(1)}>
<Plus width={18} height={18} /> <Plus width={18} height={18} />
</button> </button>

View File

@ -10,6 +10,7 @@ import CartItem from '../CartItem'
import s from './CartSidebarView.module.css' import s from './CartSidebarView.module.css'
const CartSidebarView: FC = () => { const CartSidebarView: FC = () => {
const { closeSidebar } = useUI()
const { data, isEmpty } = useCart() const { data, isEmpty } = useCart()
const { price: subTotal } = usePrice( const { price: subTotal } = usePrice(
data && { data && {
@ -23,7 +24,6 @@ const CartSidebarView: FC = () => {
currencyCode: data.currency.code, currencyCode: data.currency.code,
} }
) )
const { closeSidebar } = useUI()
const handleClose = () => closeSidebar() const handleClose = () => closeSidebar()
const items = data?.line_items.physical_items ?? [] const items = data?.line_items.physical_items ?? []

View File

@ -1,5 +1,5 @@
import cn from 'classnames' import cn from 'classnames'
import { FC, useState } from 'react' import { FC, useState, useMemo, useRef, useEffect } from 'react'
import { getRandomPairOfColors } from '@lib/colors' import { getRandomPairOfColors } from '@lib/colors'
interface Props { interface Props {
@ -8,14 +8,19 @@ interface Props {
} }
const Avatar: FC<Props> = ({}) => { const Avatar: FC<Props> = ({}) => {
const [bg] = useState(getRandomPairOfColors) const [bg] = useState(useMemo(() => getRandomPairOfColors, []))
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
useEffect(() => {
if (ref && ref.current) {
ref.current.style.backgroundImage = `linear-gradient(140deg, ${bg[0]}, ${bg[1]} 100%)`
}
}, [bg])
return ( return (
<div <div
ref={ref}
className="inline-block h-8 w-8 rounded-full border-2 border-primary hover:border-secondary focus:border-secondary transition linear-out duration-150" className="inline-block h-8 w-8 rounded-full border-2 border-primary hover:border-secondary focus:border-secondary transition linear-out duration-150"
style={{
backgroundImage: `linear-gradient(140deg, ${bg[0]}, ${bg[1]} 100%)`,
}}
> >
{/* Add an image - We're generating a gradient as placeholder <img></img> */} {/* Add an image - We're generating a gradient as placeholder <img></img> */}
</div> </div>

View File

@ -84,7 +84,11 @@ const Footer: FC<Props> = ({ className, pages }) => {
</div> </div>
<div className="col-span-1 lg:col-span-6 flex items-start lg:justify-end text-primary"> <div className="col-span-1 lg:col-span-6 flex items-start lg:justify-end text-primary">
<div className="flex space-x-6 items-center h-10"> <div className="flex space-x-6 items-center h-10">
<a href="https://github.com/vercel/commerce" className={s.link}> <a
aria-label="Github Repository"
href="https://github.com/vercel/commerce"
className={s.link}
>
<Github /> <Github />
</a> </a>
<I18nWidget /> <I18nWidget />

View File

@ -1,26 +0,0 @@
.root {
@apply text-lg leading-7 font-medium max-w-6xl mx-auto;
}
.root p {
@apply text-justify;
}
.root h1 {
@apply text-5xl mb-12;
}
.root h2 {
@apply text-3xl mt-12 mb-4 leading-snug;
}
.root h3 {
@apply text-2xl mt-8 mb-4 leading-snug;
}
.root p,
.root ul,
.root ol,
.root blockquote {
@apply mb-6;
}

View File

@ -1,16 +0,0 @@
import cn from 'classnames'
import s from './HTMLContent.module.css'
type Props = {
className?: 'string'
html: string
}
export default function HTMLContent({ className, html }: Props) {
return (
<div
className={cn(s.root, className)}
dangerouslySetInnerHTML={{ __html: html }}
/>
)
}

View File

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

View File

@ -1,9 +1,9 @@
import { FC } from 'react' import { FC } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { getCategoryPath, getDesignerPath } from '@lib/search'
import { Grid } from '@components/ui' import { Grid } from '@components/ui'
import { ProductCard } from '@components/product' import { ProductCard } from '@components/product'
import s from './HomeAllProductsGrid.module.css' import s from './HomeAllProductsGrid.module.css'
import { getCategoryPath, getDesignerPath } from '@lib/search'
interface Props { interface Props {
categories?: any categories?: any

View File

@ -1,11 +1,9 @@
import { FC } from 'react'
import cn from 'classnames' import cn from 'classnames'
import { useRouter } from 'next/router'
import Link from 'next/link' import Link from 'next/link'
import { Menu } from '@headlessui/react' import { FC, useState } from 'react'
import { DoubleChevron } from '@components/icons' import { useRouter } from 'next/router'
import s from './I18nWidget.module.css' import s from './I18nWidget.module.css'
import { Cross } from '@components/icons'
interface LOCALE_DATA { interface LOCALE_DATA {
name: string name: string
img: { img: {
@ -32,6 +30,7 @@ const LOCALES_MAP: Record<string, LOCALE_DATA> = {
} }
const I18nWidget: FC = () => { const I18nWidget: FC = () => {
const [display, setDisplay] = useState(false)
const { const {
locale, locale,
locales, locales,
@ -39,42 +38,61 @@ const I18nWidget: FC = () => {
asPath: currentPath, asPath: currentPath,
} = useRouter() } = useRouter()
const options = locales?.filter((val) => val !== locale) const options = locales?.filter((val) => val !== locale)
const currentLocale = locale || defaultLocale const currentLocale = locale || defaultLocale
return ( return (
<nav className={s.root}> <nav className={s.root}>
<Menu> <div className="flex items-center relative">
<Menu.Button className={s.button} aria-label="Language selector"> <button className={s.button} aria-label="Language selector" />
<img <img
className="block mr-2 w-5" className="block mr-2 w-5"
src={`/${LOCALES_MAP[currentLocale].img.filename}`} src={`/${LOCALES_MAP[currentLocale].img.filename}`}
alt={LOCALES_MAP[currentLocale].img.alt} alt={LOCALES_MAP[currentLocale].img.alt}
/> />
<span className="mr-2">{LOCALES_MAP[currentLocale].name}</span> {options && (
{options && ( <span className="cursor-pointer" onClick={() => setDisplay(!display)}>
<span> <svg
<DoubleChevron /> viewBox="0 0 24 24"
</span> width="24"
)} height="24"
</Menu.Button> stroke="currentColor"
strokeWidth="1.5"
{options?.length ? ( strokeLinecap="round"
<Menu.Items className={s.dropdownMenu}> strokeLinejoin="round"
{options.map((locale) => ( fill="none"
<Menu.Item key={locale}> shapeRendering="geometricPrecision"
{({ active }) => ( >
<path d="M6 9l6 6 6-6" />
</svg>
</span>
)}
</div>
<div className="absolute top-0 right-0">
{options?.length && display ? (
<div className={s.dropdownMenu}>
<div className="flex flex-row justify-end px-6">
<button
onClick={() => setDisplay(false)}
aria-label="Close panel"
className={s.closeButton}
>
<Cross className="h-6 w-6" />
</button>
</div>
<ul>
{options.map((locale) => (
<li key={locale}>
<Link href={currentPath} locale={locale}> <Link href={currentPath} locale={locale}>
<a className={cn(s.item, { [s.active]: active })}> <a className={cn(s.item)} onClick={() => setDisplay(false)}>
{LOCALES_MAP[locale].name} {LOCALES_MAP[locale].name}
</a> </a>
</Link> </Link>
)} </li>
</Menu.Item> ))}
))} </ul>
</Menu.Items> </div>
) : null} ) : null}
</Menu> </div>
</nav> </nav>
) )
} }

View File

@ -1,17 +1,43 @@
import { FC, useCallback, useEffect, useState } from 'react'
import cn from 'classnames' import cn from 'classnames'
import { useRouter } from 'next/router' import dynamic from 'next/dynamic'
import type { Page } from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
import { CommerceProvider } from '@bigcommerce/storefront-data-hooks'
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { CartSidebarView } from '@components/cart'
import { Container, Sidebar, Button, Modal, Toast } from '@components/ui'
import { Navbar, FeatureBar, Footer } from '@components/common'
import { LoginView, SignUpView, ForgotPassword } from '@components/auth'
import { useUI } from '@components/ui/context'
import { usePreventScroll } from '@react-aria/overlays'
import s from './Layout.module.css' import s from './Layout.module.css'
import debounce from 'lodash.debounce' import { useRouter } from 'next/router'
import React, { FC } from 'react'
import { useUI } from '@components/ui/context'
import { Navbar, Footer } from '@components/common'
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { CommerceProvider } from '@bigcommerce/storefront-data-hooks'
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
import type { Page } from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
import { CartSidebarView } from '@components/cart'
const Loading = () => (
<div className="w-80 h-80 flex items-center text-center justify-center p-3">
<LoadingDots />
</div>
)
const dynamicProps = {
loading: () => <Loading />,
}
const LoginView = dynamic(
() => import('@components/auth/LoginView'),
dynamicProps
)
const SignUpView = dynamic(
() => import('@components/auth/SignUpView'),
dynamicProps
)
const ForgotPassword = dynamic(
() => import('@components/auth/ForgotPassword'),
dynamicProps
)
const FeatureBar = dynamic(
() => import('@components/common/FeatureBar'),
dynamicProps
)
interface Props { interface Props {
pageProps: { pageProps: {
pages?: Page[] pages?: Page[]
@ -25,51 +51,17 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
closeSidebar, closeSidebar,
closeModal, closeModal,
modalView, modalView,
toastText,
closeToast,
displayToast,
} = useUI() } = useUI()
const { acceptedCookies, onAcceptCookies } = useAcceptCookies() const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
const [hasScrolled, setHasScrolled] = useState(false)
const { locale = 'en-US' } = useRouter() const { locale = 'en-US' } = useRouter()
usePreventScroll({
isDisabled: !(displaySidebar || displayModal),
})
const handleScroll = useCallback(
debounce(() => {
const offset = 0
const { scrollTop } = document.documentElement
const scrolled = scrollTop > offset
setHasScrolled(scrolled)
}, 1),
[]
)
useEffect(() => {
document.addEventListener('scroll', handleScroll)
return () => {
document.removeEventListener('scroll', handleScroll)
}
}, [handleScroll])
return ( return (
<CommerceProvider locale={locale}> <CommerceProvider locale={locale}>
<div className={cn(s.root)}> <div className={cn(s.root)}>
<header <Navbar />
className={cn(
'sticky top-0 bg-primary z-40 transition-all duration-150',
{ 'shadow-magical': hasScrolled }
)}
>
<Container>
<Navbar />
</Container>
</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>
@ -79,6 +71,7 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
{modalView === 'SIGNUP_VIEW' && <SignUpView />} {modalView === 'SIGNUP_VIEW' && <SignUpView />}
{modalView === 'FORGOT_VIEW' && <ForgotPassword />} {modalView === 'FORGOT_VIEW' && <ForgotPassword />}
</Modal> </Modal>
<FeatureBar <FeatureBar
title="This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy." title="This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy."
hide={acceptedCookies} hide={acceptedCookies}
@ -88,10 +81,6 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
</Button> </Button>
} }
/> />
{/* <Toast open={displayToast} onClose={closeModal}>
{toastText}
</Toast> */}
</div> </div>
</CommerceProvider> </CommerceProvider>
) )

View File

@ -1,3 +1,7 @@
.root {
@apply sticky top-0 bg-primary z-40 transition-all duration-150;
}
.link { .link {
@apply inline-flex items-center text-primary leading-6 font-medium transition ease-in-out duration-75 cursor-pointer text-accents-6; @apply inline-flex items-center text-primary leading-6 font-medium transition ease-in-out duration-75 cursor-pointer text-accents-6;
} }

View File

@ -1,49 +1,64 @@
import { FC } from 'react' import { FC, useState, useEffect } from 'react'
import Link from 'next/link' import Link from 'next/link'
import s from './Navbar.module.css' import s from './Navbar.module.css'
import { Logo } from '@components/ui' import { Logo, Container } from '@components/ui'
import { Searchbar, UserNav } from '@components/common' import { Searchbar, UserNav } from '@components/common'
interface Props { import cn from 'classnames'
className?: string import throttle from 'lodash.throttle'
}
const Navbar: FC<Props> = ({ className }) => { const Navbar: FC = () => {
const rootClassName = className const [hasScrolled, setHasScrolled] = useState(false)
const handleScroll = () => {
const offset = 0
const { scrollTop } = document.documentElement
const scrolled = scrollTop > offset
setHasScrolled(scrolled)
}
useEffect(() => {
document.addEventListener('scroll', throttle(handleScroll, 200))
return () => {
document.removeEventListener('scroll', handleScroll)
}
}, [handleScroll])
return ( return (
<div className={rootClassName}> <div className={cn(s.root, { 'shadow-magical': hasScrolled })}>
<div className="flex justify-between align-center flex-row py-4 md:py-6 relative"> <Container>
<div className="flex flex-1 items-center"> <div className="flex justify-between align-center flex-row py-4 md:py-6 relative">
<Link href="/"> <div className="flex flex-1 items-center">
<a className={s.logo} aria-label="Logo">
<Logo />
</a>
</Link>
<nav className="space-x-4 ml-6 hidden lg:block">
<Link href="/"> <Link href="/">
<a className={s.link}>All</a> <a className={s.logo} aria-label="Logo">
<Logo />
</a>
</Link> </Link>
<Link href="/search?q=clothes"> <nav className="space-x-4 ml-6 hidden lg:block">
<a className={s.link}>Clothes</a> <Link href="/search">
</Link> <a className={s.link}>All</a>
<Link href="/search?q=accessories"> </Link>
<a className={s.link}>Accessories</a> <Link href="/search?q=clothes">
</Link> <a className={s.link}>Clothes</a>
</nav> </Link>
<Link href="/search?q=accessories">
<a className={s.link}>Accessories</a>
</Link>
</nav>
</div>
<div className="flex-1 justify-center hidden lg:flex">
<Searchbar />
</div>
<div className="flex flex-1 justify-end space-x-8">
<UserNav />
</div>
</div> </div>
<div className="flex-1 justify-center hidden lg:flex"> <div className="flex pb-4 lg:px-6 lg:hidden">
<Searchbar /> <Searchbar id="mobile-search" />
</div> </div>
</Container>
<div className="flex flex-1 justify-end space-x-8">
<UserNav />
</div>
</div>
<div className="flex pb-4 lg:px-6 lg:hidden">
<Searchbar id="mobileSearch" />
</div>
</div> </div>
) )
} }

View File

@ -1,4 +1,4 @@
import { FC, useEffect } from 'react' import { FC, useEffect, useMemo } from 'react'
import cn from 'classnames' import cn from 'classnames'
import s from './Searchbar.module.css' import s from './Searchbar.module.css'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
@ -15,14 +15,17 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
router.prefetch('/search') router.prefetch('/search')
}, []) }, [])
return ( return useMemo(
<div () => (
className={cn( <div
'relative text-sm bg-accents-1 text-base w-full transition-colors duration-150', className={cn(
className 'relative text-sm bg-accents-1 text-base w-full transition-colors duration-150',
)} className
> )}
<label htmlFor={id}> >
<label className="hidden" htmlFor={id}>
Search
</label>
<input <input
id={id} id={id}
className={s.input} className={s.input}
@ -45,17 +48,18 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
} }
}} }}
/> />
</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 fillRule="evenodd"
fillRule="evenodd" clipRule="evenodd"
clipRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" />
/> </svg>
</svg> </div>
</div> </div>
</div> ),
[]
) )
} }

View File

@ -1,2 +0,0 @@
.root {
}

View File

@ -1,55 +0,0 @@
import React, { FC } from 'react'
import { Switch } from '@headlessui/react'
import { Moon, Sun } from '@components/icons'
interface Props {
className?: string
checked: boolean
onChange: any
}
const Toggle: FC<Props> = ({ className, checked, onChange }) => {
return (
<Switch
checked={checked}
onChange={onChange}
className="focus:outline-none"
>
<span
role="checkbox"
aria-checked="false"
tabIndex={0}
className={`${
checked ? 'bg-gray-800' : 'bg-gray-200'
} relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-150 focus:outline-none focus:shadow-outline`}
>
<span
aria-hidden="true"
className={`${
checked ? 'translate-x-5' : 'translate-x-0'
} translate-x-0 relative inline-block h-5 w-5 rounded-full bg-white shadow transform transition ease-in-out duration-150`}
>
<span
className={`${
checked
? 'opacity-0 ease-out duration-150'
: 'opacity-100 ease-in duration-150'
} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`}
>
<Sun className="h-3 w-3 text-accent-3" />
</span>
<span
className={`${
checked
? 'opacity-100 ease-in 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`}
>
<Moon className="h-3 w-3 text-yellow-400" />
</span>
</span>
</span>
</Switch>
)
}
export default Toggle

View File

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

View File

@ -1,8 +1,8 @@
.dropdownMenu { .dropdownMenu {
@apply fixed right-0 mt-7 origin-top-right outline-none bg-primary z-40 w-full h-full; @apply fixed right-0 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
@screen lg { @screen lg {
@apply absolute border border-accents-1 shadow-lg w-56 h-auto; @apply absolute top-10 border border-accents-1 shadow-lg w-56 h-auto;
} }
} }

View File

@ -1,16 +1,16 @@
import { FC } from 'react'
import Link from 'next/link'
import { useTheme } from 'next-themes'
import cn from 'classnames' import cn from 'classnames'
import Link from 'next/link'
import { FC, useState } from 'react'
import { useTheme } from 'next-themes'
import { useRouter } from 'next/router'
import s from './DropdownMenu.module.css' import s from './DropdownMenu.module.css'
import { Avatar } from '@components/common'
import { Moon, Sun } from '@components/icons' import { Moon, Sun } from '@components/icons'
import { useUI } from '@components/ui/context' import { useUI } from '@components/ui/context'
import { Menu, Transition } from '@headlessui/react'
import useLogout from '@bigcommerce/storefront-data-hooks/use-logout'
import { useRouter } from 'next/router'
import useLogout from '@bigcommerce/storefront-data-hooks/use-logout'
interface DropdownMenuProps { interface DropdownMenuProps {
open: boolean open?: boolean
} }
const LINKS = [ const LINKS = [
@ -29,68 +29,70 @@ const LINKS = [
] ]
const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => { const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
const { theme, setTheme } = useTheme()
const logout = useLogout() const logout = useLogout()
const { pathname } = useRouter() const { pathname } = useRouter()
const { theme, setTheme } = useTheme()
const [display, setDisplay] = useState(false)
const { closeSidebarIfPresent } = useUI() const { closeSidebarIfPresent } = useUI()
return ( return (
<Transition <div>
show={open} <button
enter="transition ease-out duration-150 z-20" className={s.avatarButton}
enterFrom="transform opacity-0 scale-95" onClick={() => setDisplay(!display)}
enterTo="transform opacity-100 scale-100" aria-label="Menu"
leave="transition ease-in duration-75" >
leaveFrom="transform opacity-100 scale-100" <Avatar />
leaveTo="transform opacity-0 scale-95" </button>
>
<Menu.Items className={s.dropdownMenu}> {display && (
{LINKS.map(({ name, href }) => ( <ul className={s.dropdownMenu}>
<Menu.Item key={href}> {LINKS.map(({ name, href }) => (
<div> <li key={href}>
<Link href={href}> <div>
<a <Link href={href}>
className={cn(s.link, { <a
[s.active]: pathname === href, className={cn(s.link, {
})} [s.active]: pathname === href,
onClick={closeSidebarIfPresent} })}
> onClick={closeSidebarIfPresent}
{name} >
</a> {name}
</Link> </a>
</div> </Link>
</Menu.Item> </div>
))} </li>
<Menu.Item> ))}
<a <li>
className={cn(s.link, 'justify-between')} <a
onClick={() => className={cn(s.link, 'justify-between')}
theme === 'dark' ? setTheme('light') : setTheme('dark') onClick={() =>
} theme === 'dark' ? setTheme('light') : setTheme('dark')
> }
<div> >
Theme: <strong>{theme}</strong>{' '} <div>
</div> Theme: <strong>{theme}</strong>{' '}
<div className="ml-3"> </div>
{theme == 'dark' ? ( <div className="ml-3">
<Moon width={20} height={20} /> {theme == 'dark' ? (
) : ( <Moon width={20} height={20} />
<Sun width="20" height={20} /> ) : (
)} <Sun width="20" height={20} />
</div> )}
</a> </div>
</Menu.Item> </a>
<Menu.Item> </li>
<a <li>
className={cn(s.link, 'border-t border-accents-2 mt-4')} <a
onClick={() => logout()} className={cn(s.link, 'border-t border-accents-2 mt-4')}
> onClick={() => logout()}
Logout >
</a> Logout
</Menu.Item> </a>
</Menu.Items> </li>
</Transition> </ul>
)}
</div>
) )
} }

View File

@ -3,12 +3,11 @@ import Link from 'next/link'
import cn from 'classnames' import cn from 'classnames'
import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart' import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart'
import useCustomer from '@bigcommerce/storefront-data-hooks/use-customer' import useCustomer from '@bigcommerce/storefront-data-hooks/use-customer'
import { Menu } from '@headlessui/react'
import { Heart, Bag } from '@components/icons' import { Heart, Bag } from '@components/icons'
import { Avatar } from '@components/common'
import { useUI } from '@components/ui/context' import { useUI } from '@components/ui/context'
import DropdownMenu from './DropdownMenu' import DropdownMenu from './DropdownMenu'
import s from './UserNav.module.css' import s from './UserNav.module.css'
import { Avatar } from '@components/common'
interface Props { interface Props {
className?: string className?: string
@ -21,9 +20,9 @@ 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 { data: customer } = useCustomer() const { data: customer } = useCustomer()
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI() const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0) const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0)
return ( return (
<nav className={cn(s.root, className)}> <nav className={cn(s.root, className)}>
<div className={s.mainContainer}> <div className={s.mainContainer}>
@ -34,23 +33,14 @@ const UserNav: FC<Props> = ({ className, children, ...props }) => {
</li> </li>
<li className={s.item}> <li className={s.item}>
<Link href="/wishlist"> <Link href="/wishlist">
<a onClick={closeSidebarIfPresent}> <a onClick={closeSidebarIfPresent} aria-label="Wishlist">
<Heart /> <Heart />
</a> </a>
</Link> </Link>
</li> </li>
<li className={s.item}> <li className={s.item}>
{customer ? ( {customer ? (
<Menu> <DropdownMenu />
{({ open }) => (
<>
<Menu.Button className={s.avatarButton} aria-label="Menu">
<Avatar />
</Menu.Button>
<DropdownMenu open={open} />
</>
)}
</Menu>
) : ( ) : (
<button <button
className={s.avatarButton} className={s.avatarButton}

View File

@ -5,7 +5,5 @@ export { default as Layout } from './Layout'
export { default as Navbar } from './Navbar' export { default as Navbar } from './Navbar'
export { default as Searchbar } from './Searchbar' export { default as Searchbar } from './Searchbar'
export { default as UserNav } from './UserNav' export { default as UserNav } from './UserNav'
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 I18nWidget } from './I18nWidget' export { default as I18nWidget } from './I18nWidget'

View File

@ -5,11 +5,11 @@ const Cross = ({ ...props }) => {
width="24" width="24"
height="24" height="24"
stroke="currentColor" stroke="currentColor"
stroke-width="1.5" strokeWidth="1.5"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
fill="none" fill="none"
shape-rendering="geometricPrecision" shapeRendering="geometricPrecision"
{...props} {...props}
> >
<path d="M18 6L6 18" /> <path d="M18 6L6 18" />

View File

@ -1,4 +1,4 @@
const Sun = ({ ...props }) => { const Github = ({ ...props }) => {
return ( return (
<svg <svg
width="24" width="24"
@ -17,4 +17,4 @@ const Sun = ({ ...props }) => {
) )
} }
export default Sun export default Github

View File

@ -5,11 +5,11 @@ const Info = ({ ...props }) => {
width="24" width="24"
height="24" height="24"
stroke="currentColor" stroke="currentColor"
stroke-width="1.5" strokeWidth="1.5"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
fill="none" fill="none"
shape-rendering="geometricPrecision" shapeRendering="geometricPrecision"
{...props} {...props}
> >
<circle cx="12" cy="12" r="10" fill="transparent" /> <circle cx="12" cy="12" r="10" fill="transparent" />

View File

@ -1,5 +1,7 @@
.root { .root {
@apply relative max-h-full w-full box-border overflow-hidden bg-no-repeat bg-center bg-cover transition ease-linear cursor-pointer; @apply relative max-h-full w-full box-border overflow-hidden
bg-no-repeat bg-center bg-cover transition-transform
ease-linear cursor-pointer;
height: 100% !important; height: 100% !important;
&:hover { &:hover {
@ -67,24 +69,24 @@
.productTitle > span, .productTitle > span,
.productPrice, .productPrice,
.wishlistButton { .wishlistButton {
@apply transition ease-in-out duration-500; @apply transition-colors ease-in-out duration-500;
} }
.squareBg { .squareBg {
@apply transform absolute inset-0 z-0; @apply transition-colors absolute inset-0 z-0;
background-color: #212529; background-color: #212529;
} }
.squareBg:before { .squareBg:before {
@apply transition ease-in-out duration-500 bg-repeat-space w-full h-full block; @apply transition ease-in-out duration-500 bg-repeat-space w-full h-full block;
background-image: url('/bg-products.svg');
content: ''; content: '';
background-image: url("data:image/svg+xml,%3Csvg width='48' height='46' viewBox='0 0 48 46' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cline opacity='0.1' x1='9.41421' y1='8' x2='21' y2='19.5858' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cline opacity='0.1' x1='1' y1='-1' x2='17.3848' y2='-1' transform='matrix(-0.707107 0.707107 0.707107 0.707107 40 8)' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cline opacity='0.1' x1='1' y1='-1' x2='17.3848' y2='-1' transform='matrix(0.707107 -0.707107 -0.707107 -0.707107 8 38)' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cline opacity='0.1' x1='38.5858' y1='38' x2='27' y2='26.4142' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
} }
.simple { .simple {
& .squareBg { & .squareBg {
@apply bg-accents-0 !important; @apply bg-accents-0 !important;
background-image: url("data:image/svg+xml,%3Csvg width='48' height='46' viewBox='0 0 48 46' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cline opacity='0.05' x1='9.41421' y1='8' x2='21' y2='19.5858' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cline opacity='0.05' x1='1' y1='-1' x2='17.3848' y2='-1' transform='matrix(-0.707107 0.707107 0.707107 0.707107 40 8)' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cline opacity='0.05' x1='1' y1='-1' x2='17.3848' y2='-1' transform='matrix(0.707107 -0.707107 -0.707107 -0.707107 8 38)' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cline opacity='0.05' x1='38.5858' y1='38' x2='27' y2='26.4142' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A"); background-image: url('/bg-products.svg');
} }
& .productTitle { & .productTitle {
@ -125,15 +127,15 @@
} }
.imageContainer { .imageContainer {
@apply flex items-center justify-center;
overflow: hidden;
& > div { & > div {
& > div { min-width: 100%;
height: 100%;
margin: 0 auto;
}
} }
} }
.product-image { .image {
height: 120% !important; object-fit: cover;
top: -10% !important; transform: scale(1.2);
} }

View File

@ -13,7 +13,10 @@ interface Props {
variant?: 'slim' | 'simple' variant?: 'slim' | 'simple'
imgWidth: number | string imgWidth: number | string
imgHeight: number | string imgHeight: number | string
priority?: boolean imgLayout?: 'fixed' | 'intrinsic' | 'responsive' | undefined
imgPriority?: boolean
imgLoading?: 'eager' | 'lazy'
imgSizes?: string
} }
const ProductCard: FC<Props> = ({ const ProductCard: FC<Props> = ({
@ -22,7 +25,10 @@ const ProductCard: FC<Props> = ({
variant, variant,
imgWidth, imgWidth,
imgHeight, imgHeight,
priority, imgPriority,
imgLoading,
imgSizes,
imgLayout = 'responsive',
}) => { }) => {
const src = p.images.edges?.[0]?.node?.urlOriginal! const src = p.images.edges?.[0]?.node?.urlOriginal!
const { price } = usePrice({ const { price } = usePrice({
@ -44,12 +50,15 @@ const ProductCard: FC<Props> = ({
</span> </span>
</div> </div>
<Image <Image
quality="85"
width={imgWidth}
sizes={imgSizes}
height={imgHeight}
layout={imgLayout}
loading={imgLoading}
priority={imgPriority}
src={p.images.edges?.[0]?.node.urlOriginal!} src={p.images.edges?.[0]?.node.urlOriginal!}
alt={p.images.edges?.[0]?.node.altText || 'Product Image'} alt={p.images.edges?.[0]?.node.altText || 'Product Image'}
width={imgWidth}
height={imgHeight}
priority={priority}
quality="85"
/> />
</div> </div>
) : ( ) : (
@ -70,13 +79,16 @@ const ProductCard: FC<Props> = ({
</div> </div>
<div className={s.imageContainer}> <div className={s.imageContainer}>
<Image <Image
alt={p.name}
className={cn('w-full object-cover', s['product-image'])}
src={src}
width={imgWidth}
height={imgHeight}
priority={priority}
quality="85" quality="85"
src={src}
alt={p.name}
className={s.image}
width={imgWidth}
sizes={imgSizes}
height={imgHeight}
layout={imgLayout}
loading={imgLoading}
priority={imgPriority}
/> />
</div> </div>
</> </>

View File

@ -6,8 +6,7 @@ import { NextSeo } from 'next-seo'
import s from './ProductView.module.css' import s from './ProductView.module.css'
import { useUI } from '@components/ui/context' import { useUI } from '@components/ui/context'
import { Swatch, ProductSlider } from '@components/product' import { Swatch, ProductSlider } from '@components/product'
import { Button, Container } from '@components/ui' import { Button, Container, Text } from '@components/ui'
import { HTMLContent } from '@components/common'
import usePrice from '@bigcommerce/storefront-data-hooks/use-price' import usePrice from '@bigcommerce/storefront-data-hooks/use-price'
import useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item' import useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item'
@ -137,7 +136,7 @@ const ProductView: FC<Props> = ({ product }) => {
))} ))}
<div className="pb-14 break-words w-full max-w-xl"> <div className="pb-14 break-words w-full max-w-xl">
<HTMLContent html={product.description} /> <Text html={product.description} />
</div> </div>
</section> </section>
<div> <div>

View File

@ -6,7 +6,6 @@ import React, {
useRef, useRef,
} from 'react' } from 'react'
import mergeRefs from 'react-merge-refs' import mergeRefs from 'react-merge-refs'
import { useButton } from 'react-aria'
import s from './Button.module.css' import s from './Button.module.css'
import { LoadingDots } from '@components/ui' import { LoadingDots } from '@components/ui'
@ -34,19 +33,8 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
loading = false, loading = false,
disabled = false, disabled = false,
style = {}, style = {},
...rest
} = props } = props
const ref = useRef<typeof Component>(null) const ref = useRef<typeof Component>(null)
const { buttonProps, isPressed } = useButton(
{
...rest,
// @ts-ignore onClick === onPress for our purposes
onPress: onClick,
isDisabled: disabled,
elementType: Component,
},
ref
)
const rootClassName = cn( const rootClassName = cn(
s.root, s.root,
@ -63,8 +51,6 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
aria-pressed={active} aria-pressed={active}
data-variant={variant} data-variant={variant}
ref={mergeRefs([ref, buttonRef])} ref={mergeRefs([ref, buttonRef])}
{...buttonProps}
data-active={isPressed ? '' : undefined}
className={rootClassName} className={rootClassName}
disabled={disabled} disabled={disabled}
style={{ style={{

View File

@ -1,6 +1,7 @@
.root { .root {
--row-height: calc(100vh - 80px - 56px); --row-height: calc(100vh - 88px);
@apply grid grid-cols-1 gap-0; @apply grid grid-cols-1 gap-0;
min-height: var(--row-height);
@screen lg { @screen lg {
@apply grid-cols-3 grid-rows-2; @apply grid-cols-3 grid-rows-2;

View File

@ -2,6 +2,7 @@ import cn from 'classnames'
import s from './Marquee.module.css' import s from './Marquee.module.css'
import { FC, ReactNode, Component } from 'react' import { FC, ReactNode, Component } from 'react'
import Ticker from 'react-ticker' import Ticker from 'react-ticker'
import { useInView } from 'react-intersection-observer'
interface Props { interface Props {
className?: string className?: string
@ -9,7 +10,11 @@ interface Props {
variant?: 'primary' | 'secondary' variant?: 'primary' | 'secondary'
} }
const M: FC<Props> = ({ className = '', children, variant = 'primary' }) => { const Maquee: FC<Props> = ({
className = '',
children,
variant = 'primary',
}) => {
const rootClassName = cn( const rootClassName = cn(
s.root, s.root,
{ {
@ -18,14 +23,20 @@ const M: FC<Props> = ({ className = '', children, variant = 'primary' }) => {
}, },
className className
) )
const [ref, inView] = useInView({
triggerOnce: true,
rootMargin: '200px 0px',
})
return ( return (
<div className={rootClassName}> <div className={rootClassName} ref={ref}>
<Ticker offset={80}> {inView ? (
{({ index }) => <div className={s.container}>{children}</div>} <Ticker offset={80}>
</Ticker> {() => <div className={s.container}>{children}</div>}
</Ticker>
) : null}
</div> </div>
) )
} }
export default M export default Maquee

View File

@ -1,11 +1,13 @@
import cn from 'classnames' import { FC, useRef, useEffect } from 'react'
import { FC, useRef } from 'react' import Portal from '@reach/portal'
import s from './Modal.module.css' import s from './Modal.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'
import { Cross } from '@components/icons' import { Cross } from '@components/icons'
import {
disableBodyScroll,
enableBodyScroll,
clearAllBodyScrollLocks,
} from 'body-scroll-lock'
interface Props { interface Props {
className?: string className?: string
children?: any children?: any
@ -13,64 +15,41 @@ interface Props {
onClose: () => void onClose: () => void
} }
const Modal: FC<Props> = ({ const Modal: FC<Props> = ({ children, open, onClose }) => {
className, const ref = useRef() as React.MutableRefObject<HTMLDivElement>
children,
open = false, useEffect(() => {
onClose, if (ref.current) {
...props if (open) {
}) => { disableBodyScroll(ref.current)
const rootClassName = cn(s.root, className) } else {
let ref = useRef() as React.MutableRefObject<HTMLInputElement> enableBodyScroll(ref.current)
let { modalProps } = useModal() }
let { dialogProps } = useDialog({}, ref) }
let { overlayProps } = useOverlay( return () => {
{ clearAllBodyScrollLocks()
isOpen: open, }
isDismissable: false, }, [open])
onClose: onClose,
...props,
},
ref
)
return ( return (
<Transition show={open}> <Portal>
<OverlayContainer> {open ? (
<FocusScope contain restoreFocus autoFocus> <div className={s.root} ref={ref}>
<div className={rootClassName}> <div className={s.modal}>
<Transition.Child <div className="h-7 flex items-center justify-end w-full">
enter="transition-opacity ease-linear duration-300" <button
enterFrom="opacity-0" onClick={() => onClose()}
enterTo="opacity-100" aria-label="Close panel"
leave="transition-opacity ease-linear duration-300" className="hover:text-gray-500 transition ease-in-out duration-150 focus:outline-none"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div
className={s.modal}
{...overlayProps}
{...dialogProps}
{...modalProps}
ref={ref}
> >
<div className="h-7 flex items-center justify-end w-full"> <Cross className="h-6 w-6" />
<button </button>
onClick={() => onClose()} </div>
aria-label="Close panel" {children}
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> </div>
</FocusScope> </div>
</OverlayContainer> ) : null}
</Transition> </Portal>
) )
} }

View File

@ -1,79 +1,54 @@
import cn from 'classnames'
import { FC, useRef } from 'react'
import s from './Sidebar.module.css' import s from './Sidebar.module.css'
import { Transition } from '@headlessui/react' import Portal from '@reach/portal'
import { useOverlay, useModal, OverlayContainer } from '@react-aria/overlays' import { FC, useEffect, useRef } from 'react'
import { useDialog } from '@react-aria/dialog' import {
import { FocusScope } from '@react-aria/focus' disableBodyScroll,
enableBodyScroll,
clearAllBodyScrollLocks,
} from 'body-scroll-lock'
interface Props { interface Props {
className?: string children: any
children?: any open: boolean
open?: boolean
onClose: () => void onClose: () => void
} }
const Sidebar: FC<Props> = ({ className, children, open = false, onClose }) => { const Sidebar: FC<Props> = ({ children, open = false, onClose }) => {
const rootClassName = cn(s.root, className) const ref = useRef() as React.MutableRefObject<HTMLDivElement>
const ref = useRef<HTMLDivElement>(null)
const { modalProps } = useModal() useEffect(() => {
const { overlayProps } = useOverlay( if (ref.current) {
{ if (open) {
isOpen: open, disableBodyScroll(ref.current)
isDismissable: true, } else {
onClose: onClose, enableBodyScroll(ref.current)
}, }
ref }
) return () => {
const { dialogProps } = useDialog({}, ref) clearAllBodyScrollLocks()
}
}, [open])
return ( return (
<Transition show={open}> <Portal>
<OverlayContainer> {open ? (
<FocusScope contain restoreFocus autoFocus> <div className={s.root} ref={ref}>
<div className={rootClassName}> <div className="absolute inset-0 overflow-hidden">
<div className="absolute inset-0 overflow-hidden"> <div
<Transition.Child className="absolute inset-0 bg-black bg-opacity-50 transition-opacity"
enter="transition-opacity ease-linear duration-300" onClick={onClose}
enterFrom="opacity-0" />
enterTo="opacity-100" <section className="absolute inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16 outline-none">
leave="transition-opacity ease-linear duration-300" <div className="h-full md:w-screen md:max-w-md">
leaveFrom="opacity-100" <div className="h-full flex flex-col text-base bg-accents-1 shadow-xl overflow-y-auto">
leaveTo="opacity-0" {children}
> </div>
<div </div>
className="absolute inset-0 bg-black bg-opacity-50 transition-opacity" </section>
// Close the sidebar when clicking on the backdrop
onClick={onClose}
/>
</Transition.Child>
<section
className="absolute inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16 outline-none"
{...dialogProps}
{...overlayProps}
{...modalProps}
ref={ref}
>
<Transition.Child
enter="transition ease-in-out duration-300 transform"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="h-full md:w-screen md:max-w-md">
<div className="h-full flex flex-col text-base bg-accents-1 shadow-xl overflow-y-auto">
{children}
</div>
</div>
</Transition.Child>
</section>
</div>
</div> </div>
</FocusScope> </div>
</OverlayContainer> ) : null}
</Transition> </Portal>
) )
} }

View File

@ -10,7 +10,8 @@ interface Props {
variant?: Variant variant?: Variant
className?: string className?: string
style?: CSSProperties style?: CSSProperties
children: React.ReactNode | any children?: React.ReactNode | any
html?: string
} }
type Variant = 'heading' | 'body' | 'pageHeading' | 'sectionHeading' type Variant = 'heading' | 'body' | 'pageHeading' | 'sectionHeading'
@ -20,6 +21,7 @@ const Text: FunctionComponent<Props> = ({
className = '', className = '',
variant = 'body', variant = 'body',
children, children,
html,
}) => { }) => {
const componentsMap: { const componentsMap: {
[P in Variant]: React.ComponentType<any> | string [P in Variant]: React.ComponentType<any> | string
@ -36,6 +38,12 @@ const Text: FunctionComponent<Props> = ({
| React.ComponentType<any> | React.ComponentType<any>
| string = componentsMap![variant!] | string = componentsMap![variant!]
const htmlContentProps = html
? {
dangerouslySetInnerHTML: { __html: html },
}
: {}
return ( return (
<Component <Component
className={cn( className={cn(
@ -49,6 +57,7 @@ const Text: FunctionComponent<Props> = ({
className className
)} )}
style={style} style={style}
{...htmlContentProps}
> >
{children} {children}
</Component> </Component>

View File

@ -1,9 +0,0 @@
.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

@ -1,73 +0,0 @@
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

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

View File

@ -1,6 +1,5 @@
import React, { FC, useMemo } from 'react' import React, { FC, useMemo } from 'react'
import { ThemeProvider } from 'next-themes' import { ThemeProvider } from 'next-themes'
import { SSRProvider, OverlayProvider } from 'react-aria'
export interface State { export interface State {
displaySidebar: boolean displaySidebar: boolean
@ -181,10 +180,6 @@ export const useUI = () => {
export const ManagedUIContext: FC = ({ children }) => ( export const ManagedUIContext: FC = ({ children }) => (
<UIProvider> <UIProvider>
<ThemeProvider> <ThemeProvider>{children}</ThemeProvider>
<SSRProvider>
<OverlayProvider>{children}</OverlayProvider>
</SSRProvider>
</ThemeProvider>
</UIProvider> </UIProvider>
) )

View File

@ -10,4 +10,3 @@ 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 Text } from './Text'
export { default as Input } from './Input' export { default as Input } from './Input'
export { default as Toast } from './Toast'

View File

@ -7,8 +7,7 @@ import usePrice from '@bigcommerce/storefront-data-hooks/use-price'
import useRemoveItem from '@bigcommerce/storefront-data-hooks/wishlist/use-remove-item' import useRemoveItem from '@bigcommerce/storefront-data-hooks/wishlist/use-remove-item'
import useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item' import useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item'
import { useUI } from '@components/ui/context' import { useUI } from '@components/ui/context'
import { Button } from '@components/ui' import { Button, Text } from '@components/ui'
import { HTMLContent } from '@components/common'
import { Trash } from '@components/icons' import { Trash } from '@components/icons'
import s from './WishlistCard.module.css' import s from './WishlistCard.module.css'
@ -72,7 +71,7 @@ const WishlistCard: FC<Props> = ({ item }) => {
</Link> </Link>
</h3> </h3>
<div className="mb-4"> <div className="mb-4">
<HTMLContent html={product.description!} /> <Text html={product.description} />
</div> </div>
<Button <Button
aria-label="Add to Cart" aria-label="Add to Cart"

View File

@ -1,16 +0,0 @@
import * as Bowser from 'bowser'
export function isDesktop(): boolean {
const browser = Bowser.getParser(window.navigator.userAgent)
return browser.getPlatform().type === 'desktop'
}
export function isMobile(): boolean {
const browser = Bowser.getParser(window.navigator.userAgent)
return browser.getPlatform().type === 'mobile'
}
export function isTablet(): boolean {
const browser = Bowser.getParser(window.navigator.userAgent)
return browser.getPlatform().type === 'tablet'
}

14
lib/defaults.ts Normal file
View File

@ -0,0 +1,14 @@
// Fallback to CMS Data
export const defatultPageProps = {
header: {
links: [
{
link: {
title: 'New Arrivals',
url: '/',
},
},
],
},
}

21
license.md Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020 Vercel, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,6 +1,11 @@
module.exports = {
const bundleAnalyzer = require('@next/bundle-analyzer')({
enabled: !!process.env.BUNDLE_ANALYZE
})
module.exports = bundleAnalyzer({
images: { images: {
sizes: [320, 480, 820, 1200, 1600],
domains: ['cdn11.bigcommerce.com'], domains: ['cdn11.bigcommerce.com'],
}, },
i18n: { i18n: {
@ -51,4 +56,4 @@ module.exports = {
}, },
] ]
}, },
} });

8868
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,47 +4,81 @@
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start" "start": "next start",
"analyze": "BUNDLE_ANALYZE=both yarn build",
"find:unused": "next-unused"
}, },
"prettier": { "prettier": {
"semi": false, "semi": false,
"singleQuote": true "singleQuote": true
}, },
"next-unused": {
"alias": {
"@lib/*": [
"lib/*"
],
"@assets/*": [
"assets/*"
],
"@config/*": [
"config/*"
],
"@components/*": [
"components/*"
],
"@utils/*": [
"utils/*"
]
},
"debug": true,
"include": [
"components",
"lib",
"pages"
],
"exclude": [],
"entrypoints": [
"pages"
]
},
"dependencies": { "dependencies": {
"@bigcommerce/storefront-data-hooks": "^1.0.2", "@bigcommerce/storefront-data-hooks": "^1.0.2",
"@headlessui/react": "^0.2.0", "@reach/portal": "^0.11.2",
"@react-aria/overlays": "^3.4.0",
"@tailwindcss/ui": "^0.6.2", "@tailwindcss/ui": "^0.6.2",
"@types/body-scroll-lock": "^2.6.1",
"@types/lodash.throttle": "^4.1.6",
"@vercel/fetch": "^6.1.0", "@vercel/fetch": "^6.1.0",
"body-scroll-lock": "^3.1.5",
"bowser": "^2.11.0", "bowser": "^2.11.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"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.random": "^3.2.0", "lodash.random": "^3.2.0",
"next": "^10.0.1-canary.7", "lodash.throttle": "^4.1.1",
"next": "^10.0.3-canary.3",
"next-seo": "^4.11.0", "next-seo": "^4.11.0",
"next-themes": "^0.0.4", "next-themes": "^0.0.4",
"postcss-nesting": "^7.0.1", "postcss-nesting": "^7.0.1",
"react": "^16.14.0", "react": "^16.14.0",
"react-aria": "^3.0.0",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"react-intersection-observer": "^8.30.1",
"react-merge-refs": "^1.1.0", "react-merge-refs": "^1.1.0",
"react-ticker": "^1.2.2", "react-ticker": "^1.2.2",
"tailwindcss": "^1.9" "tailwindcss": "^1.9"
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "^10.0.1",
"@types/bunyan": "^1.8.6", "@types/bunyan": "^1.8.6",
"@types/bunyan-prettystream": "^0.1.31", "@types/bunyan-prettystream": "^0.1.31",
"@types/classnames": "^2.2.10", "@types/classnames": "^2.2.10",
"@types/js-cookie": "^2.2.6", "@types/js-cookie": "^2.2.6",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.random": "^3.2.6", "@types/lodash.random": "^3.2.6",
"@types/node": "^14.11.2", "@types/node": "^14.11.2",
"@types/react": "^16.9.49", "@types/react": "^16.9.49",
"bunyan": "^1.8.14", "bunyan": "^1.8.14",
"bunyan-prettystream": "^0.1.3", "bunyan-prettystream": "^0.1.3",
"next-unused": "^0.0.3",
"postcss-flexbugs-fixes": "^4.2.1", "postcss-flexbugs-fixes": "^4.2.1",
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"prettier": "^2.1.2", "prettier": "^2.1.2",

View File

@ -3,12 +3,14 @@ import type {
GetStaticPropsContext, GetStaticPropsContext,
InferGetStaticPropsType, InferGetStaticPropsType,
} from 'next' } from 'next'
import getSlug from '@lib/get-slug'
import { missingLocaleInPages } from '@lib/usage-warns'
import { Layout } from '@components/common'
import { Text } from '@components/ui'
import { getConfig } from '@bigcommerce/storefront-data-hooks/api' import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
import getPage from '@bigcommerce/storefront-data-hooks/api/operations/get-page' import getPage from '@bigcommerce/storefront-data-hooks/api/operations/get-page'
import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages' import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
import getSlug from '@lib/get-slug' import { defatultPageProps } from '@lib/defaults'
import { missingLocaleInPages } from '@lib/usage-warns'
import { Layout, HTMLContent } from '@components/common'
export async function getStaticProps({ export async function getStaticProps({
preview, preview,
@ -32,7 +34,7 @@ export async function getStaticProps({
} }
return { return {
props: { pages, page }, props: { ...defatultPageProps, pages, page },
revalidate: 60 * 60, // Every hour revalidate: 60 * 60, // Every hour
} }
} }
@ -64,7 +66,7 @@ export default function Pages({
}: InferGetStaticPropsType<typeof getStaticProps>) { }: InferGetStaticPropsType<typeof getStaticProps>) {
return ( return (
<div className="max-w-2xl mx-auto py-20"> <div className="max-w-2xl mx-auto py-20">
{page?.body && <HTMLContent html={page.body} />} {page?.body && <Text html={page.body} />}
</div> </div>
) )
} }

View File

@ -1,10 +1,9 @@
import { useMemo } from 'react'
import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
import rangeMap from '@lib/range-map' import rangeMap from '@lib/range-map'
import { Layout } from '@components/common' import { Layout } from '@components/common'
import { Grid, Marquee, Hero } from '@components/ui'
import { ProductCard } from '@components/product' import { ProductCard } from '@components/product'
import { Grid, Marquee, Hero } from '@components/ui'
import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid' import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid'
import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
import { getConfig } from '@bigcommerce/storefront-data-hooks/api' import { getConfig } from '@bigcommerce/storefront-data-hooks/api'
import getAllProducts from '@bigcommerce/storefront-data-hooks/api/operations/get-all-products' import getAllProducts from '@bigcommerce/storefront-data-hooks/api/operations/get-all-products'
@ -17,47 +16,33 @@ export async function getStaticProps({
}: GetStaticPropsContext) { }: GetStaticPropsContext) {
const config = getConfig({ locale }) const config = getConfig({ locale })
// Get Featured Products
const { products: featuredProducts } = await getAllProducts({ const { products: featuredProducts } = await getAllProducts({
variables: { field: 'featuredProducts', first: 6 }, variables: { field: 'featuredProducts', first: 6 },
config, config,
preview, preview,
}) })
// Get Best Selling Products
const { products: bestSellingProducts } = await getAllProducts({ const { products: bestSellingProducts } = await getAllProducts({
variables: { field: 'bestSellingProducts', first: 6 }, variables: { field: 'bestSellingProducts', first: 6 },
config, config,
preview, preview,
}) })
// Get Best Newest Products
const { products: newestProducts } = await getAllProducts({ const { products: newestProducts } = await getAllProducts({
variables: { field: 'newestProducts', first: 12 }, variables: { field: 'newestProducts', first: 12 },
config, config,
preview, preview,
}) })
const { categories, brands } = await getSiteInfo({ config, preview }) const { categories, brands } = await getSiteInfo({ config, preview })
const { pages } = await getAllPages({ config, preview }) const { pages } = await getAllPages({ config, preview })
return { // These are the products that are going to be displayed in the landing.
props: { // We prefer to do the computation at buildtime/servertime
featuredProducts, const { featured, bestSelling } = (() => {
bestSellingProducts,
newestProducts,
categories,
brands,
pages,
},
revalidate: 10,
}
}
const nonNullable = (v: any) => v
export default function Home({
featuredProducts,
bestSellingProducts,
newestProducts,
categories,
brands,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const { featured, bestSelling } = useMemo(() => {
// Create a copy of products that we can mutate // Create a copy of products that we can mutate
const products = [...newestProducts] const products = [...newestProducts]
// If the lists of featured and best selling products don't have enough // If the lists of featured and best selling products don't have enough
@ -73,8 +58,30 @@ export default function Home({
(i) => bestSellingProducts[i] ?? products.shift() (i) => bestSellingProducts[i] ?? products.shift()
).filter(nonNullable), ).filter(nonNullable),
} }
}, [newestProducts, featuredProducts, bestSellingProducts]) })()
return {
props: {
featured,
bestSelling,
newestProducts,
categories,
brands,
pages,
},
revalidate: 14400,
}
}
const nonNullable = (v: any) => v
export default function Home({
featured,
bestSelling,
brands,
categories,
newestProducts,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return ( return (
<div> <div>
<Grid> <Grid>
@ -82,10 +89,10 @@ export default function Home({
<ProductCard <ProductCard
key={node.path} key={node.path}
product={node} product={node}
// The first image is the largest one in the grid imgWidth={i === 0 ? 1080 : 540}
imgWidth={i === 0 ? 1600 : 820} imgHeight={i === 0 ? 1080 : 540}
imgHeight={i === 0 ? 1600 : 820} imgPriority
priority imgLoading="eager"
/> />
))} ))}
</Grid> </Grid>
@ -97,6 +104,7 @@ export default function Home({
variant="slim" variant="slim"
imgWidth={320} imgWidth={320}
imgHeight={320} imgHeight={320}
imgLayout="fixed"
/> />
))} ))}
</Marquee> </Marquee>
@ -115,9 +123,8 @@ export default function Home({
<ProductCard <ProductCard
key={node.path} key={node.path}
product={node} product={node}
// The second image is the largest one in the grid imgWidth={i === 1 ? 1080 : 540}
imgWidth={i === 1 ? 1600 : 820} imgHeight={i === 1 ? 1080 : 540}
imgHeight={i === 1 ? 1600 : 820}
/> />
))} ))}
</Grid> </Grid>
@ -129,6 +136,7 @@ export default function Home({
variant="slim" variant="slim"
imgWidth={320} imgWidth={320}
imgHeight={320} imgHeight={320}
imgLayout="fixed"
/> />
))} ))}
</Marquee> </Marquee>

View File

@ -51,8 +51,7 @@ export async function getStaticPaths({ locales }: GetStaticPathsContext) {
return arr return arr
}, []) }, [])
: products.map((product) => `/product${product.node.path}`), : products.map((product) => `/product${product.node.path}`),
// If your store has tons of products, enable fallback mode to improve build times! fallback: 'blocking',
fallback: false,
} }
} }

View File

@ -4,9 +4,9 @@ import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-a
import useWishlist from '@bigcommerce/storefront-data-hooks/wishlist/use-wishlist' import useWishlist from '@bigcommerce/storefront-data-hooks/wishlist/use-wishlist'
import { Layout } from '@components/common' import { Layout } from '@components/common'
import { Heart } from '@components/icons' import { Heart } from '@components/icons'
import { Container, Text } from '@components/ui' import { Text, Container } from '@components/ui'
import { WishlistCard } from '@components/wishlist' import { WishlistCard } from '@components/wishlist'
import { Transition } from '@headlessui/react' import { defatultPageProps } from '@lib/defaults'
export async function getStaticProps({ export async function getStaticProps({
preview, preview,
@ -15,7 +15,7 @@ export async function getStaticProps({
const config = getConfig({ locale }) const config = getConfig({ locale })
const { pages } = await getAllPages({ config, preview }) const { pages } = await getAllPages({ config, preview })
return { return {
props: { pages }, props: { ...defatultPageProps, pages },
} }
} }
@ -28,45 +28,22 @@ export default function Wishlist() {
<Text variant="pageHeading">My Wishlist</Text> <Text variant="pageHeading">My Wishlist</Text>
<div className="group flex flex-col"> <div className="group flex flex-col">
{isEmpty ? ( {isEmpty ? (
<Transition show> <div className="flex-1 px-12 py-24 flex flex-col justify-center items-center ">
<Transition.Child <span className="border border-dashed border-secondary flex items-center justify-center w-16 h-16 bg-primary p-12 rounded-lg text-primary">
enter="transition-opacity ease-linear duration-300" <Heart className="absolute" />
enterFrom="opacity-0" </span>
enterTo="opacity-100" <h2 className="pt-6 text-2xl font-bold tracking-wide text-center">
leave="transition-opacity ease-linear duration-300" Your wishlist is empty
leaveFrom="opacity-100" </h2>
leaveTo="opacity-0" <p className="text-accents-6 px-10 text-center pt-2">
> Biscuit oat cake wafer icing ice cream tiramisu pudding cupcake.
<div className="flex-1 px-12 py-24 flex flex-col justify-center items-center "> </p>
<span className="border border-dashed border-secondary rounded-full flex items-center justify-center w-16 h-16 bg-primary p-12 rounded-lg text-primary"> </div>
<Heart className="absolute" />
</span>
<h2 className="pt-6 text-2xl font-bold tracking-wide text-center">
Your wishlist is empty
</h2>
<p className="text-accents-6 px-10 text-center pt-2">
Biscuit oat cake wafer icing ice cream tiramisu pudding
cupcake.
</p>
</div>
</Transition.Child>
</Transition>
) : ( ) : (
<Transition show> data &&
{data && data.items?.map((item) => (
data.items?.map((item) => ( <WishlistCard key={item.id} item={item} />
<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"
>
<WishlistCard key={item.id} item={item} />
</Transition.Child>
))}
</Transition>
)} )}
</div> </div>
</div> </div>

7
public/bg-products.svg Normal file
View File

@ -0,0 +1,7 @@
<svg width="48" height="46" viewBox="0 0 48 46" fill="none" xmlns="http://www.w3.org/2000/svg">
<line opacity="0.1" x1="9.41421" y1="8" x2="21" y2="19.5858" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line opacity="0.1" x1="1" y1="-1" x2="17.3848" y2="-1" transform="matrix(-0.707107 0.707107 0.707107 0.707107 40 8)" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line opacity="0.1" x1="1" y1="-1" x2="17.3848" y2="-1" transform="matrix(0.707107 -0.707107 -0.707107 -0.707107 8 38)" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line opacity="0.1" x1="38.5858" y1="38" x2="27" y2="26.4142" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 797 B

View File

@ -1,4 +1,7 @@
module.exports = { module.exports = {
future: {
purgeLayersByDefault: true,
},
purge: { purge: {
content: [ content: [
'./pages/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}',

3397
yarn.lock

File diff suppressed because it is too large Load Diff