forked from crowetic/commerce
Merge branch 'master' into jb/product-improvements
This commit is contained in:
commit
a64c805b45
@ -1,5 +1,3 @@
|
|||||||
@import './font.css';
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--primary: white;
|
--primary: white;
|
||||||
--primary-2: #f1f3f5;
|
--primary-2: #f1f3f5;
|
||||||
@ -16,14 +14,17 @@
|
|||||||
--hover-1: rgba(0, 0, 0, 0.15);
|
--hover-1: rgba(0, 0, 0, 0.15);
|
||||||
--hover-2: rgba(0, 0, 0, 0.25);
|
--hover-2: rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
--cyan: #50e3c2;
|
--cyan: #22b8cf;
|
||||||
--green: #37b679;
|
--green: #37b679;
|
||||||
--red: #da3c3c;
|
--red: #da3c3c;
|
||||||
--pink: #ff0080;
|
--pink: #e64980;
|
||||||
--purple: #f81ce5;
|
--purple: #f81ce5;
|
||||||
--violet: #7928ca;
|
|
||||||
--blue: #0070f3;
|
--blue: #0070f3;
|
||||||
|
|
||||||
|
--violet-light: #7048e8;
|
||||||
|
--violet: #5f3dc4;
|
||||||
|
|
||||||
--accents-0: #f8f9fa;
|
--accents-0: #f8f9fa;
|
||||||
--accents-1: #f1f3f5;
|
--accents-1: #f1f3f5;
|
||||||
--accents-2: #e9ecef;
|
--accents-2: #e9ecef;
|
||||||
@ -62,10 +63,6 @@
|
|||||||
--accents-9: #f8f9fa;
|
--accents-9: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fit {
|
|
||||||
min-height: calc(100vh - 88px - 41px);
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*:before,
|
*:before,
|
||||||
*:after {
|
*:after {
|
3
assets/components.css
Normal file
3
assets/components.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.fit {
|
||||||
|
min-height: calc(100vh - 88px - 41px);
|
||||||
|
}
|
8
assets/main.css
Normal file
8
assets/main.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
@import 'tailwindcss/base';
|
||||||
|
@import './font.css';
|
||||||
|
@import './base.css';
|
||||||
|
|
||||||
|
@import 'tailwindcss/components';
|
||||||
|
@import './components.css';
|
||||||
|
|
||||||
|
@import 'tailwindcss/utilities';
|
@ -1,2 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
@ -1 +0,0 @@
|
|||||||
@tailwind utilities;
|
|
@ -12,7 +12,7 @@ const Avatar: FC<Props> = ({}) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="inline-block h-8 w-8 rounded-full border-2 border-accents-2 transition-colors duration-150"
|
className="inline-block h-8 w-8 rounded-full border-2 border-primary hover:border-secondary transition linear-out duration-150"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `linear-gradient(140deg, ${bg[0]}, ${bg[1]} 100%)`,
|
backgroundImage: `linear-gradient(140deg, ${bg[0]}, ${bg[1]} 100%)`,
|
||||||
}}
|
}}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
.separator {
|
|
||||||
@apply mx-3 bg-secondary;
|
|
||||||
width: 1px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator:before {
|
|
||||||
content: '';
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import s from './Featurebar.module.css'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
@ -18,15 +17,16 @@ const Featurebar: FC<Props> = ({
|
|||||||
hide,
|
hide,
|
||||||
}) => {
|
}) => {
|
||||||
const rootClassName = cn(
|
const rootClassName = cn(
|
||||||
'transition-all transform duration-150 ease-out p-6 bg-primary text-base text-sm md:flex flex-row justify-center items-center font-medium fixed bottom-0 w-full z-10',
|
'transition-transform transform duration-500 text-center ease-out p-6 bg-primary text-base text-sm md:flex md:text-left flex-row justify-center items-center font-medium fixed bottom-0 w-full z-10',
|
||||||
{ 'translate-y-full': hide },
|
{ 'translate-y-full': hide },
|
||||||
className
|
className
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<div className={rootClassName}>
|
<div className={rootClassName}>
|
||||||
<span>{title}</span>
|
<span className="block md:inline">{title}</span>
|
||||||
<span className={s.separator} />
|
<span className="block mb-6 md:inline md:mb-0 md:ml-2">
|
||||||
<span>{description}</span>
|
{description}
|
||||||
|
</span>
|
||||||
{action && action}
|
{action && action}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -3,8 +3,8 @@ import cn from 'classnames'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import type { Page } from '@lib/bigcommerce/api/operations/get-all-pages'
|
import type { Page } from '@lib/bigcommerce/api/operations/get-all-pages'
|
||||||
import getSlug from '@utils/get-slug'
|
import getSlug from '@utils/get-slug'
|
||||||
import { Logo } from '@components/ui'
|
import { Logo, Container } from '@components/ui'
|
||||||
import s from './Footer.module.css'
|
import { Github, DoubleChevron } from '@components/icon'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
@ -15,53 +15,100 @@ interface Props {
|
|||||||
const LEGAL_PAGES = ['terms-of-use', 'shipping-returns', 'privacy-policy']
|
const LEGAL_PAGES = ['terms-of-use', 'shipping-returns', 'privacy-policy']
|
||||||
|
|
||||||
const Footer: FC<Props> = ({ className, pages }) => {
|
const Footer: FC<Props> = ({ className, pages }) => {
|
||||||
const rootClassName = cn(
|
const rootClassName = cn(className)
|
||||||
'flex flex-col p-6 md:py-12 md:flex-row flex-wrap max-w-screen-xl m-auto',
|
|
||||||
className
|
|
||||||
)
|
|
||||||
const { sitePages, legalPages } = getPages(pages)
|
const { sitePages, legalPages } = getPages(pages)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-black text-white">
|
<footer className={rootClassName}>
|
||||||
<footer className={rootClassName}>
|
<Container>
|
||||||
<Link href="/">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 border-b border-accents-2 py-12 text-primary bg-primary">
|
||||||
<a className="flex flex-initial items-center md:items-start font-bold md:mr-24">
|
<div className="col-span-1 lg:col-span-2">
|
||||||
<span className="rounded-full border border-gray-700 mr-2">
|
<Link href="/">
|
||||||
<Logo />
|
<a className="flex flex-initial items-center font-bold md:mr-24">
|
||||||
</span>
|
<span className="rounded-full border border-gray-700 mr-2">
|
||||||
<span>ACME</span>
|
<Logo />
|
||||||
</a>
|
</span>
|
||||||
</Link>
|
<span>ACME</span>
|
||||||
|
</a>
|
||||||
<ul className="flex flex-initial flex-col divide-y divide-gray-700 md:divide-y-0 my-12 md:my-0 md:flex-1">
|
</Link>
|
||||||
{sitePages.map((page) => (
|
</div>
|
||||||
<li key={page.url} className="py-3 md:py-0 md:pb-4">
|
<div className="col-span-1 lg:col-span-2">
|
||||||
<Link href={page.url!}>
|
<ul className="flex flex-initial flex-col md:flex-1">
|
||||||
<a className="text-gray-400 hover:text-white transition ease-in-out duration-150">
|
<li className="py-3 md:py-0 md:pb-4">
|
||||||
{page.name}
|
<Link href="/">
|
||||||
</a>
|
<a className="text-gray-400 hover:text-white transition ease-in-out duration-100">
|
||||||
</Link>
|
Home
|
||||||
</li>
|
</a>
|
||||||
))}
|
</Link>
|
||||||
</ul>
|
</li>
|
||||||
|
<li className="py-3 md:py-0 md:pb-4">
|
||||||
<ul className="flex flex-initial flex-col divide-y divide-gray-700 md:divide-y-0 my-12 md:my-0 md:flex-1">
|
<Link href="/">
|
||||||
{legalPages.map((page) => (
|
<a className="text-gray-400 hover:text-white transition ease-in-out duration-100">
|
||||||
<li key={page.url} className="py-3 md:py-0 md:pb-4">
|
Careers
|
||||||
<Link href={page.url!}>
|
</a>
|
||||||
<a className="text-gray-400 hover:text-white transition ease-in-out duration-150">
|
</Link>
|
||||||
{page.name}
|
</li>
|
||||||
</a>
|
<li className="py-3 md:py-0 md:pb-4">
|
||||||
</Link>
|
<Link href="/blog">
|
||||||
</li>
|
<a className="text-gray-400 hover:text-white transition ease-in-out duration-100">
|
||||||
))}
|
Blog
|
||||||
</ul>
|
</a>
|
||||||
|
</Link>
|
||||||
<small className={cn('text-white', s.copyright)}>
|
</li>
|
||||||
© 2020 ACME, Inc. All rights reserved.
|
{sitePages.map((page) => (
|
||||||
</small>
|
<li key={page.url} className="py-3 md:py-0 md:pb-4">
|
||||||
</footer>
|
<Link href={page.url!}>
|
||||||
</div>
|
<a className="text-gray-400 hover:text-white transition ease-in-out duration-100">
|
||||||
|
{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-gray-400 hover:text-white transition ease-in-out duration-100">
|
||||||
|
{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">
|
||||||
|
<Github />
|
||||||
|
<div className="h-10 px-2 rounded-md border border-accents-2 flex items-center space-x-2 justify-center">
|
||||||
|
<img className="" src="/flag-us.png" />
|
||||||
|
<span>English</span>
|
||||||
|
<span className="">
|
||||||
|
<DoubleChevron />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</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 text-accents-4">
|
||||||
|
<span>Crafted by</span>
|
||||||
|
<a href="https://vercel.com">
|
||||||
|
<img
|
||||||
|
src="/vercel.png"
|
||||||
|
alt="Vercel.com Logo"
|
||||||
|
className="inline-block h-6 ml-4"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</footer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,12 +55,12 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
|
|||||||
<Featurebar
|
<Featurebar
|
||||||
title="This site uses cookies to improve your experience."
|
title="This site uses cookies to improve your experience."
|
||||||
description="By clicking, you agree to our Privacy Policy."
|
description="By clicking, you agree to our Privacy Policy."
|
||||||
|
hide={acceptedCookies}
|
||||||
action={
|
action={
|
||||||
<Button className="mx-5" onClick={() => setAcceptedCookies(true)}>
|
<Button className="mx-5" onClick={() => setAcceptedCookies(true)}>
|
||||||
Accept cookies
|
Accept cookies
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
className={cn({ ['translate-y-full']: acceptedCookies })}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CommerceProvider>
|
</CommerceProvider>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import s from './Navbar.module.css'
|
import s from './Navbar.module.css'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useTheme } from 'next-themes'
|
|
||||||
import { Logo } from '@components/ui'
|
import { Logo } from '@components/ui'
|
||||||
import { Searchbar, UserNav } from '@components/core'
|
import { Searchbar, UserNav } from '@components/core'
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -1,26 +1,13 @@
|
|||||||
.dropdownMenu {
|
.dropdownMenu {
|
||||||
@apply fixed top-0 right-0 z-20 w-full h-full;
|
@apply fixed right-0 mt-7 origin-top-right outline-none bg-primary z-40 w-full h-full;
|
||||||
|
|
||||||
@screen lg {
|
@screen lg {
|
||||||
@apply absolute right-0 w-screen;
|
@apply absolute border border-accents-1 shadow-lg w-56 h-auto;
|
||||||
max-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .dropdownMenuContainer {
|
|
||||||
@apply flex-col py-6 bg-primary h-full justify-around;
|
|
||||||
|
|
||||||
@screen lg {
|
|
||||||
@apply border border-accents-1 shadow-lg py-2 h-auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& .link {
|
& .link {
|
||||||
@apply flex space-x-2 cursor-pointer px-6 py-3 block space-y-1 hover:bg-accents-1 transition ease-in-out duration-150 text-base leading-6 font-medium text-gray-900 items-center;
|
@apply flex cursor-pointer px-6 py-3 block hover:bg-accents-1 transition ease-in-out duration-150 text-base leading-6 font-medium text-gray-900 items-center;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
|
|
||||||
& .icons svg {
|
|
||||||
@apply w-6 h-6;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.off {
|
&.off {
|
||||||
|
@ -1,74 +1,75 @@
|
|||||||
import { useTheme } from 'next-themes'
|
|
||||||
import s from './DropdownMenu.module.css'
|
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { FocusScope } from '@react-aria/focus'
|
|
||||||
import {
|
|
||||||
useOverlay,
|
|
||||||
DismissButton,
|
|
||||||
usePreventScroll,
|
|
||||||
} from '@react-aria/overlays'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
import s from './DropdownMenu.module.css'
|
||||||
import { Moon, Sun } from '@components/icon'
|
import { Moon, Sun } from '@components/icon'
|
||||||
|
import { Menu, Transition } from '@headlessui/react'
|
||||||
|
import { usePreventScroll } from '@react-aria/overlays'
|
||||||
interface DropdownMenuProps {
|
interface DropdownMenuProps {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
innerRef: React.MutableRefObject<HTMLInputElement>
|
open: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownMenu: FC<DropdownMenuProps> = ({
|
const DropdownMenu: FC<DropdownMenuProps> = ({
|
||||||
onClose,
|
onClose,
|
||||||
children,
|
children,
|
||||||
innerRef,
|
open = false,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
|
|
||||||
let { overlayProps } = useOverlay(
|
usePreventScroll({
|
||||||
{
|
isDisabled: !open,
|
||||||
isDismissable: true,
|
})
|
||||||
onClose: onClose,
|
|
||||||
isOpen: true,
|
|
||||||
},
|
|
||||||
innerRef
|
|
||||||
)
|
|
||||||
|
|
||||||
usePreventScroll()
|
|
||||||
return (
|
return (
|
||||||
<FocusScope restoreFocus>
|
<Transition
|
||||||
<div className={cn(s.dropdownMenu)} ref={innerRef} {...overlayProps}>
|
show={open}
|
||||||
{/* Needed placeholder for User Interation*/}
|
enter="transition ease-out duration-100 z-20"
|
||||||
<div className="flex justify-end">
|
enterFrom="transform opacity-0 scale-95"
|
||||||
<span onClick={onClose} className="bg-transparent h-12 w-12" />
|
enterTo="transform opacity-100 scale-100"
|
||||||
</div>
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
<nav className={s.dropdownMenuContainer}>
|
leaveTo="transform opacity-0 scale-95"
|
||||||
<Link href="#">
|
>
|
||||||
<a className={s.link}>My Purchases</a>
|
<Menu.Items className={s.dropdownMenu}>
|
||||||
</Link>
|
<Menu.Item>
|
||||||
<Link href="#">
|
{({ active }) => <a className={s.link}>My Purchases</a>}
|
||||||
<a className={s.link}>My Account</a>
|
</Menu.Item>
|
||||||
</Link>
|
<Menu.Item>
|
||||||
<a
|
{({ active }) => <a className={s.link}>My Account</a>}
|
||||||
className={s.link}
|
</Menu.Item>
|
||||||
onClick={() =>
|
<Menu.Item>
|
||||||
theme === 'dark' ? setTheme('light') : setTheme('dark')
|
{({ active }) => (
|
||||||
}
|
<a
|
||||||
>
|
className={cn(s.link, 'justify-between')}
|
||||||
<span>
|
onClick={() =>
|
||||||
Theme: <strong>{theme}</strong>{' '}
|
theme === 'dark' ? setTheme('light') : setTheme('dark')
|
||||||
</span>
|
}
|
||||||
<span className={s.icons}>
|
>
|
||||||
{theme === 'dark' ? <Moon /> : <Sun />}
|
<div>
|
||||||
</span>
|
Theme: <strong>{theme}</strong>{' '}
|
||||||
</a>
|
</div>
|
||||||
<Link href="#">
|
<div className="ml-3">
|
||||||
|
{theme == 'dark' ? (
|
||||||
|
<Moon width={20} height={20} />
|
||||||
|
) : (
|
||||||
|
<Sun width="20" height={20} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
<a className={cn(s.link, 'border-t border-accents-2 mt-4')}>
|
<a className={cn(s.link, 'border-t border-accents-2 mt-4')}>
|
||||||
Logout
|
Logout
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
)}
|
||||||
</nav>
|
</Menu.Item>
|
||||||
</div>
|
</Menu.Items>
|
||||||
</FocusScope>
|
</Transition>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,14 +12,14 @@
|
|||||||
.item {
|
.item {
|
||||||
@apply mr-6 cursor-pointer relative transition ease-in-out duration-150 text-base flex items-center;
|
@apply mr-6 cursor-pointer relative transition ease-in-out duration-150 text-base flex items-center;
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply text-accents-8 transition ease-in-out duration-100 transform scale-110;
|
@apply text-accents-8 transition scale-110 duration-100;
|
||||||
}
|
|
||||||
|
|
||||||
&.heart:hover svg {
|
|
||||||
fill: var(--accents-9);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@apply mr-0;
|
@apply mr-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,11 @@ import Link from 'next/link'
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import s from './UserNav.module.css'
|
import s from './UserNav.module.css'
|
||||||
import { FC, useRef } from 'react'
|
import { FC, useRef } from 'react'
|
||||||
|
|
||||||
import { Avatar } from '@components/core'
|
import { Avatar } from '@components/core'
|
||||||
import { Heart, Bag } from '@components/icon'
|
import { Heart, Bag } from '@components/icon'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import DropdownMenu from './DropdownMenu'
|
import DropdownMenu from './DropdownMenu'
|
||||||
|
import { Menu } from '@headlessui/react'
|
||||||
import useCart from '@lib/bigcommerce/cart/use-cart'
|
import useCart from '@lib/bigcommerce/cart/use-cart'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -48,22 +47,24 @@ const UserNav: FC<Props> = ({ className, children, ...props }) => {
|
|||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
<Link href="/wishlist">
|
<Link href="/wishlist">
|
||||||
<li className={cn(s.item, s.heart)}>
|
<li className={s.item}>
|
||||||
<Heart />
|
<Heart />
|
||||||
</li>
|
</li>
|
||||||
</Link>
|
</Link>
|
||||||
<li
|
<li className={s.item}>
|
||||||
className={s.item}
|
<Menu>
|
||||||
onClick={() => (displayDropdown ? closeDropdown() : openDropdown())}
|
{({ open }) => (
|
||||||
>
|
<>
|
||||||
<Avatar />
|
<Menu.Button className="inline-flex justify-center rounded-full">
|
||||||
|
<Avatar />
|
||||||
|
</Menu.Button>
|
||||||
|
<DropdownMenu onClose={closeDropdown} open={open} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{displayDropdown && (
|
|
||||||
<DropdownMenu onClose={closeDropdown} innerRef={ref} />
|
|
||||||
)}
|
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
22
components/icon/DoubleChevron.tsx
Normal file
22
components/icon/DoubleChevron.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const DoubleChevron = ({ ...props }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M16 8.90482L12 4L8 8.90482M8 15.0952L12 20L16 15.0952"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DoubleChevron
|
20
components/icon/Github.tsx
Normal file
20
components/icon/Github.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const Sun = ({ ...props }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M12 0C5.37 0 0 5.50583 0 12.3035C0 17.7478 3.435 22.3463 8.205 23.9765C8.805 24.0842 9.03 23.715 9.03 23.3921C9.03 23.0999 9.015 22.131 9.015 21.1005C6 21.6696 5.22 20.347 4.98 19.6549C4.845 19.3012 4.26 18.2092 3.75 17.917C3.33 17.6863 2.73 17.1173 3.735 17.1019C4.68 17.0865 5.355 17.9939 5.58 18.363C6.66 20.2239 8.385 19.701 9.075 19.3781C9.18 18.5783 9.495 18.04 9.84 17.7325C7.17 17.4249 4.38 16.3637 4.38 11.6576C4.38 10.3196 4.845 9.21227 5.61 8.35102C5.49 8.04343 5.07 6.78232 5.73 5.09058C5.73 5.09058 6.735 4.76762 9.03 6.3517C9.99 6.07487 11.01 5.93645 12.03 5.93645C13.05 5.93645 14.07 6.07487 15.03 6.3517C17.325 4.75224 18.33 5.09058 18.33 5.09058C18.99 6.78232 18.57 8.04343 18.45 8.35102C19.215 9.21227 19.68 10.3042 19.68 11.6576C19.68 16.3791 16.875 17.4249 14.205 17.7325C14.64 18.1169 15.015 18.8552 15.015 20.0086C15.015 21.6542 15 22.9768 15 23.3921C15 23.715 15.225 24.0995 15.825 23.9765C18.2072 23.1519 20.2773 21.5822 21.7438 19.4882C23.2103 17.3942 23.9994 14.8814 24 12.3035C24 5.50583 18.63 0 12 0Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sun
|
@ -10,6 +10,7 @@ const Moon = ({ ...props }) => {
|
|||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
shape-rendering="geometricPrecision"
|
shape-rendering="geometricPrecision"
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
|
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -10,6 +10,7 @@ const Sun = ({ ...props }) => {
|
|||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
shape-rendering="geometricPrecision"
|
shape-rendering="geometricPrecision"
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<circle cx="12" cy="12" r="5" />
|
<circle cx="12" cy="12" r="5" />
|
||||||
<path d="M12 1v2" />
|
<path d="M12 1v2" />
|
||||||
|
@ -8,3 +8,5 @@ export { default as Minus } from './Minus'
|
|||||||
export { default as Check } from './Check'
|
export { default as Check } from './Check'
|
||||||
export { default as Sun } from './Sun'
|
export { default as Sun } from './Sun'
|
||||||
export { default as Moon } from './Moon'
|
export { default as Moon } from './Moon'
|
||||||
|
export { default as Github } from './Github'
|
||||||
|
export { default as DoubleChevron } from './DoubleChevron'
|
||||||
|
@ -2,8 +2,12 @@
|
|||||||
@apply relative w-full box-border overflow-hidden bg-no-repeat bg-center bg-cover transition ease-linear cursor-pointer;
|
@apply relative w-full box-border overflow-hidden bg-no-repeat bg-center bg-cover transition ease-linear cursor-pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
& .squareBg {
|
& .squareBg:before {
|
||||||
@apply scale-75;
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .product-image {
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .productTitle > span,
|
& .productTitle > span,
|
||||||
@ -37,6 +41,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& .product-image {
|
||||||
|
@apply transform transition-transform duration-500;
|
||||||
|
}
|
||||||
|
|
||||||
&:nth-child(6n + 1) .squareBg {
|
&:nth-child(6n + 1) .squareBg {
|
||||||
@apply bg-violet;
|
@apply bg-violet;
|
||||||
}
|
}
|
||||||
@ -58,16 +66,23 @@
|
|||||||
.productTitle > span,
|
.productTitle > span,
|
||||||
.productPrice,
|
.productPrice,
|
||||||
.wishlistButton {
|
.wishlistButton {
|
||||||
@apply transition ease-in-out duration-150;
|
@apply transition ease-in-out duration-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.squareBg {
|
.squareBg {
|
||||||
@apply transform absolute inset-0 z-0 bg-secondary;
|
@apply transform 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;
|
||||||
|
content: '';
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg width='48' height='46' viewBox='0 0 48 46' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cline opacity='0.1' x1='9.41421' y1='8' x2='21' y2='19.5858' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cline opacity='0.1' x1='1' y1='-1' x2='17.3848' y2='-1' transform='matrix(-0.707107 0.707107 0.707107 0.707107 40 8)' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cline opacity='0.1' x1='1' y1='-1' x2='17.3848' y2='-1' transform='matrix(0.707107 -0.707107 -0.707107 -0.707107 8 38)' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cline opacity='0.1' x1='38.5858' y1='38' x2='27' y2='26.4142' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E%0A");
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple {
|
.simple {
|
||||||
& .squareBg {
|
& .squareBg {
|
||||||
@apply bg-gray-300 !important;
|
@apply bg-accents-0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .productTitle {
|
& .productTitle {
|
||||||
@ -102,5 +117,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wishlistButton {
|
.wishlistButton {
|
||||||
@apply w-10 h-10 flex items-center justify-center bg-primary text-base font-semibold inline-block text-xs leading-6 cursor-pointer;
|
@apply w-10 h-10 flex ml-auto items-center justify-center bg-primary text-base font-semibold inline-block text-xs leading-6 cursor-pointer;
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ const ProductCard: FC<Props> = ({ className, product: p, variant }) => {
|
|||||||
>
|
>
|
||||||
<div className="absolute z-10 inset-0 flex items-center justify-center">
|
<div className="absolute z-10 inset-0 flex items-center justify-center">
|
||||||
<img
|
<img
|
||||||
className="w-full object-cover"
|
className={cn('w-full object-cover', s['product-image'])}
|
||||||
src={p.images.edges?.[0]?.node.urlXL}
|
src={p.images.edges?.[0]?.node.urlXL}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.productDisplay {
|
.productDisplay {
|
||||||
@apply relative flex px-0 pb-0 relative box-border col-span-7;
|
@apply relative flex px-0 pb-0 relative box-border col-span-1 bg-violet;
|
||||||
margin-right: -2rem;
|
margin-right: -2rem;
|
||||||
margin-left: -2rem;
|
margin-left: -2rem;
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@screen lg {
|
@screen lg {
|
||||||
@apply mx-0;
|
@apply mx-0 col-span-7;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@ -49,9 +49,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
@apply flex flex-col col-span-5;
|
@apply flex flex-col col-span-1;
|
||||||
|
|
||||||
@screen lg {
|
@screen lg {
|
||||||
|
@apply col-span-5;
|
||||||
padding-top: 5rem;
|
padding-top: 5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
|
import cn from 'classnames'
|
||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
|
import s from './ProductView.module.css'
|
||||||
import { FC, useState, useEffect } from 'react'
|
import { FC, useState, useEffect } from 'react'
|
||||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import { Button, Container } from '@components/ui'
|
import { Button, Container } from '@components/ui'
|
||||||
import { Swatch, ProductSlider } from '@components/product'
|
import { Swatch, ProductSlider } from '@components/product'
|
||||||
import useAddItem from '@lib/bigcommerce/cart/use-add-item'
|
import useAddItem from '@lib/bigcommerce/cart/use-add-item'
|
||||||
import { getProductOptions } from '../helpers'
|
|
||||||
import s from './ProductView.module.css'
|
|
||||||
import { isDesktop } from '@lib/browser'
|
import { isDesktop } from '@lib/browser'
|
||||||
import cn from 'classnames'
|
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
||||||
|
import { getProductOptions } from '../helpers'
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
children?: any
|
children?: any
|
||||||
@ -66,7 +66,6 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
|||||||
/>
|
/>
|
||||||
<div className={cn(s.root, 'fit')}>
|
<div className={cn(s.root, 'fit')}>
|
||||||
<div className={cn(s.productDisplay, 'fit')}>
|
<div className={cn(s.productDisplay, 'fit')}>
|
||||||
<div className={s.squareBg}></div>
|
|
||||||
<div className={s.nameBox}>
|
<div className={s.nameBox}>
|
||||||
<h1 className={s.name}>{product.name}</h1>
|
<h1 className={s.name}>{product.name}</h1>
|
||||||
<div className={s.price}>
|
<div className={s.price}>
|
||||||
@ -130,7 +129,7 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
|||||||
))}
|
))}
|
||||||
<div className="pb-12">
|
<div className="pb-12">
|
||||||
<div
|
<div
|
||||||
className="pb-14 break-words"
|
className="pb-14 break-words w-full"
|
||||||
dangerouslySetInnerHTML={{ __html: product.description }}
|
dangerouslySetInnerHTML={{ __html: product.description }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
@ -8,7 +8,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Container: FC<Props> = ({ children, className, el = 'div' }) => {
|
const Container: FC<Props> = ({ children, className, el = 'div' }) => {
|
||||||
const rootClassName = cn('mx-auto max-w-7xl px-6', className)
|
const rootClassName = cn('mx-auto max-w-8xl px-12', className)
|
||||||
|
|
||||||
let Component: React.ComponentType<React.HTMLAttributes<
|
let Component: React.ComponentType<React.HTMLAttributes<
|
||||||
HTMLDivElement
|
HTMLDivElement
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply fixed bg-black flex items-center inset-0 z-50 justify-center;
|
@apply fixed bg-primary text-primary flex items-center inset-0 z-50 justify-center;
|
||||||
background-color: rgba(0, 0, 0, 0.35);
|
background-color: rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
@apply bg-white p-12;
|
@apply bg-primary p-12 border border-accents-2;
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,7 @@ import cn from 'classnames'
|
|||||||
import { FC, useRef } from 'react'
|
import { FC, useRef } from 'react'
|
||||||
import s from './Modal.module.css'
|
import s from './Modal.module.css'
|
||||||
import { useDialog } from '@react-aria/dialog'
|
import { useDialog } from '@react-aria/dialog'
|
||||||
import {
|
import { useOverlay, usePreventScroll, useModal } from '@react-aria/overlays'
|
||||||
useOverlay,
|
|
||||||
usePreventScroll,
|
|
||||||
useModal,
|
|
||||||
OverlayProvider,
|
|
||||||
OverlayContainer,
|
|
||||||
} from '@react-aria/overlays'
|
|
||||||
import { FocusScope } from '@react-aria/focus'
|
import { FocusScope } from '@react-aria/focus'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -27,11 +21,14 @@ const Modal: FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const rootClassName = cn(s.root, className)
|
const rootClassName = cn(s.root, className)
|
||||||
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
|
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
|
||||||
usePreventScroll()
|
|
||||||
let { modalProps } = useModal()
|
let { modalProps } = useModal()
|
||||||
let { overlayProps } = useOverlay(props, ref)
|
let { overlayProps } = useOverlay(props, ref)
|
||||||
let { dialogProps } = useDialog(props, ref)
|
let { dialogProps } = useDialog(props, ref)
|
||||||
|
|
||||||
|
usePreventScroll({
|
||||||
|
isDisabled: !show,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={rootClassName}>
|
<div className={rootClassName}>
|
||||||
<FocusScope contain restoreFocus autoFocus>
|
<FocusScope contain restoreFocus autoFocus>
|
||||||
|
@ -72,7 +72,7 @@ const Sidebar: FC<Props> = ({ className, children, show = true, close }) => {
|
|||||||
leaveFrom="translate-x-0"
|
leaveFrom="translate-x-0"
|
||||||
leaveTo="translate-x-full"
|
leaveTo="translate-x-full"
|
||||||
>
|
>
|
||||||
<div className="h-full w-screen max-w-lg">
|
<div className="h-full w-screen max-w-xl">
|
||||||
<div className="h-full flex flex-col text-base bg-accents-1 shadow-xl overflow-y-auto">
|
<div className="h-full flex flex-col text-base bg-accents-1 shadow-xl overflow-y-auto">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.skeleton {
|
.skeleton {
|
||||||
@apply block rounded;
|
@apply block;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
270deg,
|
270deg,
|
||||||
var(--accents-1),
|
var(--accents-1),
|
||||||
|
@ -26,7 +26,7 @@ const getLoggedInCustomer: CustomersHandlers['getLoggedInCustomer'] = async ({
|
|||||||
res,
|
res,
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
const data = await config.fetch<GetLoggedInCustomerQuery>(
|
const { data } = await config.fetch<GetLoggedInCustomerQuery>(
|
||||||
getLoggedInCustomerQuery
|
getLoggedInCustomerQuery
|
||||||
)
|
)
|
||||||
const { customer } = data
|
const { customer } = data
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
import { FetcherError } from '@lib/commerce/utils/errors'
|
||||||
import login from '../../operations/login'
|
import login from '../../operations/login'
|
||||||
import type { LoginHandlers } from '../login'
|
import type { LoginHandlers } from '../login'
|
||||||
|
|
||||||
|
const invalidCredentials = /invalid credentials/i
|
||||||
|
|
||||||
const loginHandler: LoginHandlers['login'] = async ({
|
const loginHandler: LoginHandlers['login'] = async ({
|
||||||
res,
|
res,
|
||||||
body: { email, password },
|
body: { email, password },
|
||||||
@ -17,8 +20,28 @@ const loginHandler: LoginHandlers['login'] = async ({
|
|||||||
// Passwords must be at least 7 characters and contain both alphabetic
|
// Passwords must be at least 7 characters and contain both alphabetic
|
||||||
// and numeric characters.
|
// and numeric characters.
|
||||||
|
|
||||||
// TODO: Currently not working, fix this asap.
|
try {
|
||||||
const loginData = await login({ variables: { email, password }, config })
|
await login({ variables: { email, password }, config, res })
|
||||||
|
} catch (error) {
|
||||||
|
// Check if the email and password didn't match an existing account
|
||||||
|
if (
|
||||||
|
error instanceof FetcherError &&
|
||||||
|
invalidCredentials.test(error.message)
|
||||||
|
) {
|
||||||
|
return res.status(401).json({
|
||||||
|
data: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Cannot find an account that matches the provided credentials',
|
||||||
|
code: 'invalid_credentials',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
res.status(200).json({ data: null })
|
res.status(200).json({ data: null })
|
||||||
}
|
}
|
||||||
|
@ -18,51 +18,45 @@ const signup: SignupHandlers['signup'] = async ({
|
|||||||
// Passwords must be at least 7 characters and contain both alphabetic
|
// Passwords must be at least 7 characters and contain both alphabetic
|
||||||
// and numeric characters.
|
// and numeric characters.
|
||||||
|
|
||||||
let result: { data?: any } = {}
|
try {
|
||||||
|
await config.storeApiFetch('/v3/customers', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify([
|
||||||
|
{
|
||||||
|
first_name: firstName,
|
||||||
|
last_name: lastName,
|
||||||
|
email,
|
||||||
|
authentication: {
|
||||||
|
new_password: password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof BigcommerceApiError && error.status === 422) {
|
||||||
|
const hasEmailError = '0.email' in error.data?.errors
|
||||||
|
|
||||||
// try {
|
// If there's an error with the email, it most likely means it's duplicated
|
||||||
// result = await config.storeApiFetch('/v3/customers', {
|
if (hasEmailError) {
|
||||||
// method: 'POST',
|
return res.status(400).json({
|
||||||
// body: JSON.stringify([
|
data: null,
|
||||||
// {
|
errors: [
|
||||||
// first_name: firstName,
|
{
|
||||||
// last_name: lastName,
|
message: 'The email is already in use',
|
||||||
// email,
|
code: 'duplicated_email',
|
||||||
// authentication: {
|
},
|
||||||
// new_password: password,
|
],
|
||||||
// },
|
})
|
||||||
// },
|
}
|
||||||
// ]),
|
}
|
||||||
// })
|
|
||||||
// } catch (error) {
|
|
||||||
// if (error instanceof BigcommerceApiError && error.status === 422) {
|
|
||||||
// const hasEmailError = '0.email' in error.data?.errors
|
|
||||||
|
|
||||||
// // If there's an error with the email, it most likely means it's duplicated
|
throw error
|
||||||
// if (hasEmailError) {
|
}
|
||||||
// return res.status(400).json({
|
|
||||||
// data: null,
|
|
||||||
// errors: [
|
|
||||||
// {
|
|
||||||
// message: 'The email is already in use',
|
|
||||||
// code: 'duplicated_email',
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// throw error
|
// Login the customer right after creating it
|
||||||
// }
|
await login({ variables: { email, password }, res, config })
|
||||||
|
|
||||||
console.log('DATA', result.data)
|
res.status(200).json({ data: null })
|
||||||
|
|
||||||
// TODO: Currently not working, fix this asap.
|
|
||||||
const loginData = await login({ variables: { email, password }, config })
|
|
||||||
|
|
||||||
console.log('LOGIN DATA', loginData)
|
|
||||||
|
|
||||||
res.status(200).json({ data: result.data ?? null })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default signup
|
export default signup
|
||||||
|
@ -49,9 +49,9 @@ async function getAllProductPaths({
|
|||||||
config = getConfig(config)
|
config = getConfig(config)
|
||||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
// RecursivePartial forces the method to check for every prop in the data, which is
|
||||||
// required in case there's a custom `query`
|
// required in case there's a custom `query`
|
||||||
const data = await config.fetch<RecursivePartial<GetAllProductPathsQuery>>(
|
const { data } = await config.fetch<
|
||||||
query
|
RecursivePartial<GetAllProductPathsQuery>
|
||||||
)
|
>(query)
|
||||||
const products = data.site?.products?.edges
|
const products = data.site?.products?.edges
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -111,7 +111,7 @@ async function getAllProducts({
|
|||||||
|
|
||||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
// RecursivePartial forces the method to check for every prop in the data, which is
|
||||||
// required in case there's a custom `query`
|
// required in case there's a custom `query`
|
||||||
const data = await config.fetch<RecursivePartial<GetAllProductsQuery>>(
|
const { data } = await config.fetch<RecursivePartial<GetAllProductsQuery>>(
|
||||||
query,
|
query,
|
||||||
{ variables }
|
{ variables }
|
||||||
)
|
)
|
||||||
|
@ -71,9 +71,10 @@ async function getProduct({
|
|||||||
...vars,
|
...vars,
|
||||||
path: slug ? `/${slug}/` : vars.path!,
|
path: slug ? `/${slug}/` : vars.path!,
|
||||||
}
|
}
|
||||||
const data = await config.fetch<RecursivePartial<GetProductQuery>>(query, {
|
const { data } = await config.fetch<RecursivePartial<GetProductQuery>>(
|
||||||
variables,
|
query,
|
||||||
})
|
{ variables }
|
||||||
|
)
|
||||||
const product = data.site?.route?.node
|
const product = data.site?.route?.node
|
||||||
|
|
||||||
if (product?.__typename === 'Product') {
|
if (product?.__typename === 'Product') {
|
||||||
|
@ -90,9 +90,10 @@ async function getSiteInfo({
|
|||||||
config = getConfig(config)
|
config = getConfig(config)
|
||||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
// RecursivePartial forces the method to check for every prop in the data, which is
|
||||||
// required in case there's a custom `query`
|
// required in case there's a custom `query`
|
||||||
const data = await config.fetch<RecursivePartial<GetSiteInfoQuery>>(query, {
|
const { data } = await config.fetch<RecursivePartial<GetSiteInfoQuery>>(
|
||||||
variables,
|
query,
|
||||||
})
|
{ variables }
|
||||||
|
)
|
||||||
const categories = data.site?.categoryTree
|
const categories = data.site?.categoryTree
|
||||||
const brands = data.site?.brands?.edges
|
const brands = data.site?.brands?.edges
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import type { ServerResponse } from 'http'
|
||||||
import type {
|
import type {
|
||||||
LoginMutation,
|
LoginMutation,
|
||||||
LoginMutationVariables,
|
LoginMutationVariables,
|
||||||
} from 'lib/bigcommerce/schema'
|
} from 'lib/bigcommerce/schema'
|
||||||
import type { RecursivePartial } from '../utils/types'
|
import type { RecursivePartial } from '../utils/types'
|
||||||
|
import concatHeader from '../utils/concat-cookie'
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
import { BigcommerceConfig, getConfig } from '..'
|
||||||
|
|
||||||
export const loginMutation = /* GraphQL */ `
|
export const loginMutation = /* GraphQL */ `
|
||||||
@ -20,28 +22,49 @@ export type LoginVariables = LoginMutationVariables
|
|||||||
async function login(opts: {
|
async function login(opts: {
|
||||||
variables: LoginVariables
|
variables: LoginVariables
|
||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
|
res: ServerResponse
|
||||||
}): Promise<LoginResult>
|
}): Promise<LoginResult>
|
||||||
|
|
||||||
async function login<T extends { result?: any }, V = any>(opts: {
|
async function login<T extends { result?: any }, V = any>(opts: {
|
||||||
query: string
|
query: string
|
||||||
variables: V
|
variables: V
|
||||||
|
res: ServerResponse
|
||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
}): Promise<LoginResult<T>>
|
}): Promise<LoginResult<T>>
|
||||||
|
|
||||||
async function login({
|
async function login({
|
||||||
query = loginMutation,
|
query = loginMutation,
|
||||||
variables,
|
variables,
|
||||||
|
res: response,
|
||||||
config,
|
config,
|
||||||
}: {
|
}: {
|
||||||
query?: string
|
query?: string
|
||||||
variables: LoginVariables
|
variables: LoginVariables
|
||||||
|
res: ServerResponse
|
||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
}): Promise<LoginResult> {
|
}): Promise<LoginResult> {
|
||||||
config = getConfig(config)
|
config = getConfig(config)
|
||||||
|
|
||||||
const data = await config.fetch<RecursivePartial<LoginMutation>>(query, {
|
const { data, res } = await config.fetch<RecursivePartial<LoginMutation>>(
|
||||||
variables,
|
query,
|
||||||
})
|
{ variables }
|
||||||
|
)
|
||||||
|
// Bigcommerce returns a Set-Cookie header with the auth cookie
|
||||||
|
let cookie = res.headers.get('Set-Cookie')
|
||||||
|
|
||||||
|
if (cookie && typeof cookie === 'string') {
|
||||||
|
// In development, don't set a secure cookie or the browser will ignore it
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
cookie = cookie.replace('; Secure', '')
|
||||||
|
// SameSite=none can't be set unless the cookie is Secure
|
||||||
|
cookie = cookie.replace('; SameSite=none', '; SameSite=lax')
|
||||||
|
}
|
||||||
|
|
||||||
|
response.setHeader(
|
||||||
|
'Set-Cookie',
|
||||||
|
concatHeader(response.getHeader('Set-Cookie'), cookie)!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: data.login?.result,
|
result: data.login?.result,
|
||||||
|
14
lib/bigcommerce/api/utils/concat-cookie.ts
Normal file
14
lib/bigcommerce/api/utils/concat-cookie.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
type Header = string | number | string[] | undefined
|
||||||
|
|
||||||
|
export default function concatHeader(prev: Header, val: Header) {
|
||||||
|
if (!val) return prev
|
||||||
|
if (!prev) return val
|
||||||
|
|
||||||
|
if (Array.isArray(prev)) return prev.concat(String(val))
|
||||||
|
|
||||||
|
prev = String(prev)
|
||||||
|
|
||||||
|
if (Array.isArray(val)) return [prev].concat(val)
|
||||||
|
|
||||||
|
return [prev, String(val)]
|
||||||
|
}
|
@ -1,18 +1,22 @@
|
|||||||
import { CommerceAPIFetchOptions } from 'lib/commerce/api'
|
import { FetcherError } from '@lib/commerce/utils/errors'
|
||||||
|
import type { GraphQLFetcher } from 'lib/commerce/api'
|
||||||
import { getConfig } from '..'
|
import { getConfig } from '..'
|
||||||
import log from '@lib/logger'
|
import log from '@lib/logger'
|
||||||
|
|
||||||
export default async function fetchGraphqlApi<Q, V = any>(
|
const fetchGraphqlApi: GraphQLFetcher = async (
|
||||||
query: string,
|
query: string,
|
||||||
{ variables, preview }: CommerceAPIFetchOptions<V> = {}
|
{ variables, preview } = {},
|
||||||
): Promise<Q> {
|
fetchOptions
|
||||||
|
) => {
|
||||||
// log.warn(query)
|
// log.warn(query)
|
||||||
const config = getConfig()
|
const config = getConfig()
|
||||||
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
|
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
|
||||||
|
...fetchOptions,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${config.apiToken}`,
|
Authorization: `Bearer ${config.apiToken}`,
|
||||||
|
...fetchOptions?.headers,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
query,
|
query,
|
||||||
@ -20,22 +24,15 @@ export default async function fetchGraphqlApi<Q, V = any>(
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// console.log('HEADERS', getRawHeaders(res))
|
|
||||||
|
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
if (json.errors) {
|
if (json.errors) {
|
||||||
console.error(json.errors)
|
throw new FetcherError({
|
||||||
throw new Error('Failed to fetch BigCommerce API')
|
errors: json.errors ?? [{ message: 'Failed to fetch Bigcommerce API' }],
|
||||||
|
status: res.status,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return json.data
|
|
||||||
|
return { data: json.data, res }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRawHeaders(res: Response) {
|
export default fetchGraphqlApi
|
||||||
const headers: { [key: string]: string } = {}
|
|
||||||
|
|
||||||
res.headers.forEach((value, key) => {
|
|
||||||
headers[key] = value
|
|
||||||
})
|
|
||||||
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { HookFetcher } from '@lib/commerce/utils/types'
|
import type { HookFetcher } from '@lib/commerce/utils/types'
|
||||||
|
import { CommerceError } from '@lib/commerce/utils/errors'
|
||||||
import useCartAddItem from '@lib/commerce/cart/use-add-item'
|
import useCartAddItem from '@lib/commerce/cart/use-add-item'
|
||||||
import type { ItemBody, AddItemBody } from '../api/cart'
|
import type { ItemBody, AddItemBody } from '../api/cart'
|
||||||
import useCart, { Cart } from './use-cart'
|
import useCart, { Cart } from './use-cart'
|
||||||
@ -20,9 +21,9 @@ export const fetcher: HookFetcher<Cart, AddItemBody> = (
|
|||||||
item.quantity &&
|
item.quantity &&
|
||||||
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new CommerceError({
|
||||||
'The item quantity has to be a valid integer greater than 0'
|
message: 'The item quantity has to be a valid integer greater than 0',
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch({
|
return fetch({
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
import { HookFetcher } from '@lib/commerce/utils/types'
|
import type { HookFetcher } from '@lib/commerce/utils/types'
|
||||||
|
import { CommerceError } from '@lib/commerce/utils/errors'
|
||||||
import useCartUpdateItem from '@lib/commerce/cart/use-update-item'
|
import useCartUpdateItem from '@lib/commerce/cart/use-update-item'
|
||||||
import type { ItemBody, UpdateItemBody } from '../api/cart'
|
import type { ItemBody, UpdateItemBody } from '../api/cart'
|
||||||
import { fetcher as removeFetcher } from './use-remove-item'
|
import { fetcher as removeFetcher } from './use-remove-item'
|
||||||
@ -24,7 +25,9 @@ export const fetcher: HookFetcher<Cart | null, UpdateItemBody> = (
|
|||||||
return removeFetcher(null, { itemId }, fetch)
|
return removeFetcher(null, { itemId }, fetch)
|
||||||
}
|
}
|
||||||
} else if (item.quantity) {
|
} else if (item.quantity) {
|
||||||
throw new Error('The item quantity has to be a valid integer')
|
throw new CommerceError({
|
||||||
|
message: 'The item quantity has to be a valid integer',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch({
|
return fetch({
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
CommerceProvider as CoreCommerceProvider,
|
CommerceProvider as CoreCommerceProvider,
|
||||||
useCommerce as useCoreCommerce,
|
useCommerce as useCoreCommerce,
|
||||||
} from 'lib/commerce'
|
} from 'lib/commerce'
|
||||||
|
import { FetcherError } from '@lib/commerce/utils/errors'
|
||||||
|
|
||||||
async function getText(res: Response) {
|
async function getText(res: Response) {
|
||||||
try {
|
try {
|
||||||
@ -16,9 +17,9 @@ async function getText(res: Response) {
|
|||||||
async function getError(res: Response) {
|
async function getError(res: Response) {
|
||||||
if (res.headers.get('Content-Type')?.includes('application/json')) {
|
if (res.headers.get('Content-Type')?.includes('application/json')) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return data.errors[0]
|
return new FetcherError({ errors: data.errors, status: res.status })
|
||||||
}
|
}
|
||||||
return { message: await getText(res) }
|
return new FetcherError({ message: await getText(res), status: res.status })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bigcommerceConfig: CommerceConfig = {
|
export const bigcommerceConfig: CommerceConfig = {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { HookFetcher } from '@lib/commerce/utils/types'
|
import type { HookFetcher } from '@lib/commerce/utils/types'
|
||||||
|
import { CommerceError } from '@lib/commerce/utils/errors'
|
||||||
import useCommerceLogin from '@lib/commerce/use-login'
|
import useCommerceLogin from '@lib/commerce/use-login'
|
||||||
import type { LoginBody } from './api/customers/login'
|
import type { LoginBody } from './api/customers/login'
|
||||||
|
|
||||||
@ -16,9 +17,10 @@ export const fetcher: HookFetcher<null, LoginBody> = (
|
|||||||
fetch
|
fetch
|
||||||
) => {
|
) => {
|
||||||
if (!(email && password)) {
|
if (!(email && password)) {
|
||||||
throw new Error(
|
throw new CommerceError({
|
||||||
'A first name, last name, email and password are required to login'
|
message:
|
||||||
)
|
'A first name, last name, email and password are required to login',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch({
|
return fetch({
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { HookFetcher } from '@lib/commerce/utils/types'
|
import type { HookFetcher } from '@lib/commerce/utils/types'
|
||||||
|
import { CommerceError } from '@lib/commerce/utils/errors'
|
||||||
import useCommerceSignup from '@lib/commerce/use-signup'
|
import useCommerceSignup from '@lib/commerce/use-signup'
|
||||||
import type { SignupBody } from './api/customers/signup'
|
import type { SignupBody } from './api/customers/signup'
|
||||||
|
|
||||||
@ -16,9 +17,10 @@ export const fetcher: HookFetcher<null, SignupBody> = (
|
|||||||
fetch
|
fetch
|
||||||
) => {
|
) => {
|
||||||
if (!(firstName && lastName && email && password)) {
|
if (!(firstName && lastName && email && password)) {
|
||||||
throw new Error(
|
throw new CommerceError({
|
||||||
'A first name, last name, email and password are required to signup'
|
message:
|
||||||
)
|
'A first name, last name, email and password are required to signup',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch({
|
return fetch({
|
||||||
|
@ -3,14 +3,29 @@ export interface CommerceAPIConfig {
|
|||||||
apiToken: string
|
apiToken: string
|
||||||
cartCookie: string
|
cartCookie: string
|
||||||
cartCookieMaxAge: number
|
cartCookieMaxAge: number
|
||||||
fetch<Q, V = any>(
|
fetch<Data = any, Variables = any>(
|
||||||
query: string,
|
query: string,
|
||||||
queryData?: CommerceAPIFetchOptions<V>
|
queryData?: CommerceAPIFetchOptions<Variables>,
|
||||||
): Promise<Q>
|
fetchOptions?: RequestInit
|
||||||
|
): Promise<GraphQLFetcherResult<Data>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommerceAPIFetchOptions<V> {
|
export type GraphQLFetcher<
|
||||||
variables?: V
|
Data extends GraphQLFetcherResult = GraphQLFetcherResult,
|
||||||
|
Variables = any
|
||||||
|
> = (
|
||||||
|
query: string,
|
||||||
|
queryData?: CommerceAPIFetchOptions<Variables>,
|
||||||
|
fetchOptions?: RequestInit
|
||||||
|
) => Promise<Data>
|
||||||
|
|
||||||
|
export interface GraphQLFetcherResult<Data = any> {
|
||||||
|
data: Data
|
||||||
|
res: Response
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommerceAPIFetchOptions<Variables> {
|
||||||
|
variables?: Variables
|
||||||
preview?: boolean
|
preview?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
40
lib/commerce/utils/errors.ts
Normal file
40
lib/commerce/utils/errors.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
export type ErrorData = {
|
||||||
|
message: string
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ErrorProps = {
|
||||||
|
code?: string
|
||||||
|
} & (
|
||||||
|
| { message: string; errors?: never }
|
||||||
|
| { message?: never; errors: ErrorData[] }
|
||||||
|
)
|
||||||
|
|
||||||
|
export class CommerceError extends Error {
|
||||||
|
code?: string
|
||||||
|
errors: ErrorData[]
|
||||||
|
|
||||||
|
constructor({ message, code, errors }: ErrorProps) {
|
||||||
|
const error: ErrorData = message
|
||||||
|
? { message, ...(code ? { code } : {}) }
|
||||||
|
: errors![0]
|
||||||
|
|
||||||
|
super(error.message)
|
||||||
|
this.errors = message ? [error] : errors!
|
||||||
|
|
||||||
|
if (error.code) this.code = error.code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FetcherError extends CommerceError {
|
||||||
|
status: number
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
options: {
|
||||||
|
status: number
|
||||||
|
} & ErrorProps
|
||||||
|
) {
|
||||||
|
super(options)
|
||||||
|
this.status = options.status
|
||||||
|
}
|
||||||
|
}
|
@ -35,6 +35,7 @@
|
|||||||
"next-seo": "^4.11.0",
|
"next-seo": "^4.11.0",
|
||||||
"next-themes": "^0.0.4",
|
"next-themes": "^0.0.4",
|
||||||
"nextjs-progressbar": "^0.0.6",
|
"nextjs-progressbar": "^0.0.6",
|
||||||
|
"postcss-import": "^13.0.0",
|
||||||
"postcss-nesting": "^7.0.1",
|
"postcss-nesting": "^7.0.1",
|
||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
"react-aria": "^3.0.0",
|
"react-aria": "^3.0.0",
|
||||||
@ -44,7 +45,8 @@
|
|||||||
"react-swipeable-views": "^0.13.9",
|
"react-swipeable-views": "^0.13.9",
|
||||||
"react-swipeable-views-utils": "^0.14.0-alpha.0",
|
"react-swipeable-views-utils": "^0.14.0-alpha.0",
|
||||||
"react-ticker": "^1.2.2",
|
"react-ticker": "^1.2.2",
|
||||||
"swr": "^0.3.3"
|
"swr": "^0.3.3",
|
||||||
|
"tailwindcss": "^1.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^1.17.10",
|
"@graphql-codegen/cli": "^1.17.10",
|
||||||
@ -66,7 +68,6 @@
|
|||||||
"postcss-flexbugs-fixes": "^4.2.1",
|
"postcss-flexbugs-fixes": "^4.2.1",
|
||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-preset-env": "^6.7.0",
|
||||||
"prettier": "^2.1.2",
|
"prettier": "^2.1.2",
|
||||||
"tailwindcss": "^1.8.10",
|
|
||||||
"typescript": "^4.0.3"
|
"typescript": "^4.0.3"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
import '@assets/global.css'
|
import '@assets/global.css'
|
||||||
import '@assets/tailwind.css'
|
import '@assets/tailwind.css'
|
||||||
import '@assets/utils.css'
|
import '@assets/utils.css'
|
||||||
import 'keen-slider/keen-slider.min.css'
|
import 'keen-slider/keen-slider.min.css'
|
||||||
|
=======
|
||||||
|
import '@assets/main.css'
|
||||||
|
>>>>>>> master
|
||||||
|
|
||||||
// To be removed
|
// To be removed
|
||||||
import 'animate.css'
|
import 'animate.css'
|
||||||
|
@ -70,7 +70,7 @@ export default function Home({
|
|||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Marquee variant="secondary">
|
<Marquee variant="secondary">
|
||||||
{bestSelling.slice(0, 3).map(({ node }) => (
|
{bestSelling.slice(3, 6).map(({ node }) => (
|
||||||
<ProductCard key={node.path} product={node} variant="slim" />
|
<ProductCard key={node.path} product={node} variant="slim" />
|
||||||
))}
|
))}
|
||||||
</Marquee>
|
</Marquee>
|
||||||
|
@ -1,28 +1,73 @@
|
|||||||
|
import useSignup from '@lib/bigcommerce/use-signup'
|
||||||
import { Layout } from '@components/core'
|
import { Layout } from '@components/core'
|
||||||
import { Logo, Modal, Button } from '@components/ui'
|
import { Logo, Modal, Button } from '@components/ui'
|
||||||
|
import useLogin from '@lib/bigcommerce/use-login'
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
|
const signup = useSignup()
|
||||||
|
const login = useLogin()
|
||||||
|
// TODO: use this method. It can take more than 5 seconds to do a signup
|
||||||
|
const handleSignup = async () => {
|
||||||
|
// TODO: validate the password and email before calling the signup
|
||||||
|
// Passwords must be at least 7 characters and contain both alphabetic
|
||||||
|
// and numeric characters.
|
||||||
|
try {
|
||||||
|
await signup({
|
||||||
|
// This account already exists, so it will throw the "duplicated_email" error
|
||||||
|
email: 'luis@vercel.com',
|
||||||
|
firstName: 'Luis',
|
||||||
|
lastName: 'Alvarez',
|
||||||
|
password: 'luis123',
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'duplicated_email') {
|
||||||
|
// TODO: handle duplicated email
|
||||||
|
}
|
||||||
|
// Show a generic error saying that something bad happened, try again later
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
// TODO: validate the password and email before calling the signup
|
||||||
|
// Passwords must be at least 7 characters and contain both alphabetic
|
||||||
|
// and numeric characters.
|
||||||
|
try {
|
||||||
|
await login({
|
||||||
|
email: 'luis@vercel.com',
|
||||||
|
// This is an invalid password so it will throw the "invalid_credentials" error
|
||||||
|
password: 'luis1234', // will work with `luis123`
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'invalid_credentials') {
|
||||||
|
// The email and password didn't match an existing account
|
||||||
|
}
|
||||||
|
// Show a generic error saying that something bad happened, try again later
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-20">
|
<div className="pb-20">
|
||||||
<Modal close={() => {}}>
|
<Modal close={() => {}}>
|
||||||
<div className="h-80 w-80 flex flex-col justify-between py-3 px-3">
|
<div className="h-80 w-80 flex flex-col justify-between py-3 px-3">
|
||||||
<div className="flex justify-center pb-12">
|
<div className="flex justify-center pb-12 ">
|
||||||
<Logo width="64px" height="64px" />
|
<Logo width="64px" height="64px" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
<div className="border border-accents-3 text-accents-6">
|
<div className="border border-accents-3 text-accents-6">
|
||||||
<input
|
<input
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
className="focus:outline-none focus:shadow-outline-gray border-none py-2 px-6 w-full appearance-none transition duration-150 ease-in-out placeholder-accents-5 pr-10"
|
className="focus:outline-none bg-primary focus:shadow-outline-gray border-none py-2 px-6 w-full appearance-none transition duration-150 ease-in-out placeholder-accents-5 pr-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="border border-accents-3 text-accents-6">
|
<div className="border border-accents-3 text-accents-6">
|
||||||
<input
|
<input
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
className="focus:outline-none focus:shadow-outline-gray border-none py-2 px-6 w-full appearance-none transition duration-150 ease-in-out placeholder-accents-5 pr-10"
|
className="bg-primary focus:outline-none focus:shadow-outline-gray border-none py-2 px-6 w-full appearance-none transition duration-150 ease-in-out placeholder-accents-5 pr-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="slim">Log In</Button>
|
<Button variant="slim" onClick={handleSignup}>
|
||||||
|
Log In
|
||||||
|
</Button>
|
||||||
<span className="pt-3 text-center text-sm">
|
<span className="pt-3 text-center text-sm">
|
||||||
<span className="text-accents-7">Don't have an account?</span>
|
<span className="text-accents-7">Don't have an account?</span>
|
||||||
{` `}
|
{` `}
|
||||||
|
BIN
public/flag-us.png
Normal file
BIN
public/flag-us.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 762 B |
BIN
public/vercel.png
Normal file
BIN
public/vercel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
@ -6,6 +6,9 @@ module.exports = {
|
|||||||
purge: ['./components/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}'],
|
purge: ['./components/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
maxWidth: {
|
||||||
|
'8xl': '1920px',
|
||||||
|
},
|
||||||
backgroundOpacity: {
|
backgroundOpacity: {
|
||||||
075: '0.75',
|
075: '0.75',
|
||||||
},
|
},
|
||||||
@ -28,6 +31,7 @@ module.exports = {
|
|||||||
'accents-8': 'var(--accents-8)',
|
'accents-8': 'var(--accents-8)',
|
||||||
'accents-9': 'var(--accents-9)',
|
'accents-9': 'var(--accents-9)',
|
||||||
violet: 'var(--violet)',
|
violet: 'var(--violet)',
|
||||||
|
'violet-light': 'var(--violet-light)',
|
||||||
pink: 'var(--pink)',
|
pink: 'var(--pink)',
|
||||||
cyan: 'var(--cyan)',
|
cyan: 'var(--cyan)',
|
||||||
blue: 'var(--blue)',
|
blue: 'var(--blue)',
|
||||||
|
27
yarn.lock
27
yarn.lock
@ -6444,6 +6444,11 @@ picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
|
||||||
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
|
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
|
||||||
|
|
||||||
|
pify@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||||
|
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
|
||||||
|
|
||||||
pkg-dir@^4.1.0, pkg-dir@^4.2.0:
|
pkg-dir@^4.1.0, pkg-dir@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
|
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
|
||||||
@ -6618,6 +6623,15 @@ postcss-image-set-function@^3.0.1:
|
|||||||
postcss "^7.0.2"
|
postcss "^7.0.2"
|
||||||
postcss-values-parser "^2.0.0"
|
postcss-values-parser "^2.0.0"
|
||||||
|
|
||||||
|
postcss-import@^13.0.0:
|
||||||
|
version "13.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-13.0.0.tgz#d6960cd9e3de5464743b04dd8cd9d870662f8b8c"
|
||||||
|
integrity sha512-LPUbm3ytpYopwQQjqgUH4S3EM/Gb9QsaSPP/5vnoi+oKVy3/mIk2sc0Paqw7RL57GpScm9MdIMUypw2znWiBpg==
|
||||||
|
dependencies:
|
||||||
|
postcss-value-parser "^4.0.0"
|
||||||
|
read-cache "^1.0.0"
|
||||||
|
resolve "^1.1.7"
|
||||||
|
|
||||||
postcss-initial@^3.0.0:
|
postcss-initial@^3.0.0:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.2.tgz#f018563694b3c16ae8eaabe3c585ac6319637b2d"
|
resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.2.tgz#f018563694b3c16ae8eaabe3c585ac6319637b2d"
|
||||||
@ -6832,7 +6846,7 @@ postcss-value-parser@^3.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
|
||||||
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
|
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
|
||||||
|
|
||||||
postcss-value-parser@^4.1.0:
|
postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
|
||||||
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
|
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
|
||||||
@ -7144,6 +7158,13 @@ react@^16.14.0:
|
|||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
|
read-cache@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
|
||||||
|
integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=
|
||||||
|
dependencies:
|
||||||
|
pify "^2.3.0"
|
||||||
|
|
||||||
read-pkg-up@^7.0.1:
|
read-pkg-up@^7.0.1:
|
||||||
version "7.0.1"
|
version "7.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
|
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
|
||||||
@ -7424,7 +7445,7 @@ resolve-url@^0.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
|
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
|
||||||
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
|
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
|
||||||
|
|
||||||
resolve@^1.10.0, resolve@^1.14.2, resolve@^1.3.2, resolve@^1.8.1:
|
resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.3.2, resolve@^1.8.1:
|
||||||
version "1.18.1"
|
version "1.18.1"
|
||||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130"
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130"
|
||||||
integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==
|
integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==
|
||||||
@ -8022,7 +8043,7 @@ symbol-observable@^1.0.4, symbol-observable@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||||
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
||||||
|
|
||||||
tailwindcss@^1.8.10:
|
tailwindcss@^1.9:
|
||||||
version "1.9.5"
|
version "1.9.5"
|
||||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.9.5.tgz#3339b790a68bc1f09a8efd8eb94cb05aed5235c2"
|
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-1.9.5.tgz#3339b790a68bc1f09a8efd8eb94cb05aed5235c2"
|
||||||
integrity sha512-Je5t1fAfyW333YTpSxF+8uJwbnrkpyBskDtZYgSMMKQbNp6QUhEKJ4g/JIevZjD2Zidz9VxLraEUq/yWOx6nQg==
|
integrity sha512-Je5t1fAfyW333YTpSxF+8uJwbnrkpyBskDtZYgSMMKQbNp6QUhEKJ4g/JIevZjD2Zidz9VxLraEUq/yWOx6nQg==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user