Monorepo with Turborepo (#651)

* Moved everything

* Figuring out how to make imports work

* Updated exports

* Added missing exports

* Added @vercel/commerce-local to `site`

* Updated commerce config

* Updated exports and commerce config

* Updated commerce hoc

* Fixed exports in local

* Added publish config

* Updated imports in site

* It's actually working

* Don't use debugger in dev for better speeds

* Improved DX when editing packages

* Set up eslint with husky

* Updated prettier config

* Added prettier setup to every package

* Moved bigcommerce

* Moved Bigcommerce to src and package updates

* Updated setup of bigcommerce

* Moved definitions script

* Moved commercejs

* Move to src

* Fixed types in commercejs

* Moved kibocommerce

* Moved kibocommerce to src

* Added package/tsconfig to kibocommerce

* Fixed imports and other things

* Moved ordercloud

* Moved ordercloud to src

* Fixed imports

* Added missing prettier files

* Moved Saleor

* Moved Saleor to src

* Fixed imports

* Replaced all imports to @commerce

* Added prettierignore/rc to all providers

* Moved shopify to src

* Build shopify in packages

* Moved Spree

* Moved spree to src

* Updated spree

* Moved swell

* Moved swell to src

* Fixed type imports in swell

* Moved Vendure to packages

* Moved vendure to src

* Fixed imports in vendure

* Added codegen to saleor

* Updated codegen setup for shopify

* Added codegen to vendure

* Added codegen to kibocommerce

* Added all packages to site's deps

* Updated codegen setup in bigcommerce

* Minor fixes

* Updated providers' names in site

* Updated packages based on Bel's changes

* Updated turbo to latest

* Fixed ts complains

* Set npm engine in root

* New lockfile install

* remove engines

* Regen lockfile

* Switched from npm to yarn

* Updated typesVersions in all packages

* Moved dep

* Updated SWR to the just released 1.2.0

* Removed "isolatedModules" from packages

* Updated list of providers and default

* Updated swell declaration

* Removed next import from kibocommerce

* Added COMMERCE_PROVIDER log

* Added another log

* Updated turbo config

* Updated docs

* Removed test logs

Co-authored-by: Jared Palmer <jared@jaredpalmer.com>
This commit is contained in:
Luis Alvarez D
2022-02-01 14:14:05 -05:00
committed by GitHub
parent d0ef346189
commit 0afe686fe9
1326 changed files with 9109 additions and 19494 deletions

View File

@@ -0,0 +1,24 @@
import { FC, useRef, useEffect } from 'react'
import { useUserAvatar } from '@lib/hooks/useUserAvatar'
interface Props {
className?: string
children?: any
}
const Avatar: FC<Props> = ({}) => {
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
let { userAvatar } = useUserAvatar()
return (
<div
ref={ref}
style={{ backgroundImage: userAvatar }}
className="inline-block h-8 w-8 rounded-full border-2 border-primary hover:border-secondary focus:border-secondary transition-colors ease-linear"
>
{/* Add an image - We're generating a gradient as placeholder <img></img> */}
</div>
)
}
export default Avatar

View File

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

View File

@@ -0,0 +1,6 @@
.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 transition-all duration-300 ease-out
md:flex md:text-left;
}

View File

@@ -0,0 +1,39 @@
import cn from 'classnames'
import s from './FeatureBar.module.css'
interface FeatureBarProps {
className?: string
title: string
description?: string
hide?: boolean
action?: React.ReactNode
}
const FeatureBar: React.FC<FeatureBarProps> = ({
title,
description,
className,
action,
hide,
}) => {
const rootClassName = cn(
s.root,
{
transform: true,
'translate-y-0 opacity-100': !hide,
'translate-y-full opacity-0': 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

View File

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

View File

@@ -0,0 +1,13 @@
.root {
@apply border-t border-accent-2;
}
.link {
& > svg {
@apply transform duration-75 ease-linear;
}
&:hover > svg {
@apply scale-110;
}
}

View File

@@ -0,0 +1,117 @@
import { FC } from 'react'
import cn from 'classnames'
import Link from 'next/link'
import { useRouter } from 'next/router'
import type { Page } from '@commerce/types/page'
import getSlug from '@lib/get-slug'
import { Github, Vercel } from '@components/icons'
import { Logo, Container } from '@components/ui'
import { I18nWidget } from '@components/common'
import s from './Footer.module.css'
interface Props {
className?: string
children?: any
pages?: Page[]
}
const links = [
{
name: 'Home',
url: '/',
},
]
const Footer: FC<Props> = ({ className, pages }) => {
const { sitePages } = usePages(pages)
const rootClassName = cn(s.root, className)
return (
<footer className={rootClassName}>
<Container>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 border-b border-accent-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-accent-6 mr-2">
<Logo />
</span>
<span>ACME</span>
</a>
</Link>
</div>
<div className="col-span-1 lg:col-span-8">
<div className="grid md:grid-rows-4 md:grid-cols-3 md:grid-flow-col">
{[...links, ...sitePages].map((page) => (
<span key={page.url} className="py-3 md:py-0 md:pb-4">
<Link href={page.url!}>
<a className="text-accent-9 hover:text-accent-6 transition ease-in-out duration-150">
{page.name}
</a>
</Link>
</span>
))}
</div>
</div>
<div className="col-span-1 lg:col-span-2 flex items-start lg:justify-end text-primary">
<div className="flex space-x-6 items-center h-10">
<a
className={s.link}
aria-label="Github Repository"
href="https://github.com/vercel/commerce"
>
<Github />
</a>
<I18nWidget />
</div>
</div>
</div>
<div className="pt-6 pb-10 flex flex-col md:flex-row justify-between items-center space-y-4 text-accent-6 text-sm">
<div>
<span>&copy; 2020 ACME, Inc. All rights reserved.</span>
</div>
<div className="flex items-center text-primary text-sm">
<span className="text-primary">Created by</span>
<a
rel="noopener noreferrer"
href="https://vercel.com"
aria-label="Vercel.com Link"
target="_blank"
className="text-primary"
>
<Vercel
className="inline-block h-6 ml-3 text-primary"
alt="Vercel.com Logo"
/>
</a>
</div>
</div>
</Container>
</footer>
)
}
function usePages(pages?: Page[]) {
const { locale } = useRouter()
const sitePages: Page[] = []
if (pages) {
pages.forEach((page) => {
const slug = page.url && getSlug(page.url)
if (!slug) return
if (locale && !slug.startsWith(`${locale}/`)) return
sitePages.push(page)
})
}
return {
sitePages: sitePages.sort(bySortOrder),
}
}
// 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

View File

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

View 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

View File

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

View File

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

View File

@@ -0,0 +1,73 @@
import { FC } from 'react'
import Link from 'next/link'
import type { Product } from '@commerce/types/product'
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
brands?: any
products?: Product[]
}
const HomeAllProductsGrid: FC<Props> = ({
categories,
brands,
products = [],
}) => {
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-accent-8 text-base">
<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-accent-8 text-base">
<Link href={getDesignerPath(node.path)}>
<a>{node.name}</a>
</Link>
</li>
))}
</ul>
</div>
</div>
<div className="flex-1">
<Grid layout="normal">
{products.map((product) => (
<ProductCard
key={product.path}
product={product}
variant="simple"
imgProps={{
width: 480,
height: 480,
}}
/>
))}
</Grid>
</div>
</div>
)
}
export default HomeAllProductsGrid

View File

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

View File

@@ -0,0 +1,48 @@
.root {
@apply relative;
}
.button {
@apply h-10 px-2 rounded-md border border-accent-2 flex items-center justify-center transition-colors ease-linear;
}
.button:hover {
@apply border-accent-3 shadow-sm;
}
.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;
}
.item {
@apply flex cursor-pointer px-6 py-3 transition ease-in-out duration-150 text-primary leading-6 font-medium items-center;
text-transform: capitalize;
}
.item:hover {
@apply bg-accent-1;
}
.icon {
transition: transform 0.2s ease;
}
.icon.active {
transform: rotate(180deg);
}
@screen lg {
.dropdownMenu {
@apply absolute border border-accent-1 shadow-lg w-56 h-auto;
}
}
@screen md {
.closeButton {
@apply hidden;
}
}

View File

@@ -0,0 +1,101 @@
import cn from 'classnames'
import Link from 'next/link'
import { FC, useState } from 'react'
import { useRouter } from 'next/router'
import s from './I18nWidget.module.css'
import { Cross, ChevronUp } from '@components/icons'
import ClickOutside from '@lib/click-outside'
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 [display, setDisplay] = useState(false)
const {
locale,
locales,
defaultLocale = 'en-US',
asPath: currentPath,
} = useRouter()
const options = locales?.filter((val) => val !== locale)
const currentLocale = locale || defaultLocale
return (
<ClickOutside active={display} onClick={() => setDisplay(false)}>
<nav className={s.root}>
<div
className="flex items-center relative"
onClick={() => setDisplay(!display)}
>
<button className={s.button} aria-label="Language selector">
<img
width="20"
height="20"
className="block mr-2 w-5"
src={`/${LOCALES_MAP[currentLocale].img.filename}`}
alt={LOCALES_MAP[currentLocale].img.alt}
/>
{options && (
<span className="cursor-pointer">
<ChevronUp className={cn(s.icon, { [s.active]: display })} />
</span>
)}
</button>
</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)}
onClick={() => setDisplay(false)}
>
{LOCALES_MAP[locale].name}
</a>
</Link>
</li>
))}
</ul>
</div>
) : null}
</div>
</nav>
</ClickOutside>
)
}
export default I18nWidget

View File

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

View File

@@ -0,0 +1,4 @@
.root {
@apply h-full bg-primary mx-auto transition-colors duration-150;
max-width: 2460px;
}

View File

@@ -0,0 +1,150 @@
import cn from 'classnames'
import React, { FC } from 'react'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import { CommerceProvider } from '@framework'
import { useUI } from '@components/ui/context'
import type { Page } from '@commerce/types/page'
import { Navbar, Footer } from '@components/common'
import type { Category } from '@commerce/types/site'
import ShippingView from '@components/checkout/ShippingView'
import CartSidebarView from '@components/cart/CartSidebarView'
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { Sidebar, Button, LoadingDots } from '@components/ui'
import PaymentMethodView from '@components/checkout/PaymentMethodView'
import CheckoutSidebarView from '@components/checkout/CheckoutSidebarView'
import { CheckoutProvider } from '@components/checkout/context'
import MenuSidebarView, { Link } from '../UserNav/MenuSidebarView'
import LoginView from '@components/auth/LoginView'
import s from './Layout.module.css'
const Loading = () => (
<div className="w-80 h-80 flex items-center text-center justify-center p-3">
<LoadingDots />
</div>
)
const dynamicProps = {
loading: Loading,
}
const SignUpView = dynamic(
() => import('@components/auth/SignUpView'),
{
...dynamicProps
}
)
const ForgotPassword = dynamic(
() => import('@components/auth/ForgotPassword'),
{
...dynamicProps
}
)
const FeatureBar = dynamic(
() => import('@components/common/FeatureBar'),
{
...dynamicProps
}
)
const Modal = dynamic(
() => import('@components/ui/Modal'),
{
...dynamicProps,
ssr: false
}
)
interface Props {
pageProps: {
pages?: Page[]
categories: Category[]
}
}
const ModalView: FC<{ modalView: string; closeModal(): any }> = ({
modalView,
closeModal,
}) => {
return (
<Modal onClose={closeModal}>
{modalView === 'LOGIN_VIEW' && <LoginView />}
{modalView === 'SIGNUP_VIEW' && <SignUpView />}
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
</Modal>
)
}
const ModalUI: FC = () => {
const { displayModal, closeModal, modalView } = useUI()
return displayModal ? (
<ModalView modalView={modalView} closeModal={closeModal} />
) : null
}
const SidebarView: FC<{
sidebarView: string
closeSidebar(): any
links: Link[]
}> = ({ sidebarView, closeSidebar, links }) => {
return (
<Sidebar onClose={closeSidebar}>
{sidebarView === 'MOBILEMENU_VIEW' && <MenuSidebarView links={links} />}
{sidebarView === 'CART_VIEW' && <CartSidebarView />}
{sidebarView === 'CHECKOUT_VIEW' && <CheckoutSidebarView />}
{sidebarView === 'PAYMENT_VIEW' && <PaymentMethodView />}
{sidebarView === 'SHIPPING_VIEW' && <ShippingView />}
</Sidebar>
)
}
const SidebarUI: FC<{ links: any }> = ({ links }) => {
const { displaySidebar, closeSidebar, sidebarView } = useUI()
return displaySidebar ? (
<SidebarView
sidebarView={sidebarView}
closeSidebar={closeSidebar}
links={links}
/>
) : null
}
const Layout: FC<Props> = ({
children,
pageProps: { categories = [], ...pageProps },
}) => {
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
const { locale = 'en-US' } = useRouter()
const navBarlinks = categories.slice(0, 2).map((c) => ({
label: c.name,
href: `/search/${c.slug}`,
}))
return (
<CommerceProvider locale={locale}>
<div className={cn(s.root)}>
<Navbar links={navBarlinks} />
<main className="fit">{children}</main>
<Footer pages={pageProps.pages} />
<ModalUI />
<CheckoutProvider>
<SidebarUI links={navBarlinks} />
</CheckoutProvider>
<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={() => onAcceptCookies()}>
Accept cookies
</Button>
}
/>
</div>
</CommerceProvider>
)
}
export default Layout

View File

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

View File

@@ -0,0 +1,35 @@
.root {
@apply sticky top-0 bg-primary z-40 transition-all duration-150;
min-height: 74px;
}
.nav {
@apply relative flex flex-row justify-between py-4 md:py-4;
}
.navMenu {
@apply hidden ml-6 space-x-4 lg:block;
}
.link {
@apply inline-flex items-center leading-6
transition ease-in-out duration-75 cursor-pointer
text-accent-5;
}
.link:hover {
@apply text-accent-9;
}
.link:focus {
@apply outline-none text-accent-8;
}
.logo {
@apply cursor-pointer rounded-full border transform duration-100 ease-in-out;
&:hover {
@apply shadow-md;
transform: scale(1.05);
}
}

View File

@@ -0,0 +1,56 @@
import { FC } from 'react'
import Link from 'next/link'
import s from './Navbar.module.css'
import NavbarRoot from './NavbarRoot'
import { Logo, Container } from '@components/ui'
import { Searchbar, UserNav } from '@components/common'
interface Link {
href: string
label: string
}
interface NavbarProps {
links?: Link[]
}
const Navbar: FC<NavbarProps> = ({ links }) => (
<NavbarRoot>
<Container>
<div className={s.nav}>
<div className="flex items-center flex-1">
<Link href="/">
<a className={s.logo} aria-label="Logo">
<Logo />
</a>
</Link>
<nav className={s.navMenu}>
<Link href="/search">
<a className={s.link}>All</a>
</Link>
{links?.map((l) => (
<Link href={l.href} key={l.href}>
<a className={s.link}>{l.label}</a>
</Link>
))}
</nav>
</div>
{process.env.COMMERCE_SEARCH_ENABLED && (
<div className="justify-center flex-1 hidden lg:flex">
<Searchbar />
</div>
)}
<div className="flex items-center justify-end flex-1 space-x-8">
<UserNav />
</div>
</div>
{process.env.COMMERCE_SEARCH_ENABLED && (
<div className="flex pb-4 lg:px-6 lg:hidden">
<Searchbar id="mobile-search" />
</div>
)}
</Container>
</NavbarRoot>
)
export default Navbar

View File

@@ -0,0 +1,33 @@
import { FC, useState, useEffect } from 'react'
import throttle from 'lodash.throttle'
import cn from 'classnames'
import s from './Navbar.module.css'
const NavbarRoot: FC = ({ children }) => {
const [hasScrolled, setHasScrolled] = useState(false)
useEffect(() => {
const handleScroll = throttle(() => {
const offset = 0
const { scrollTop } = document.documentElement
const scrolled = scrollTop > offset
if (hasScrolled !== scrolled) {
setHasScrolled(scrolled)
}
}, 200)
document.addEventListener('scroll', handleScroll)
return () => {
document.removeEventListener('scroll', handleScroll)
}
}, [hasScrolled])
return (
<div className={cn(s.root, { 'shadow-magical': hasScrolled })}>
{children}
</div>
)
}
export default NavbarRoot

View File

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

View File

@@ -0,0 +1,29 @@
.root {
@apply relative text-sm bg-accent-0 text-base w-full transition-colors duration-150 border border-accent-2;
}
.input {
@apply bg-transparent px-3 py-2 appearance-none w-full transition duration-150 ease-in-out pr-10;
}
.input::placeholder {
@apply text-accent-3;
}
.input:focus {
@apply outline-none shadow-outline-normal;
}
.iconContainer {
@apply absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none;
}
.icon {
@apply h-5 w-5;
}
@screen sm {
.input {
min-width: 300px;
}
}

View File

@@ -0,0 +1,60 @@
import { FC, memo, 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')
}, [router])
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
e.preventDefault()
if (e.key === 'Enter') {
const q = e.currentTarget.value
router.push(
{
pathname: `/search`,
query: q ? { q } : {},
},
undefined,
{ shallow: true }
)
}
}
return (
<div className={cn(s.root, className)}>
<label className="hidden" htmlFor={id}>
Search
</label>
<input
id={id}
className={s.input}
placeholder="Search for products..."
defaultValue={router.query.q}
onKeyUp={handleKeyUp}
/>
<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 memo(Searchbar)

View File

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

View File

@@ -0,0 +1,20 @@
.root {
@apply relative h-full flex flex-col;
}
.header {
@apply sticky top-0 pl-4 py-4 pr-6
flex items-center justify-between
bg-accent-0 box-border w-full z-10;
min-height: 66px;
}
.container {
@apply flex flex-col flex-1 box-border;
}
@screen lg {
.header {
min-height: 74px;
}
}

View File

@@ -0,0 +1,50 @@
import React, { FC } from 'react'
import { Cross, ChevronLeft } from '@components/icons'
import { UserNav } from '@components/common'
import cn from 'classnames'
import s from './SidebarLayout.module.css'
type ComponentProps = { className?: string } & (
| { handleClose: () => any; handleBack?: never }
| { handleBack: () => any; handleClose?: never }
)
const SidebarLayout: FC<ComponentProps> = ({
children,
className,
handleClose,
handleBack,
}) => {
return (
<div className={cn(s.root, className)}>
<header className={s.header}>
{handleClose && (
<button
onClick={handleClose}
aria-label="Close"
className="hover:text-accent-5 transition ease-in-out duration-150 flex items-center focus:outline-none mr-6"
>
<Cross className="h-6 w-6 hover:text-accent-3" />
<span className="ml-2 text-accent-7 text-sm ">Close</span>
</button>
)}
{handleBack && (
<button
onClick={handleBack}
aria-label="Go back"
className="hover:text-accent-5 transition ease-in-out duration-150 flex items-center focus:outline-none"
>
<ChevronLeft className="h-6 w-6 hover:text-accent-3" />
<span className="ml-2 text-accent-7 text-xs">Back</span>
</button>
)}
<span className={s.nav}>
<UserNav />
</span>
</header>
<div className={s.container}>{children}</div>
</div>
)
}
export default SidebarLayout

View File

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

View File

@@ -0,0 +1,26 @@
@screen lg {
.dropdownMenu {
@apply absolute top-10 border border-accent-1 shadow-lg w-56 h-auto;
}
}
.dropdownMenu {
@apply fixed right-0 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
}
.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-accent-1;
}
.link.active {
@apply font-bold bg-accent-2;
}
.off {
@apply hidden;
}

View File

@@ -0,0 +1,125 @@
import cn from 'classnames'
import Link from 'next/link'
import { FC, useRef, useState, useEffect } 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 ClickOutside from '@lib/click-outside'
import useLogout from '@framework/auth/use-logout'
import {
disableBodyScroll,
enableBodyScroll,
clearAllBodyScrollLocks,
} from 'body-scroll-lock'
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 logout = useLogout()
const { pathname } = useRouter()
const { theme, setTheme } = useTheme()
const [display, setDisplay] = useState(false)
const { closeSidebarIfPresent } = useUI()
const ref = useRef() as React.MutableRefObject<HTMLUListElement>
useEffect(() => {
if (ref.current) {
if (display) {
disableBodyScroll(ref.current)
} else {
enableBodyScroll(ref.current)
}
}
return () => {
clearAllBodyScrollLocks()
}
}, [display])
return (
<ClickOutside active={display} onClick={() => setDisplay(false)}>
<div>
<button
className={s.avatarButton}
onClick={() => setDisplay(!display)}
aria-label="Menu"
>
<Avatar />
</button>
{display && (
<ul className={s.dropdownMenu} ref={ref}>
{LINKS.map(({ name, href }) => (
<li key={href}>
<div>
<Link href={href}>
<a
className={cn(s.link, {
[s.active]: pathname === href,
})}
onClick={() => {
setDisplay(false)
closeSidebarIfPresent()
}}
>
{name}
</a>
</Link>
</div>
</li>
))}
<li>
<a
className={cn(s.link, 'justify-between')}
onClick={() => {
theme === 'dark' ? setTheme('light') : setTheme('dark')
setDisplay(false)
}}
>
<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-accent-2 mt-4')}
onClick={() => logout()}
>
Logout
</a>
</li>
</ul>
)}
</div>
</ClickOutside>
)
}
export default DropdownMenu

View File

@@ -0,0 +1,7 @@
.root {
@apply px-4 sm:px-6 sm:w-full flex-1 z-20;
}
.item {
@apply text-2xl font-bold;
}

View File

@@ -0,0 +1,41 @@
import Link from 'next/link'
import s from './MenuSidebarView.module.css'
import { FC } from 'react'
import { useUI } from '@components/ui/context'
import SidebarLayout from '@components/common/SidebarLayout'
import { Link as LinkProps} from '.'
interface MenuProps {
links?: LinkProps[]
}
const MenuSidebarView: FC<MenuProps> = (props) => {
const { closeSidebar } = useUI()
const handleClose = () => closeSidebar()
return (
<SidebarLayout handleClose={handleClose}>
<div className={s.root}>
<nav>
<ul>
<li className={s.item}>
<Link href="/search">
<a>All</a>
</Link>
</li>
{props.links?.map((l: any) => (
<li key={l.href} className={s.item}>
<Link href={l.href}>
<a>{l.label}</a>
</Link>
</li>
))}
</ul>
</nav>
</div>
</SidebarLayout>
)
}
export default MenuSidebarView

View File

@@ -0,0 +1,6 @@
export { default } from './MenuSidebarView'
export interface Link {
href: string
label: string
}

View File

@@ -0,0 +1,45 @@
.root {
@apply relative flex items-center;
}
.list {
@apply flex flex-row items-center justify-items-end h-full;
}
.item {
@apply ml-6 cursor-pointer relative transition ease-in-out duration-100 flex items-center outline-none text-primary;
&:hover {
@apply text-accent-6 transition scale-110 duration-100;
}
&:first-child {
@apply ml-0;
}
&:focus,
&:active {
@apply outline-none;
}
}
.bagCount {
@apply border border-accent-1 bg-secondary text-secondary absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs;
padding-left: 2.5px;
padding-right: 2.5px;
min-width: 1.25rem;
min-height: 1.25rem;
}
.avatarButton {
@apply inline-flex justify-center rounded-full;
}
.mobileMenu {
@apply flex lg:hidden ml-6
}
.avatarButton:focus,
.mobileMenu:focus {
@apply outline-none;
}

View File

@@ -0,0 +1,91 @@
import { FC } from 'react'
import Link from 'next/link'
import cn from 'classnames'
import type { LineItem } from '@commerce/types/cart'
import useCart from '@framework/cart/use-cart'
import useCustomer from '@framework/customer/use-customer'
import { Avatar } from '@components/common'
import { Heart, Bag } from '@components/icons'
import { useUI } from '@components/ui/context'
import Button from '@components/ui/Button'
import DropdownMenu from './DropdownMenu'
import s from './UserNav.module.css'
import Menu from '@components/icons/Menu'
interface Props {
className?: string
}
const countItem = (count: number, item: LineItem) => count + item.quantity
const UserNav: FC<Props> = ({ className }) => {
const { data } = useCart()
const { data: customer } = useCustomer()
const { toggleSidebar, closeSidebarIfPresent, openModal, setSidebarView } =
useUI()
const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0
return (
<nav className={cn(s.root, className)}>
<ul className={s.list}>
{process.env.COMMERCE_CART_ENABLED && (
<li className={s.item}>
<Button
className={s.item}
variant="naked"
onClick={() => {
setSidebarView('CART_VIEW')
toggleSidebar()
}}
aria-label={`Cart items: ${itemsCount}`}
>
<Bag />
{itemsCount > 0 && (
<span className={s.bagCount}>{itemsCount}</span>
)}
</Button>
</li>
)}
{process.env.COMMERCE_WISHLIST_ENABLED && (
<li className={s.item}>
<Link href="/wishlist">
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
<Heart />
</a>
</Link>
</li>
)}
{process.env.COMMERCE_CUSTOMERAUTH_ENABLED && (
<li className={s.item}>
{customer ? (
<DropdownMenu />
) : (
<button
className={s.avatarButton}
aria-label="Menu"
onClick={() => openModal()}
>
<Avatar />
</button>
)}
</li>
)}
<li className={s.mobileMenu}>
<Button
className={s.item}
variant="naked"
onClick={() => {
setSidebarView('MOBILEMENU_VIEW')
toggleSidebar()
}}
aria-label="Menu"
>
<Menu />
</Button>
</li>
</ul>
</nav>
)
}
export default UserNav

View File

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

View File

@@ -0,0 +1,9 @@
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 Head } from './Head'
export { default as I18nWidget } from './I18nWidget'