mirror of
https://github.com/vercel/commerce.git
synced 2025-07-22 20:26:49 +00:00
Renaming to common
This commit is contained in:
25
components/common/Avatar/Avatar.tsx
Normal file
25
components/common/Avatar/Avatar.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import cn from 'classnames'
|
||||
import { FC, useState } from 'react'
|
||||
import { getRandomPairOfColors } from '@lib/colors'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any
|
||||
}
|
||||
|
||||
const Avatar: FC<Props> = ({}) => {
|
||||
const [bg] = useState(getRandomPairOfColors)
|
||||
|
||||
return (
|
||||
<div
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
export default Avatar
|
1
components/common/Avatar/index.ts
Normal file
1
components/common/Avatar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Avatar'
|
42
components/common/EnhancedImage/EnhancedImage.tsx
Normal file
42
components/common/EnhancedImage/EnhancedImage.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { FC } from 'react'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
import Image from 'next/image'
|
||||
|
||||
type Props = Omit<
|
||||
JSX.IntrinsicElements['img'],
|
||||
'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading'
|
||||
> & {
|
||||
src: string
|
||||
quality?: string
|
||||
priority?: boolean
|
||||
loading?: readonly ['lazy', 'eager', undefined]
|
||||
unoptimized?: boolean
|
||||
} & (
|
||||
| {
|
||||
width: number | string
|
||||
height: number | string
|
||||
unsized?: false
|
||||
}
|
||||
| {
|
||||
width?: number | string
|
||||
height?: number | string
|
||||
unsized: true
|
||||
}
|
||||
)
|
||||
|
||||
const EnhancedImage: FC<Props & JSX.IntrinsicElements['img']> = ({
|
||||
...props
|
||||
}) => {
|
||||
const [ref] = useInView({
|
||||
triggerOnce: true,
|
||||
rootMargin: '220px 0px',
|
||||
})
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Image {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EnhancedImage
|
1
components/common/EnhancedImage/index.ts
Normal file
1
components/common/EnhancedImage/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './EnhancedImage'
|
7
components/common/Featurebar/Featurebar.module.css
Normal file
7
components/common/Featurebar/Featurebar.module.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.root {
|
||||
@apply text-center p-6 bg-primary text-sm flex-row justify-center items-center font-medium fixed bottom-0 w-full z-30;
|
||||
|
||||
@screen md {
|
||||
@apply flex text-left;
|
||||
}
|
||||
}
|
39
components/common/Featurebar/Featurebar.tsx
Normal file
39
components/common/Featurebar/Featurebar.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import cn from 'classnames'
|
||||
import { FC } from 'react'
|
||||
|
||||
import s from './Featurebar.module.css'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
title: string
|
||||
description?: string
|
||||
hide?: boolean
|
||||
action?: React.ReactNode
|
||||
}
|
||||
|
||||
const Featurebar: FC<Props> = ({
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
action,
|
||||
hide,
|
||||
}) => {
|
||||
const rootClassName = cn(
|
||||
s.root,
|
||||
{
|
||||
'transition-transform transform duration-500 ease-out translate-y-full': hide,
|
||||
},
|
||||
className
|
||||
)
|
||||
return (
|
||||
<div className={rootClassName}>
|
||||
<span className="block md:inline">{title}</span>
|
||||
<span className="block mb-6 md:inline md:mb-0 md:ml-2">
|
||||
{description}
|
||||
</span>
|
||||
{action && action}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Featurebar
|
1
components/common/Featurebar/index.ts
Normal file
1
components/common/Featurebar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Featurebar'
|
9
components/common/Footer/Footer.module.css
Normal file
9
components/common/Footer/Footer.module.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.link {
|
||||
& > svg {
|
||||
@apply transform duration-75 ease-linear;
|
||||
}
|
||||
|
||||
&:hover > svg {
|
||||
@apply scale-110;
|
||||
}
|
||||
}
|
150
components/common/Footer/Footer.tsx
Normal file
150
components/common/Footer/Footer.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { FC } from 'react'
|
||||
import cn from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import type { Page } from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
|
||||
import getSlug from '@lib/get-slug'
|
||||
import { Github } from '@components/icons'
|
||||
import { Logo, Container } from '@components/ui'
|
||||
import { I18nWidget } from '@components/core'
|
||||
import s from './Footer.module.css'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any
|
||||
pages?: Page[]
|
||||
}
|
||||
|
||||
const LEGAL_PAGES = ['terms-of-use', 'shipping-returns', 'privacy-policy']
|
||||
|
||||
const Footer: FC<Props> = ({ className, pages }) => {
|
||||
const { sitePages, legalPages } = usePages(pages)
|
||||
const rootClassName = cn(className)
|
||||
|
||||
return (
|
||||
<footer className={rootClassName}>
|
||||
<Container>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 border-b border-accents-2 py-12 text-primary bg-primary transition-colors duration-150">
|
||||
<div className="col-span-1 lg:col-span-2">
|
||||
<Link href="/">
|
||||
<a className="flex flex-initial items-center font-bold md:mr-24">
|
||||
<span className="rounded-full border border-gray-700 mr-2">
|
||||
<Logo />
|
||||
</span>
|
||||
<span>ACME</span>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-span-1 lg:col-span-2">
|
||||
<ul className="flex flex-initial flex-col md:flex-1">
|
||||
<li className="py-3 md:py-0 md:pb-4">
|
||||
<Link href="/">
|
||||
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
|
||||
Home
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="py-3 md:py-0 md:pb-4">
|
||||
<Link href="/">
|
||||
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
|
||||
Careers
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="py-3 md:py-0 md:pb-4">
|
||||
<Link href="/blog">
|
||||
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
|
||||
Blog
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
{sitePages.map((page) => (
|
||||
<li key={page.url} className="py-3 md:py-0 md:pb-4">
|
||||
<Link href={page.url!}>
|
||||
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
|
||||
{page.name}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="col-span-1 lg:col-span-2">
|
||||
<ul className="flex flex-initial flex-col md:flex-1">
|
||||
{legalPages.map((page) => (
|
||||
<li key={page.url} className="py-3 md:py-0 md:pb-4">
|
||||
<Link href={page.url!}>
|
||||
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
|
||||
{page.name}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</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}>
|
||||
<Github />
|
||||
</a>
|
||||
<I18nWidget />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-12 flex flex-col md:flex-row justify-between items-center space-y-4">
|
||||
<div>
|
||||
<span>© 2020 ACME, Inc. All rights reserved.</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-primary">Crafted by</span>
|
||||
<a href="https://vercel.com" aria-label="Vercel.com Link">
|
||||
<img
|
||||
src="/vercel.svg"
|
||||
alt="Vercel.com Logo"
|
||||
className="inline-block h-6 ml-4 text-primary"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
function usePages(pages?: Page[]) {
|
||||
const { locale } = useRouter()
|
||||
const sitePages: Page[] = []
|
||||
const legalPages: Page[] = []
|
||||
|
||||
if (pages) {
|
||||
pages.forEach((page) => {
|
||||
const slug = page.url && getSlug(page.url)
|
||||
|
||||
if (!slug) return
|
||||
if (locale && !slug.startsWith(`${locale}/`)) return
|
||||
|
||||
if (isLegalPage(slug, locale)) {
|
||||
legalPages.push(page)
|
||||
} else {
|
||||
sitePages.push(page)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
sitePages: sitePages.sort(bySortOrder),
|
||||
legalPages: legalPages.sort(bySortOrder),
|
||||
}
|
||||
}
|
||||
|
||||
const isLegalPage = (slug: string, locale?: string) =>
|
||||
locale
|
||||
? LEGAL_PAGES.some((p) => `${locale}/${p}` === slug)
|
||||
: LEGAL_PAGES.includes(slug)
|
||||
|
||||
// Sort pages by the sort order assigned in the BC dashboard
|
||||
function bySortOrder(a: Page, b: Page) {
|
||||
return (a.sort_order ?? 0) - (b.sort_order ?? 0)
|
||||
}
|
||||
|
||||
export default Footer
|
1
components/common/Footer/index.ts
Normal file
1
components/common/Footer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Footer'
|
26
components/common/HTMLContent/HTMLContent.module.css
Normal file
26
components/common/HTMLContent/HTMLContent.module.css
Normal file
@@ -0,0 +1,26 @@
|
||||
.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;
|
||||
}
|
16
components/common/HTMLContent/HTMLContent.tsx
Normal file
16
components/common/HTMLContent/HTMLContent.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
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 }}
|
||||
/>
|
||||
)
|
||||
}
|
1
components/common/HTMLContent/index.ts
Normal file
1
components/common/HTMLContent/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './HTMLContent'
|
18
components/common/Head/Head.tsx
Normal file
18
components/common/Head/Head.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { FC } from 'react'
|
||||
import NextHead from 'next/head'
|
||||
import { DefaultSeo } from 'next-seo'
|
||||
import config from '@config/seo.json'
|
||||
|
||||
const Head: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<DefaultSeo {...config} />
|
||||
<NextHead>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="manifest" href="/site.webmanifest" key="site-manifest" />
|
||||
</NextHead>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Head
|
1
components/common/Head/index.ts
Normal file
1
components/common/Head/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Head'
|
@@ -0,0 +1,23 @@
|
||||
.root {
|
||||
@apply py-12 flex flex-col w-full px-6;
|
||||
|
||||
@screen md {
|
||||
@apply flex-row;
|
||||
}
|
||||
|
||||
& .asideWrapper {
|
||||
@apply pr-3 w-full relative;
|
||||
|
||||
@screen md {
|
||||
@apply w-48;
|
||||
}
|
||||
}
|
||||
|
||||
& .aside {
|
||||
@apply flex flex-row w-full justify-around mb-12;
|
||||
|
||||
@screen md {
|
||||
@apply mb-0 block sticky top-32;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
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'
|
||||
|
||||
interface Props {
|
||||
categories?: any
|
||||
brands?: any
|
||||
newestProducts?: any
|
||||
}
|
||||
|
||||
const Head: FC<Props> = ({ categories, brands, newestProducts }) => {
|
||||
return (
|
||||
<div className={s.root}>
|
||||
<div className={s.asideWrapper}>
|
||||
<div className={s.aside}>
|
||||
<ul className="mb-10">
|
||||
<li className="py-1 text-base font-bold tracking-wide">
|
||||
<Link href={getCategoryPath('')}>
|
||||
<a>All Categories</a>
|
||||
</Link>
|
||||
</li>
|
||||
{categories.map((cat: any) => (
|
||||
<li key={cat.path} className="py-1 text-accents-8">
|
||||
<Link href={getCategoryPath(cat.path)}>
|
||||
<a>{cat.name}</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ul className="">
|
||||
<li className="py-1 text-base font-bold tracking-wide">
|
||||
<Link href={getDesignerPath('')}>
|
||||
<a>All Designers</a>
|
||||
</Link>
|
||||
</li>
|
||||
{brands.flatMap(({ node }: any) => (
|
||||
<li key={node.path} className="py-1 text-accents-8">
|
||||
<Link href={getDesignerPath(node.path)}>
|
||||
<a>{node.name}</a>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Grid layout="normal">
|
||||
{newestProducts.map(({ node }: any) => (
|
||||
<ProductCard
|
||||
key={node.path}
|
||||
product={node}
|
||||
variant="simple"
|
||||
imgWidth={480}
|
||||
imgHeight={480}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Head
|
1
components/common/HomeAllProductsGrid/index.ts
Normal file
1
components/common/HomeAllProductsGrid/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './HomeAllProductsGrid'
|
28
components/common/I18nWidget/I18nWidget.module.css
Normal file
28
components/common/I18nWidget/I18nWidget.module.css
Normal file
@@ -0,0 +1,28 @@
|
||||
.root {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply h-10 px-2 rounded-md border border-accents-2 flex items-center justify-center;
|
||||
}
|
||||
|
||||
.button:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
.dropdownMenu {
|
||||
@apply fixed right-0 top-12 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;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
@apply flex cursor-pointer px-6 py-3 flex transition ease-in-out duration-150 text-primary leading-6 font-medium items-center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
@apply bg-accents-1;
|
||||
}
|
82
components/common/I18nWidget/I18nWidget.tsx
Normal file
82
components/common/I18nWidget/I18nWidget.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
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 s from './I18nWidget.module.css'
|
||||
|
||||
interface LOCALE_DATA {
|
||||
name: string
|
||||
img: {
|
||||
filename: string
|
||||
alt: string
|
||||
}
|
||||
}
|
||||
|
||||
const LOCALES_MAP: Record<string, LOCALE_DATA> = {
|
||||
es: {
|
||||
name: 'Español',
|
||||
img: {
|
||||
filename: 'flag-es-co.svg',
|
||||
alt: 'Bandera Colombiana',
|
||||
},
|
||||
},
|
||||
'en-US': {
|
||||
name: 'English',
|
||||
img: {
|
||||
filename: 'flag-en-us.svg',
|
||||
alt: 'US Flag',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const I18nWidget: FC = () => {
|
||||
const {
|
||||
locale,
|
||||
locales,
|
||||
defaultLocale = 'en-US',
|
||||
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 }) => (
|
||||
<Link href={currentPath} locale={locale}>
|
||||
<a className={cn(s.item, { [s.active]: active })}>
|
||||
{LOCALES_MAP[locale].name}
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
) : null}
|
||||
</Menu>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default I18nWidget
|
1
components/common/I18nWidget/index.ts
Normal file
1
components/common/I18nWidget/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './I18nWidget'
|
4
components/common/Layout/Layout.module.css
Normal file
4
components/common/Layout/Layout.module.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.root {
|
||||
@apply h-full bg-primary mx-auto transition-colors duration-150;
|
||||
max-width: 2460px;
|
||||
}
|
96
components/common/Layout/Layout.tsx
Normal file
96
components/common/Layout/Layout.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
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 { CartSidebarView } from '@components/cart'
|
||||
import { Container, Sidebar, Button, Modal, Toast } from '@components/ui'
|
||||
import { Navbar, Featurebar, Footer } from '@components/core'
|
||||
import { LoginView, SignUpView, ForgotPassword } from '@components/auth'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { usePreventScroll } from '@react-aria/overlays'
|
||||
import s from './Layout.module.css'
|
||||
import debounce from 'lodash.debounce'
|
||||
interface Props {
|
||||
pageProps: {
|
||||
pages?: Page[]
|
||||
}
|
||||
}
|
||||
|
||||
const Layout: FC<Props> = ({ children, pageProps }) => {
|
||||
const {
|
||||
displaySidebar,
|
||||
displayModal,
|
||||
closeSidebar,
|
||||
closeModal,
|
||||
modalView,
|
||||
toastText,
|
||||
closeToast,
|
||||
displayToast,
|
||||
} = useUI()
|
||||
const [acceptedCookies, setAcceptedCookies] = useState(false)
|
||||
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
|
||||
if (scrollTop > offset) setHasScrolled(true)
|
||||
else setHasScrolled(false)
|
||||
}, 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>
|
||||
<main className="fit">{children}</main>
|
||||
<Footer pages={pageProps.pages} />
|
||||
<Sidebar open={displaySidebar} onClose={closeSidebar}>
|
||||
<CartSidebarView />
|
||||
</Sidebar>
|
||||
|
||||
<Modal open={displayModal} onClose={closeModal}>
|
||||
{modalView === 'LOGIN_VIEW' && <LoginView />}
|
||||
{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}
|
||||
action={
|
||||
<Button className="mx-5" onClick={() => setAcceptedCookies(true)}>
|
||||
Accept cookies
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{/* <Toast open={displayToast} onClose={closeModal}>
|
||||
{toastText}
|
||||
</Toast> */}
|
||||
</div>
|
||||
</CommerceProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
1
components/common/Layout/index.ts
Normal file
1
components/common/Layout/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Layout'
|
20
components/common/Navbar/Navbar.module.css
Normal file
20
components/common/Navbar/Navbar.module.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.link {
|
||||
@apply inline-flex items-center text-primary leading-6 font-medium transition ease-in-out duration-75 cursor-pointer text-accents-6;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
@apply text-accents-9;
|
||||
}
|
||||
|
||||
.link:focus {
|
||||
@apply outline-none text-accents-8;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@apply cursor-pointer rounded-full border transform duration-100 ease-in-out;
|
||||
|
||||
&:hover {
|
||||
@apply shadow-md;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
51
components/common/Navbar/Navbar.tsx
Normal file
51
components/common/Navbar/Navbar.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import s from './Navbar.module.css'
|
||||
import { Logo } from '@components/ui'
|
||||
import { Searchbar, UserNav } from '@components/core'
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Navbar: FC<Props> = ({ className }) => {
|
||||
const rootClassName = className
|
||||
|
||||
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">
|
||||
<Link href="/">
|
||||
<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 pb-4 lg:px-6 lg:hidden">
|
||||
<Searchbar id="mobileSearch" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navbar
|
1
components/common/Navbar/index.ts
Normal file
1
components/common/Navbar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Navbar'
|
19
components/common/Searchbar/Searchbar.module.css
Normal file
19
components/common/Searchbar/Searchbar.module.css
Normal file
@@ -0,0 +1,19 @@
|
||||
.input {
|
||||
@apply bg-transparent px-3 py-2 appearance-none w-full transition duration-150 ease-in-out pr-10;
|
||||
|
||||
@screen sm {
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
@apply outline-none shadow-outline-2;
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
@apply absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@apply h-5 w-5;
|
||||
}
|
62
components/common/Searchbar/Searchbar.tsx
Normal file
62
components/common/Searchbar/Searchbar.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { FC, useEffect } from 'react'
|
||||
import cn from 'classnames'
|
||||
import s from './Searchbar.module.css'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
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}>
|
||||
<input
|
||||
id={id}
|
||||
className={s.input}
|
||||
placeholder="Search for products..."
|
||||
defaultValue={router.query.q}
|
||||
onKeyUp={(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
const q = e.currentTarget.value
|
||||
|
||||
router.push(
|
||||
{
|
||||
pathname: `/search`,
|
||||
query: q ? { q } : {},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Searchbar
|
1
components/common/Searchbar/index.ts
Normal file
1
components/common/Searchbar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Searchbar'
|
2
components/common/Toggle/Toggle.module.css
Normal file
2
components/common/Toggle/Toggle.module.css
Normal file
@@ -0,0 +1,2 @@
|
||||
.root {
|
||||
}
|
55
components/common/Toggle/Toggle.tsx
Normal file
55
components/common/Toggle/Toggle.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
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
|
1
components/common/Toggle/index.ts
Normal file
1
components/common/Toggle/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Toggle'
|
24
components/common/UserNav/DropdownMenu.module.css
Normal file
24
components/common/UserNav/DropdownMenu.module.css
Normal file
@@ -0,0 +1,24 @@
|
||||
.dropdownMenu {
|
||||
@apply fixed right-0 mt-7 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;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply text-primary flex cursor-pointer px-6 py-3 flex transition ease-in-out duration-150 leading-6 font-medium items-center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
@apply bg-accents-1;
|
||||
}
|
||||
|
||||
.link.active {
|
||||
@apply font-bold bg-accents-2;
|
||||
}
|
||||
|
||||
.off {
|
||||
@apply hidden;
|
||||
}
|
97
components/common/UserNav/DropdownMenu.tsx
Normal file
97
components/common/UserNav/DropdownMenu.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useTheme } from 'next-themes'
|
||||
import cn from 'classnames'
|
||||
import s from './DropdownMenu.module.css'
|
||||
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'
|
||||
|
||||
interface DropdownMenuProps {
|
||||
open: boolean
|
||||
}
|
||||
|
||||
const LINKS = [
|
||||
{
|
||||
name: 'My Orders',
|
||||
href: '/orders',
|
||||
},
|
||||
{
|
||||
name: 'My Profile',
|
||||
href: '/profile',
|
||||
},
|
||||
{
|
||||
name: 'My Cart',
|
||||
href: '/cart',
|
||||
},
|
||||
]
|
||||
|
||||
const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
|
||||
const { theme, setTheme } = useTheme()
|
||||
const logout = useLogout()
|
||||
const { pathname } = useRouter()
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
export default DropdownMenu
|
36
components/common/UserNav/UserNav.module.css
Normal file
36
components/common/UserNav/UserNav.module.css
Normal file
@@ -0,0 +1,36 @@
|
||||
.root {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.list {
|
||||
@apply flex flex-row items-center justify-items-end h-full;
|
||||
}
|
||||
|
||||
.item {
|
||||
@apply mr-6 cursor-pointer relative transition ease-in-out duration-100 flex items-center outline-none text-primary;
|
||||
|
||||
&:hover {
|
||||
@apply text-accents-6 transition scale-110 duration-100;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@apply mr-0;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
@apply outline-none;
|
||||
}
|
||||
}
|
||||
|
||||
.bagCount {
|
||||
@apply border border-accents-1 bg-secondary text-secondary h-4 w-4 absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs;
|
||||
}
|
||||
|
||||
.avatarButton {
|
||||
@apply inline-flex justify-center rounded-full;
|
||||
}
|
||||
|
||||
.avatarButton:focus {
|
||||
@apply outline-none;
|
||||
}
|
70
components/common/UserNav/UserNav.tsx
Normal file
70
components/common/UserNav/UserNav.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { FC } from 'react'
|
||||
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/core'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import DropdownMenu from './DropdownMenu'
|
||||
import s from './UserNav.module.css'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const countItem = (count: number, item: any) => count + item.quantity
|
||||
const countItems = (count: number, items: any[]) =>
|
||||
items.reduce(countItem, count)
|
||||
|
||||
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}>
|
||||
<ul className={s.list}>
|
||||
<li className={s.item} onClick={toggleSidebar}>
|
||||
<Bag />
|
||||
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
||||
</li>
|
||||
<li className={s.item}>
|
||||
<Link href="/wishlist">
|
||||
<a onClick={closeSidebarIfPresent}>
|
||||
<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>
|
||||
) : (
|
||||
<button
|
||||
className={s.avatarButton}
|
||||
aria-label="Menu"
|
||||
onClick={() => openModal()}
|
||||
>
|
||||
<Avatar />
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserNav
|
1
components/common/UserNav/index.ts
Normal file
1
components/common/UserNav/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './UserNav'
|
12
components/common/index.ts
Normal file
12
components/common/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export { default as Avatar } from './Avatar'
|
||||
export { default as Featurebar } from './Featurebar'
|
||||
export { default as Footer } from './Footer'
|
||||
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'
|
||||
export { default as EnhancedImage } from './EnhancedImage'
|
Reference in New Issue
Block a user