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
- UI Components
- Theming
- Standarized Data Hooks
- Standardized Data Hooks
- Integrations - Integrate seamlessly with the most common ecommerce platforms.
- 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.
</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
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)}>
<Minus width={18} height={18} />
</button>
<input
type="number"
max={99}
min={0}
className={s.quantity}
value={quantity}
onChange={handleQuantity}
onBlur={handleBlur}
/>
<label>
<input
type="number"
max={99}
min={0}
className={s.quantity}
value={quantity}
onChange={handleQuantity}
onBlur={handleBlur}
/>
</label>
<button type="button" onClick={() => increaseQuantity(1)}>
<Plus width={18} height={18} />
</button>

View File

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

View File

@ -1,5 +1,5 @@
import cn from 'classnames'
import { FC, useState } from 'react'
import { FC, useState, useMemo, useRef, useEffect } from 'react'
import { getRandomPairOfColors } from '@lib/colors'
interface Props {
@ -8,14 +8,19 @@ interface 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 (
<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"
style={{
backgroundImage: `linear-gradient(140deg, ${bg[0]}, ${bg[1]} 100%)`,
}}
>
{/* Add an image - We're generating a gradient as placeholder <img></img> */}
</div>

View File

@ -84,7 +84,11 @@ const Footer: FC<Props> = ({ className, pages }) => {
</div>
<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">
<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 />
</a>
<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 Link from 'next/link'
import { getCategoryPath, getDesignerPath } from '@lib/search'
import { Grid } from '@components/ui'
import { ProductCard } from '@components/product'
import s from './HomeAllProductsGrid.module.css'
import { getCategoryPath, getDesignerPath } from '@lib/search'
interface Props {
categories?: any

View File

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

View File

@ -1,17 +1,43 @@
import { FC, useCallback, useEffect, useState } from 'react'
import cn from 'classnames'
import { useRouter } from 'next/router'
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 dynamic from 'next/dynamic'
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 {
pageProps: {
pages?: Page[]
@ -25,51 +51,17 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
closeSidebar,
closeModal,
modalView,
toastText,
closeToast,
displayToast,
} = useUI()
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
const [hasScrolled, setHasScrolled] = useState(false)
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 (
<CommerceProvider locale={locale}>
<div className={cn(s.root)}>
<header
className={cn(
'sticky top-0 bg-primary z-40 transition-all duration-150',
{ 'shadow-magical': hasScrolled }
)}
>
<Container>
<Navbar />
</Container>
</header>
<Navbar />
<main className="fit">{children}</main>
<Footer pages={pageProps.pages} />
<Sidebar open={displaySidebar} onClose={closeSidebar}>
<CartSidebarView />
</Sidebar>
@ -79,6 +71,7 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
{modalView === 'SIGNUP_VIEW' && <SignUpView />}
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
</Modal>
<FeatureBar
title="This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy."
hide={acceptedCookies}
@ -88,10 +81,6 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
</Button>
}
/>
{/* <Toast open={displayToast} onClose={closeModal}>
{toastText}
</Toast> */}
</div>
</CommerceProvider>
)

View File

@ -1,3 +1,7 @@
.root {
@apply sticky top-0 bg-primary z-40 transition-all duration-150;
}
.link {
@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 s from './Navbar.module.css'
import { Logo } from '@components/ui'
import { Logo, Container } from '@components/ui'
import { Searchbar, UserNav } from '@components/common'
interface Props {
className?: string
}
import cn from 'classnames'
import throttle from 'lodash.throttle'
const Navbar: FC<Props> = ({ className }) => {
const rootClassName = className
const Navbar: FC = () => {
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 (
<div className={rootClassName}>
<div className="flex justify-between align-center flex-row py-4 md:py-6 relative">
<div className="flex flex-1 items-center">
<Link href="/">
<a className={s.logo} aria-label="Logo">
<Logo />
</a>
</Link>
<nav className="space-x-4 ml-6 hidden lg:block">
<div className={cn(s.root, { 'shadow-magical': hasScrolled })}>
<Container>
<div className="flex justify-between align-center flex-row py-4 md:py-6 relative">
<div className="flex flex-1 items-center">
<Link href="/">
<a className={s.link}>All</a>
<a className={s.logo} aria-label="Logo">
<Logo />
</a>
</Link>
<Link href="/search?q=clothes">
<a className={s.link}>Clothes</a>
</Link>
<Link href="/search?q=accessories">
<a className={s.link}>Accessories</a>
</Link>
</nav>
<nav className="space-x-4 ml-6 hidden lg:block">
<Link href="/search">
<a className={s.link}>All</a>
</Link>
<Link href="/search?q=clothes">
<a className={s.link}>Clothes</a>
</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 className="flex-1 justify-center hidden lg:flex">
<Searchbar />
<div className="flex pb-4 lg:px-6 lg:hidden">
<Searchbar id="mobile-search" />
</div>
<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>
</Container>
</div>
)
}

View File

@ -1,4 +1,4 @@
import { FC, useEffect } from 'react'
import { FC, useEffect, useMemo } from 'react'
import cn from 'classnames'
import s from './Searchbar.module.css'
import { useRouter } from 'next/router'
@ -15,14 +15,17 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
router.prefetch('/search')
}, [])
return (
<div
className={cn(
'relative text-sm bg-accents-1 text-base w-full transition-colors duration-150',
className
)}
>
<label htmlFor={id}>
return useMemo(
() => (
<div
className={cn(
'relative text-sm bg-accents-1 text-base w-full transition-colors duration-150',
className
)}
>
<label className="hidden" htmlFor={id}>
Search
</label>
<input
id={id}
className={s.input}
@ -45,17 +48,18 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
}
}}
/>
</label>
<div className={s.iconContainer}>
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="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"
/>
</svg>
<div className={s.iconContainer}>
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="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"
/>
</svg>
</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 {
@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 {
@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 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 { Avatar } from '@components/common'
import { Moon, Sun } from '@components/icons'
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 {
open: boolean
open?: boolean
}
const LINKS = [
@ -29,68 +29,70 @@ const LINKS = [
]
const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
const { theme, setTheme } = useTheme()
const logout = useLogout()
const { pathname } = useRouter()
const { theme, setTheme } = useTheme()
const [display, setDisplay] = useState(false)
const { closeSidebarIfPresent } = useUI()
return (
<Transition
show={open}
enter="transition ease-out duration-150 z-20"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className={s.dropdownMenu}>
{LINKS.map(({ name, href }) => (
<Menu.Item key={href}>
<div>
<Link href={href}>
<a
className={cn(s.link, {
[s.active]: pathname === href,
})}
onClick={closeSidebarIfPresent}
>
{name}
</a>
</Link>
</div>
</Menu.Item>
))}
<Menu.Item>
<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>
<a
className={cn(s.link, 'border-t border-accents-2 mt-4')}
onClick={() => logout()}
>
Logout
</a>
</Menu.Item>
</Menu.Items>
</Transition>
<div>
<button
className={s.avatarButton}
onClick={() => setDisplay(!display)}
aria-label="Menu"
>
<Avatar />
</button>
{display && (
<ul className={s.dropdownMenu}>
{LINKS.map(({ name, href }) => (
<li key={href}>
<div>
<Link href={href}>
<a
className={cn(s.link, {
[s.active]: pathname === href,
})}
onClick={closeSidebarIfPresent}
>
{name}
</a>
</Link>
</div>
</li>
))}
<li>
<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>
</li>
<li>
<a
className={cn(s.link, 'border-t border-accents-2 mt-4')}
onClick={() => logout()}
>
Logout
</a>
</li>
</ul>
)}
</div>
)
}

View File

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

View File

@ -5,7 +5,5 @@ export { default as Layout } from './Layout'
export { default as Navbar } from './Navbar'
export { default as Searchbar } from './Searchbar'
export { default as UserNav } from './UserNav'
export { default as Toggle } from './Toggle'
export { default as Head } from './Head'
export { default as HTMLContent } from './HTMLContent'
export { default as I18nWidget } from './I18nWidget'

View File

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

View File

@ -1,4 +1,4 @@
const Sun = ({ ...props }) => {
const Github = ({ ...props }) => {
return (
<svg
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"
height="24"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shape-rendering="geometricPrecision"
shapeRendering="geometricPrecision"
{...props}
>
<circle cx="12" cy="12" r="10" fill="transparent" />

View File

@ -1,5 +1,7 @@
.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;
&:hover {
@ -67,24 +69,24 @@
.productTitle > span,
.productPrice,
.wishlistButton {
@apply transition ease-in-out duration-500;
@apply transition-colors ease-in-out duration-500;
}
.squareBg {
@apply transform absolute inset-0 z-0;
@apply transition-colors absolute inset-0 z-0;
background-color: #212529;
}
.squareBg:before {
@apply transition ease-in-out duration-500 bg-repeat-space w-full h-full block;
background-image: url('/bg-products.svg');
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 {
& .squareBg {
@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 {
@ -125,15 +127,15 @@
}
.imageContainer {
@apply flex items-center justify-center;
overflow: hidden;
& > div {
& > div {
height: 100%;
margin: 0 auto;
}
min-width: 100%;
}
}
.product-image {
height: 120% !important;
top: -10% !important;
.image {
object-fit: cover;
transform: scale(1.2);
}

View File

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

View File

@ -6,8 +6,7 @@ import { NextSeo } from 'next-seo'
import s from './ProductView.module.css'
import { useUI } from '@components/ui/context'
import { Swatch, ProductSlider } from '@components/product'
import { Button, Container } from '@components/ui'
import { HTMLContent } from '@components/common'
import { Button, Container, Text } from '@components/ui'
import usePrice from '@bigcommerce/storefront-data-hooks/use-price'
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">
<HTMLContent html={product.description} />
<Text html={product.description} />
</div>
</section>
<div>

View File

@ -6,7 +6,6 @@ import React, {
useRef,
} from 'react'
import mergeRefs from 'react-merge-refs'
import { useButton } from 'react-aria'
import s from './Button.module.css'
import { LoadingDots } from '@components/ui'
@ -34,19 +33,8 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
loading = false,
disabled = false,
style = {},
...rest
} = props
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(
s.root,
@ -63,8 +51,6 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
aria-pressed={active}
data-variant={variant}
ref={mergeRefs([ref, buttonRef])}
{...buttonProps}
data-active={isPressed ? '' : undefined}
className={rootClassName}
disabled={disabled}
style={{

View File

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

View File

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

View File

@ -1,11 +1,13 @@
import cn from 'classnames'
import { FC, useRef } from 'react'
import { FC, useRef, useEffect } from 'react'
import Portal from '@reach/portal'
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 {
disableBodyScroll,
enableBodyScroll,
clearAllBodyScrollLocks,
} from 'body-scroll-lock'
interface Props {
className?: string
children?: any
@ -13,64 +15,41 @@ interface Props {
onClose: () => void
}
const Modal: 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: false,
onClose: onClose,
...props,
},
ref
)
const Modal: FC<Props> = ({ children, open, onClose }) => {
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
useEffect(() => {
if (ref.current) {
if (open) {
disableBodyScroll(ref.current)
} else {
enableBodyScroll(ref.current)
}
}
return () => {
clearAllBodyScrollLocks()
}
}, [open])
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.modal}
{...overlayProps}
{...dialogProps}
{...modalProps}
ref={ref}
<Portal>
{open ? (
<div className={s.root} ref={ref}>
<div className={s.modal}>
<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"
>
<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>
<Cross className="h-6 w-6" />
</button>
</div>
{children}
</div>
</FocusScope>
</OverlayContainer>
</Transition>
</div>
) : null}
</Portal>
)
}

View File

@ -1,79 +1,54 @@
import cn from 'classnames'
import { FC, useRef } from 'react'
import s from './Sidebar.module.css'
import { Transition } from '@headlessui/react'
import { useOverlay, useModal, OverlayContainer } from '@react-aria/overlays'
import { useDialog } from '@react-aria/dialog'
import { FocusScope } from '@react-aria/focus'
import Portal from '@reach/portal'
import { FC, useEffect, useRef } from 'react'
import {
disableBodyScroll,
enableBodyScroll,
clearAllBodyScrollLocks,
} from 'body-scroll-lock'
interface Props {
className?: string
children?: any
open?: boolean
children: any
open: boolean
onClose: () => void
}
const Sidebar: FC<Props> = ({ className, children, open = false, onClose }) => {
const rootClassName = cn(s.root, className)
const ref = useRef<HTMLDivElement>(null)
const { modalProps } = useModal()
const { overlayProps } = useOverlay(
{
isOpen: open,
isDismissable: true,
onClose: onClose,
},
ref
)
const { dialogProps } = useDialog({}, ref)
const Sidebar: FC<Props> = ({ children, open = false, onClose }) => {
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
useEffect(() => {
if (ref.current) {
if (open) {
disableBodyScroll(ref.current)
} else {
enableBodyScroll(ref.current)
}
}
return () => {
clearAllBodyScrollLocks()
}
}, [open])
return (
<Transition show={open}>
<OverlayContainer>
<FocusScope contain restoreFocus autoFocus>
<div className={rootClassName}>
<div className="absolute inset-0 overflow-hidden">
<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="absolute inset-0 bg-black bg-opacity-50 transition-opacity"
// 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>
<Portal>
{open ? (
<div className={s.root} ref={ref}>
<div className="absolute inset-0 overflow-hidden">
<div
className="absolute inset-0 bg-black bg-opacity-50 transition-opacity"
onClick={onClose}
/>
<section className="absolute inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16 outline-none">
<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>
</section>
</div>
</FocusScope>
</OverlayContainer>
</Transition>
</div>
) : null}
</Portal>
)
}

View File

@ -10,7 +10,8 @@ interface Props {
variant?: Variant
className?: string
style?: CSSProperties
children: React.ReactNode | any
children?: React.ReactNode | any
html?: string
}
type Variant = 'heading' | 'body' | 'pageHeading' | 'sectionHeading'
@ -20,6 +21,7 @@ const Text: FunctionComponent<Props> = ({
className = '',
variant = 'body',
children,
html,
}) => {
const componentsMap: {
[P in Variant]: React.ComponentType<any> | string
@ -36,6 +38,12 @@ const Text: FunctionComponent<Props> = ({
| React.ComponentType<any>
| string = componentsMap![variant!]
const htmlContentProps = html
? {
dangerouslySetInnerHTML: { __html: html },
}
: {}
return (
<Component
className={cn(
@ -49,6 +57,7 @@ const Text: FunctionComponent<Props> = ({
className
)}
style={style}
{...htmlContentProps}
>
{children}
</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 { ThemeProvider } from 'next-themes'
import { SSRProvider, OverlayProvider } from 'react-aria'
export interface State {
displaySidebar: boolean
@ -181,10 +180,6 @@ export const useUI = () => {
export const ManagedUIContext: FC = ({ children }) => (
<UIProvider>
<ThemeProvider>
<SSRProvider>
<OverlayProvider>{children}</OverlayProvider>
</SSRProvider>
</ThemeProvider>
<ThemeProvider>{children}</ThemeProvider>
</UIProvider>
)

View File

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

@ -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 useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item'
import { useUI } from '@components/ui/context'
import { Button } from '@components/ui'
import { HTMLContent } from '@components/common'
import { Button, Text } from '@components/ui'
import { Trash } from '@components/icons'
import s from './WishlistCard.module.css'
@ -72,7 +71,7 @@ const WishlistCard: FC<Props> = ({ item }) => {
</Link>
</h3>
<div className="mb-4">
<HTMLContent html={product.description!} />
<Text html={product.description} />
</div>
<Button
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: {
sizes: [320, 480, 820, 1200, 1600],
domains: ['cdn11.bigcommerce.com'],
},
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": {
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next start",
"analyze": "BUNDLE_ANALYZE=both yarn build",
"find:unused": "next-unused"
},
"prettier": {
"semi": false,
"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": {
"@bigcommerce/storefront-data-hooks": "^1.0.2",
"@headlessui/react": "^0.2.0",
"@react-aria/overlays": "^3.4.0",
"@reach/portal": "^0.11.2",
"@tailwindcss/ui": "^0.6.2",
"@types/body-scroll-lock": "^2.6.1",
"@types/lodash.throttle": "^4.1.6",
"@vercel/fetch": "^6.1.0",
"body-scroll-lock": "^3.1.5",
"bowser": "^2.11.0",
"classnames": "^2.2.6",
"email-validator": "^2.0.4",
"js-cookie": "^2.2.1",
"keen-slider": "^5.2.4",
"lodash.debounce": "^4.0.8",
"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-themes": "^0.0.4",
"postcss-nesting": "^7.0.1",
"react": "^16.14.0",
"react-aria": "^3.0.0",
"react-dom": "^16.14.0",
"react-intersection-observer": "^8.30.1",
"react-merge-refs": "^1.1.0",
"react-ticker": "^1.2.2",
"tailwindcss": "^1.9"
},
"devDependencies": {
"@next/bundle-analyzer": "^10.0.1",
"@types/bunyan": "^1.8.6",
"@types/bunyan-prettystream": "^0.1.31",
"@types/classnames": "^2.2.10",
"@types/js-cookie": "^2.2.6",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.random": "^3.2.6",
"@types/node": "^14.11.2",
"@types/react": "^16.9.49",
"bunyan": "^1.8.14",
"bunyan-prettystream": "^0.1.3",
"next-unused": "^0.0.3",
"postcss-flexbugs-fixes": "^4.2.1",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.1.2",

View File

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

View File

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

View File

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

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 { Layout } from '@components/common'
import { Heart } from '@components/icons'
import { Container, Text } from '@components/ui'
import { Text, Container } from '@components/ui'
import { WishlistCard } from '@components/wishlist'
import { Transition } from '@headlessui/react'
import { defatultPageProps } from '@lib/defaults'
export async function getStaticProps({
preview,
@ -15,7 +15,7 @@ export async function getStaticProps({
const config = getConfig({ locale })
const { pages } = await getAllPages({ config, preview })
return {
props: { pages },
props: { ...defatultPageProps, pages },
}
}
@ -28,45 +28,22 @@ export default function Wishlist() {
<Text variant="pageHeading">My Wishlist</Text>
<div className="group flex flex-col">
{isEmpty ? (
<Transition show>
<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="flex-1 px-12 py-24 flex flex-col justify-center items-center ">
<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">
<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>
<div className="flex-1 px-12 py-24 flex flex-col justify-center items-center ">
<span className="border border-dashed border-secondary flex items-center justify-center w-16 h-16 bg-primary p-12 rounded-lg text-primary">
<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 show>
{data &&
data.items?.map((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>
data &&
data.items?.map((item) => (
<WishlistCard key={item.id} item={item} />
))
)}
</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 = {
future: {
purgeLayersByDefault: true,
},
purge: {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',

3397
yarn.lock

File diff suppressed because it is too large Load Diff