mirror of
https://github.com/vercel/commerce.git
synced 2025-07-22 20:26:49 +00:00
Merge branch 'master' into arzafran/ui-tweaks
This commit is contained in:
89
components/auth/ForgotPassword.tsx
Normal file
89
components/auth/ForgotPassword.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { FC, useEffect, useState, useCallback } from 'react'
|
||||
import { validate } from 'email-validator'
|
||||
import { Info } from'@components/icons'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { Logo, Button, Input } from '@components/ui'
|
||||
import useSignup from '@lib/bigcommerce/use-signup'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const ForgotPassword: FC<Props> = () => {
|
||||
// Form State
|
||||
const [email, setEmail] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [dirty, setDirty] = useState(false)
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
|
||||
const signup = useSignup()
|
||||
const { setModalView, closeModal } = useUI()
|
||||
|
||||
const handleSignup = async () => {
|
||||
if (!dirty && !disabled) {
|
||||
setDirty(true)
|
||||
handleValidation()
|
||||
}
|
||||
|
||||
// try {
|
||||
// setLoading(true)
|
||||
// setMessage('')
|
||||
// await signup({
|
||||
// email,
|
||||
// })
|
||||
// setLoading(false)
|
||||
// closeModal()
|
||||
// } catch ({ errors }) {
|
||||
// setMessage(errors[0].message)
|
||||
// setLoading(false)
|
||||
// }
|
||||
}
|
||||
|
||||
const handleValidation = useCallback(() => {
|
||||
// Unable to send form unless fields are valid.
|
||||
if (dirty) {
|
||||
setDisabled(!validate(email))
|
||||
}
|
||||
}, [email, dirty])
|
||||
|
||||
useEffect(() => {
|
||||
handleValidation()
|
||||
}, [handleValidation])
|
||||
|
||||
return (
|
||||
<div className="w-80 flex flex-col justify-between p-3">
|
||||
<div className="flex justify-center pb-12 ">
|
||||
<Logo width="64px" height="64px" />
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4">
|
||||
{message && (
|
||||
<div className="text-red border border-red p-3">{message}</div>
|
||||
)}
|
||||
|
||||
<Input placeholder="Email" onChange={setEmail} />
|
||||
<div className="pt-2 w-full flex flex-col">
|
||||
<Button
|
||||
variant="slim"
|
||||
onClick={() => handleSignup()}
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
>
|
||||
Recover Password
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<span className="pt-3 text-center text-sm">
|
||||
<span className="text-accents-7">Do you have an account?</span>
|
||||
{` `}
|
||||
<a
|
||||
className="text-accent-9 font-bold hover:underline cursor-pointer"
|
||||
onClick={() => setModalView('LOGIN_VIEW')}
|
||||
>
|
||||
Log In
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ForgotPassword
|
99
components/auth/LoginView.tsx
Normal file
99
components/auth/LoginView.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { FC, useEffect, useState, useCallback } from 'react'
|
||||
import { Logo, Modal, Button, Input } from '@components/ui'
|
||||
import useLogin from '@lib/bigcommerce/use-login'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { validate } from 'email-validator'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const LoginView: FC<Props> = () => {
|
||||
// Form State
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [dirty, setDirty] = useState(false)
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
const { setModalView, closeModal } = useUI()
|
||||
|
||||
const login = useLogin()
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!dirty && !disabled) {
|
||||
setDirty(true)
|
||||
handleValidation()
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setMessage('')
|
||||
await login({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
setLoading(false)
|
||||
closeModal()
|
||||
} catch ({ errors }) {
|
||||
setMessage(errors[0].message)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleValidation = useCallback(() => {
|
||||
// Test for Alphanumeric password
|
||||
const validPassword = /^(?=.*[a-zA-Z])(?=.*[0-9])/.test(password)
|
||||
|
||||
// Unable to send form unless fields are valid.
|
||||
if (dirty) {
|
||||
setDisabled(!validate(email) || password.length < 7 || !validPassword)
|
||||
}
|
||||
}, [email, password, dirty])
|
||||
|
||||
useEffect(() => {
|
||||
handleValidation()
|
||||
}, [handleValidation])
|
||||
|
||||
return (
|
||||
<div className="w-80 flex flex-col justify-between p-3">
|
||||
<div className="flex justify-center pb-12 ">
|
||||
<Logo width="64px" height="64px" />
|
||||
</div>
|
||||
<div className="flex flex-col space-y-3">
|
||||
{message && (
|
||||
<div className="text-red border border-red p-3">
|
||||
{message}. Did you {` `}
|
||||
<a
|
||||
className="text-accent-9 inline font-bold hover:underline cursor-pointer"
|
||||
onClick={() => setModalView('FORGOT_VIEW')}
|
||||
>
|
||||
forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<Input placeholder="Email" onChange={setEmail} />
|
||||
<Input placeholder="Password" onChange={setPassword} />
|
||||
|
||||
<Button
|
||||
variant="slim"
|
||||
onClick={() => handleLogin()}
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
>
|
||||
Log In
|
||||
</Button>
|
||||
<div className="pt-1 text-center text-sm">
|
||||
<span className="text-accents-7">Don't have an account?</span>
|
||||
{` `}
|
||||
<a
|
||||
className="text-accent-9 font-bold hover:underline cursor-pointer"
|
||||
onClick={() => setModalView('SIGNUP_VIEW')}
|
||||
>
|
||||
Sign Up
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginView
|
109
components/auth/SignUpView.tsx
Normal file
109
components/auth/SignUpView.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { FC, useEffect, useState, useCallback } from 'react'
|
||||
import { validate } from 'email-validator'
|
||||
import { Info } from '@components/icons'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { Logo, Button, Input } from '@components/ui'
|
||||
import useSignup from '@lib/bigcommerce/use-signup'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const SignUpView: FC<Props> = () => {
|
||||
// Form State
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [firstName, setFirstName] = useState('')
|
||||
const [lastName, setLastName] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [dirty, setDirty] = useState(false)
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
|
||||
const signup = useSignup()
|
||||
const { setModalView, closeModal } = useUI()
|
||||
|
||||
const handleSignup = async () => {
|
||||
if (!dirty && !disabled) {
|
||||
setDirty(true)
|
||||
handleValidation()
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setMessage('')
|
||||
await signup({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
})
|
||||
setLoading(false)
|
||||
closeModal()
|
||||
} catch ({ errors }) {
|
||||
setMessage(errors[0].message)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleValidation = useCallback(() => {
|
||||
// Test for Alphanumeric password
|
||||
const validPassword = /^(?=.*[a-zA-Z])(?=.*[0-9])/.test(password)
|
||||
|
||||
// Unable to send form unless fields are valid.
|
||||
if (dirty) {
|
||||
setDisabled(!validate(email) || password.length < 7 || !validPassword)
|
||||
}
|
||||
}, [email, password, dirty])
|
||||
|
||||
useEffect(() => {
|
||||
handleValidation()
|
||||
}, [handleValidation])
|
||||
|
||||
return (
|
||||
<div className="w-96 flex flex-col justify-between p-3">
|
||||
<div className="flex justify-center pb-12 ">
|
||||
<Logo width="64px" height="64px" />
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4">
|
||||
{message && (
|
||||
<div className="text-red border border-red p-3">{message}</div>
|
||||
)}
|
||||
<Input placeholder="First Name" onChange={setFirstName} />
|
||||
<Input placeholder="Last Name" onChange={setLastName} />
|
||||
<Input placeholder="Email" onChange={setEmail} />
|
||||
<Input placeholder="Password" onChange={setPassword} />
|
||||
<span className="text-accents-8">
|
||||
<span className="inline-block align-middle ">
|
||||
<Info width="15" height="15" />
|
||||
</span>{' '}
|
||||
<span className="leading-6 text-sm">
|
||||
<strong>Info</strong>: Passwords must be longer than 7 chars and
|
||||
include numbers.{' '}
|
||||
</span>
|
||||
</span>
|
||||
<div className="pt-2 w-full flex flex-col">
|
||||
<Button
|
||||
variant="slim"
|
||||
onClick={() => handleSignup()}
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
>
|
||||
Sign Up
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<span className="pt-1 text-center text-sm">
|
||||
<span className="text-accents-7">Do you have an account?</span>
|
||||
{` `}
|
||||
<a
|
||||
className="text-accent-9 font-bold hover:underline cursor-pointer"
|
||||
onClick={() => setModalView('LOGIN_VIEW')}
|
||||
>
|
||||
Log In
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignUpView
|
3
components/auth/index.ts
Normal file
3
components/auth/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as LoginView } from './LoginView'
|
||||
export { default as SignUpView } from './SignUpView'
|
||||
export { default as ForgotPassword } from './ForgotPassword'
|
@@ -10,8 +10,9 @@
|
||||
|
||||
.productImage {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -10px;
|
||||
top: 15px;
|
||||
transform: scale(1.9);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 30% !important;
|
||||
top: 30% !important;
|
||||
}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { ChangeEvent, useEffect, useState } from 'react'
|
||||
import s from './CartItem.module.css'
|
||||
import Image from 'next/image'
|
||||
import { Trash, Plus, Minus } from '@components/icon'
|
||||
import Link from 'next/link'
|
||||
import { ChangeEvent, useEffect, useState } from 'react'
|
||||
import { Trash, Plus, Minus } from '@components/icons'
|
||||
import usePrice from '@lib/bigcommerce/use-price'
|
||||
import useUpdateItem from '@lib/bigcommerce/cart/use-update-item'
|
||||
import useRemoveItem from '@lib/bigcommerce/cart/use-remove-item'
|
||||
import s from './CartItem.module.css'
|
||||
|
||||
const CartItem = ({
|
||||
item,
|
||||
@@ -55,18 +56,26 @@ const CartItem = ({
|
||||
}, [item.quantity])
|
||||
|
||||
return (
|
||||
<li className="flex flex-row space-x-8 py-6">
|
||||
<div className="w-12 h-12 bg-violet relative overflow-hidden">
|
||||
<li className="flex flex-row space-x-8 py-8">
|
||||
<div className="w-16 h-16 bg-violet relative overflow-hidden">
|
||||
<Image
|
||||
className={s.productImage}
|
||||
src={item.image_url}
|
||||
width={60}
|
||||
height={60}
|
||||
width={150}
|
||||
height={150}
|
||||
alt="Product Image"
|
||||
// The cart item image is already optimized and very small in size
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-between text-base">
|
||||
<span className="font-bold mb-3">{item.name}</span>
|
||||
<div className="flex-1 flex flex-col text-base">
|
||||
{/** TODO: Replace this. No `path` found at Cart */}
|
||||
<Link href={`/product/${item.url.split('/')[3]}`}>
|
||||
<span className="font-bold mb-5 text-lg cursor-pointer">
|
||||
{item.name}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center">
|
||||
<button type="button" onClick={() => increaseQuantity(-1)}>
|
||||
<Minus width={18} height={18} />
|
||||
|
@@ -2,7 +2,7 @@ import { FC } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { UserNav } from '@components/core'
|
||||
import { Button } from '@components/ui'
|
||||
import { ArrowLeft, Bag, Cross, Check } from '@components/icon'
|
||||
import { ArrowLeft, Bag, Cross, Check } from '@components/icons'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import useCart from '@lib/bigcommerce/cart/use-cart'
|
||||
import usePrice from '@lib/bigcommerce/use-price'
|
||||
|
@@ -1 +1,2 @@
|
||||
export { default as CartSidebarView } from './CartSidebarView'
|
||||
export { default as CartItem } from './CartItem'
|
||||
|
42
components/core/EnhancedImage/EnhancedImage.tsx
Normal file
42
components/core/EnhancedImage/EnhancedImage.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { FC } from 'react'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
import Image from 'next/image'
|
||||
|
||||
type Props = Omit<
|
||||
JSX.IntrinsicElements['img'],
|
||||
'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading'
|
||||
> & {
|
||||
src: string
|
||||
quality?: string
|
||||
priority?: boolean
|
||||
loading?: readonly ['lazy', 'eager', undefined]
|
||||
unoptimized?: boolean
|
||||
} & (
|
||||
| {
|
||||
width: number | string
|
||||
height: number | string
|
||||
unsized?: false
|
||||
}
|
||||
| {
|
||||
width?: number | string
|
||||
height?: number | string
|
||||
unsized: true
|
||||
}
|
||||
)
|
||||
|
||||
const EnhancedImage: FC<Props & JSX.IntrinsicElements['img']> = ({
|
||||
...props
|
||||
}) => {
|
||||
const [ref] = useInView({
|
||||
triggerOnce: true,
|
||||
rootMargin: '220px 0px',
|
||||
})
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Image {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EnhancedImage
|
1
components/core/EnhancedImage/index.ts
Normal file
1
components/core/EnhancedImage/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './EnhancedImage'
|
@@ -1,11 +1,13 @@
|
||||
import { FC } from 'react'
|
||||
import cn from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import getSlug from '@utils/get-slug'
|
||||
import { Github } from '@components/icon'
|
||||
import { Logo, Container } from '@components/ui'
|
||||
import { useRouter } from 'next/router'
|
||||
import type { Page } from '@lib/bigcommerce/api/operations/get-all-pages'
|
||||
import getSlug from '@utils/get-slug'
|
||||
import { Github } from '@components/icons'
|
||||
import { Logo, Container } from '@components/ui'
|
||||
import { I18nWidget } from '@components/core'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any
|
||||
@@ -15,8 +17,8 @@ interface Props {
|
||||
const LEGAL_PAGES = ['terms-of-use', 'shipping-returns', 'privacy-policy']
|
||||
|
||||
const Footer: FC<Props> = ({ className, pages }) => {
|
||||
const { sitePages, legalPages } = usePages(pages)
|
||||
const rootClassName = cn(className)
|
||||
const { sitePages, legalPages } = getPages(pages)
|
||||
|
||||
return (
|
||||
<footer className={rootClassName}>
|
||||
@@ -36,21 +38,21 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
||||
<ul className="flex flex-initial flex-col md:flex-1">
|
||||
<li className="py-3 md:py-0 md:pb-4">
|
||||
<Link href="/">
|
||||
<a className="text-gray-400 hover:text-white transition ease-in-out duration-150">
|
||||
<a className="text-accent-3 hover:text-white transition ease-in-out duration-150">
|
||||
Home
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="py-3 md:py-0 md:pb-4">
|
||||
<Link href="/">
|
||||
<a className="text-gray-400 hover:text-white transition ease-in-out duration-150">
|
||||
<a className="text-accent-3 hover:text-white transition ease-in-out duration-150">
|
||||
Careers
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="py-3 md:py-0 md:pb-4">
|
||||
<Link href="/blog">
|
||||
<a className="text-gray-400 hover:text-white transition ease-in-out duration-150">
|
||||
<a className="text-accent-3 hover:text-white transition ease-in-out duration-150">
|
||||
Blog
|
||||
</a>
|
||||
</Link>
|
||||
@@ -58,7 +60,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
||||
{sitePages.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-150">
|
||||
<a className="text-accent-3 hover:text-white transition ease-in-out duration-150">
|
||||
{page.name}
|
||||
</a>
|
||||
</Link>
|
||||
@@ -71,7 +73,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
||||
{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-150">
|
||||
<a className="text-accent-3 hover:text-white transition ease-in-out duration-150">
|
||||
{page.name}
|
||||
</a>
|
||||
</Link>
|
||||
@@ -90,9 +92,9 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
||||
<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">
|
||||
<div className="flex items-center">
|
||||
<span className="text-accent-3">Crafted by</span>
|
||||
<a href="https://vercel.com" aria-label="Vercel.com Link">
|
||||
<img
|
||||
src="/vercel.png"
|
||||
alt="Vercel.com Logo"
|
||||
@@ -106,18 +108,22 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
||||
)
|
||||
}
|
||||
|
||||
function getPages(pages?: Page[]) {
|
||||
function usePages(pages?: Page[]) {
|
||||
const { locale } = useRouter()
|
||||
const sitePages: Page[] = []
|
||||
const legalPages: Page[] = []
|
||||
|
||||
if (pages) {
|
||||
pages.forEach((page) => {
|
||||
if (page.url) {
|
||||
if (LEGAL_PAGES.includes(getSlug(page.url))) {
|
||||
legalPages.push(page)
|
||||
} else {
|
||||
sitePages.push(page)
|
||||
}
|
||||
const slug = page.url && getSlug(page.url)
|
||||
|
||||
if (!slug) return
|
||||
if (locale && !slug.startsWith(`${locale}/`)) return
|
||||
|
||||
if (isLegalPage(slug, locale)) {
|
||||
legalPages.push(page)
|
||||
} else {
|
||||
sitePages.push(page)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -128,6 +134,11 @@ function getPages(pages?: Page[]) {
|
||||
}
|
||||
}
|
||||
|
||||
const isLegalPage = (slug: string, locale?: string) =>
|
||||
locale
|
||||
? LEGAL_PAGES.some((p) => `${locale}/${p}` === slug)
|
||||
: LEGAL_PAGES.includes(slug)
|
||||
|
||||
// Sort pages by the sort order assigned in the BC dashboard
|
||||
function bySortOrder(a: Page, b: Page) {
|
||||
return (a.sort_order ?? 0) - (b.sort_order ?? 0)
|
||||
|
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply h-10 px-2 rounded-md border border-accents-2 flex items-center space-x-2 justify-center;
|
||||
@apply h-10 px-2 rounded-md border border-accents-2 flex items-center space-x-2 justify-center outline-none focus:outline-none;
|
||||
}
|
||||
|
||||
.dropdownMenu {
|
||||
|
@@ -1,35 +1,48 @@
|
||||
import { FC } from 'react'
|
||||
import s from './I18nWidget.module.css'
|
||||
import { Menu } from '@headlessui/react'
|
||||
import { DoubleChevron } from '@components/icon'
|
||||
import cn from 'classnames'
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import { Menu } from '@headlessui/react'
|
||||
import { DoubleChevron } from '@components/icons'
|
||||
import s from './I18nWidget.module.css'
|
||||
|
||||
const LOCALES_MAP: Record<string, string> = {
|
||||
es: 'Español',
|
||||
'en-US': 'English',
|
||||
}
|
||||
|
||||
const I18nWidget: FC = () => {
|
||||
const { locale, locales, defaultLocale = 'en-US' } = useRouter()
|
||||
const options = locales?.filter((val) => val !== locale)
|
||||
|
||||
return (
|
||||
<nav className={s.root}>
|
||||
<Menu>
|
||||
<Menu.Button className={s.button}>
|
||||
<img className="" src="/flag-us.png" />
|
||||
<span>English</span>
|
||||
<span className="">
|
||||
<DoubleChevron />
|
||||
</span>
|
||||
<Menu.Button className={s.button} aria-label="Language selector">
|
||||
<img className="" src="/flag-us.png" alt="US Flag" />
|
||||
<span>{LOCALES_MAP[locale || defaultLocale]}</span>
|
||||
{options && (
|
||||
<span className="">
|
||||
<DoubleChevron />
|
||||
</span>
|
||||
)}
|
||||
</Menu.Button>
|
||||
<Menu.Items className={s.dropdownMenu}>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
className={cn(s.item, { [s.active]: active })}
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
Español
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
|
||||
{options?.length ? (
|
||||
<Menu.Items className={s.dropdownMenu}>
|
||||
{options.map((locale) => (
|
||||
<Menu.Item key={locale}>
|
||||
{({ active }) => (
|
||||
<Link href="/" locale={locale}>
|
||||
<a className={cn(s.item, { [s.active]: active })}>
|
||||
{LOCALES_MAP[locale]}
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
) : null}
|
||||
</Menu>
|
||||
</nav>
|
||||
)
|
||||
|
@@ -1,14 +1,16 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useRouter } from 'next/router'
|
||||
import type { Page } from '@lib/bigcommerce/api/operations/get-all-pages'
|
||||
import { CommerceProvider } from '@lib/bigcommerce'
|
||||
import { Navbar, Featurebar, Footer } from '@components/core'
|
||||
import { Container, Sidebar } from '@components/ui'
|
||||
import Button from '@components/ui/Button'
|
||||
import { CartSidebarView } from '@components/cart'
|
||||
import { Container, Sidebar, Button, Modal, Toast } from '@components/ui'
|
||||
import { Navbar, Featurebar, Footer } from '@components/core'
|
||||
import { LoginView, SignUpView, ForgotPassword } from '@components/auth'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import s from './Layout.module.css'
|
||||
import { usePreventScroll } from '@react-aria/overlays'
|
||||
import s from './Layout.module.css'
|
||||
import debounce from 'lodash.debounce'
|
||||
interface Props {
|
||||
pageProps: {
|
||||
pages?: Page[]
|
||||
@@ -16,32 +18,42 @@ interface Props {
|
||||
}
|
||||
|
||||
const Layout: FC<Props> = ({ children, pageProps }) => {
|
||||
const { displaySidebar, displayDropdown, closeSidebar } = useUI()
|
||||
const {
|
||||
displaySidebar,
|
||||
displayModal,
|
||||
closeSidebar,
|
||||
closeModal,
|
||||
modalView,
|
||||
toastText,
|
||||
closeToast,
|
||||
displayToast,
|
||||
} = useUI()
|
||||
const [acceptedCookies, setAcceptedCookies] = useState(false)
|
||||
const [hasScrolled, setHasScrolled] = useState(false)
|
||||
const { locale = 'en-US' } = useRouter()
|
||||
|
||||
// TODO: Update code, add throttle and more.
|
||||
useEffect(() => {
|
||||
const offset = 0
|
||||
function handleScroll() {
|
||||
usePreventScroll({
|
||||
isDisabled: !(displaySidebar || displayModal),
|
||||
})
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
debounce(() => {
|
||||
const offset = 0
|
||||
const { scrollTop } = document.documentElement
|
||||
if (scrollTop > offset) setHasScrolled(true)
|
||||
else setHasScrolled(false)
|
||||
}
|
||||
document.addEventListener('scroll', handleScroll)
|
||||
}, 1)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('scroll', handleScroll)
|
||||
return () => {
|
||||
document.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [])
|
||||
|
||||
console.log(displaySidebar, displayDropdown)
|
||||
usePreventScroll({
|
||||
isDisabled: !displaySidebar,
|
||||
})
|
||||
}, [handleScroll])
|
||||
|
||||
return (
|
||||
<CommerceProvider locale="en-us">
|
||||
<CommerceProvider locale={locale}>
|
||||
<div className={cn(s.root)}>
|
||||
<header
|
||||
className={cn(
|
||||
@@ -55,11 +67,15 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
|
||||
</header>
|
||||
<main className="fit">{children}</main>
|
||||
<Footer pages={pageProps.pages} />
|
||||
|
||||
<Sidebar open={displaySidebar} onClose={closeSidebar}>
|
||||
<CartSidebarView />
|
||||
</Sidebar>
|
||||
|
||||
<Modal open={displayModal} onClose={closeModal}>
|
||||
{modalView === 'LOGIN_VIEW' && <LoginView />}
|
||||
{modalView === 'SIGNUP_VIEW' && <SignUpView />}
|
||||
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
|
||||
</Modal>
|
||||
<Featurebar
|
||||
title="This site uses cookies to improve your experience."
|
||||
description="By clicking, you agree to our Privacy Policy."
|
||||
@@ -70,6 +86,9 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{/* <Toast open={displayToast} onClose={closeModal}>
|
||||
{toastText}
|
||||
</Toast> */}
|
||||
</div>
|
||||
</CommerceProvider>
|
||||
)
|
||||
|
@@ -15,7 +15,7 @@ const Navbar: FC<Props> = ({ className }) => {
|
||||
<div className="flex justify-between align-center flex-row py-4 md:py-6 relative">
|
||||
<div className="flex flex-1 items-center">
|
||||
<Link href="/">
|
||||
<a className="cursor-pointer">
|
||||
<a className="cursor-pointer" aria-label="Logo">
|
||||
<Logo />
|
||||
</a>
|
||||
</Link>
|
||||
@@ -42,7 +42,7 @@ const Navbar: FC<Props> = ({ className }) => {
|
||||
</div>
|
||||
|
||||
<div className="flex pb-4 lg:px-6 lg:hidden">
|
||||
<Searchbar />
|
||||
<Searchbar id="mobileSearch" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@@ -3,10 +3,6 @@
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
@apply outline-none shadow-outline-gray;
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
@apply absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none;
|
||||
}
|
||||
|
@@ -5,9 +5,10 @@ import { useRouter } from 'next/router'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
const Searchbar: FC<Props> = ({ className }) => {
|
||||
const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -21,27 +22,30 @@ const Searchbar: FC<Props> = ({ className }) => {
|
||||
className
|
||||
)}
|
||||
>
|
||||
<input
|
||||
className={s.input}
|
||||
placeholder="Search for products..."
|
||||
defaultValue={router.query.q}
|
||||
onKeyUp={(e) => {
|
||||
e.preventDefault()
|
||||
<label htmlFor={id}>
|
||||
<input
|
||||
id={id}
|
||||
className={s.input}
|
||||
placeholder="Search for products..."
|
||||
defaultValue={router.query.q}
|
||||
onKeyUp={(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
const q = e.currentTarget.value
|
||||
if (e.key === 'Enter') {
|
||||
const q = e.currentTarget.value
|
||||
|
||||
router.push(
|
||||
{
|
||||
pathname: `/search`,
|
||||
query: q ? { q } : {},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
router.push(
|
||||
{
|
||||
pathname: `/search`,
|
||||
query: q ? { q } : {},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div className={s.iconContainer}>
|
||||
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import React, { FC } from 'react'
|
||||
import { Switch } from '@headlessui/react'
|
||||
import { HiSun, HiMoon } from 'react-icons/hi'
|
||||
import { Moon, Sun } from '@components/icons'
|
||||
interface Props {
|
||||
className?: string
|
||||
checked: boolean
|
||||
@@ -35,7 +35,7 @@ const Toggle: FC<Props> = ({ className, checked, onChange }) => {
|
||||
: 'opacity-100 ease-in duration-150'
|
||||
} absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`}
|
||||
>
|
||||
<HiSun className="h-3 w-3 text-gray-400" />
|
||||
<Sun className="h-3 w-3 text-accent-3" />
|
||||
</span>
|
||||
<span
|
||||
className={`${
|
||||
@@ -44,7 +44,7 @@ const Toggle: FC<Props> = ({ className, checked, onChange }) => {
|
||||
: 'opacity-0 ease-out duration-150'
|
||||
} opacity-0 ease-out duration-150 absolute inset-0 h-full w-full flex items-center justify-center transition-opacity`}
|
||||
>
|
||||
<HiMoon className="h-3 w-3 text-yellow-400" />
|
||||
<Moon className="h-3 w-3 text-yellow-400" />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
@@ -3,16 +3,31 @@ import Link from 'next/link'
|
||||
import { useTheme } from 'next-themes'
|
||||
import cn from 'classnames'
|
||||
import s from './DropdownMenu.module.css'
|
||||
import { Moon, Sun } from '@components/icon'
|
||||
import { Moon, Sun } from '@components/icons'
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
|
||||
import useLogout from '@lib/bigcommerce/use-logout'
|
||||
interface DropdownMenuProps {
|
||||
open: boolean
|
||||
}
|
||||
|
||||
const LINKS = [
|
||||
{
|
||||
name: 'My Orders',
|
||||
href: '/orders',
|
||||
},
|
||||
{
|
||||
name: 'My Profile',
|
||||
href: '/profile',
|
||||
},
|
||||
{
|
||||
name: 'Cart',
|
||||
href: '/cart',
|
||||
},
|
||||
]
|
||||
|
||||
const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
const logout = useLogout()
|
||||
return (
|
||||
<Transition
|
||||
show={open}
|
||||
@@ -24,39 +39,41 @@ const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className={s.dropdownMenu}>
|
||||
{LINKS.map(({ name, href }) => (
|
||||
<Menu.Item key={href}>
|
||||
{({ active }) => (
|
||||
<Link href={href}>
|
||||
<a className={cn(s.link, { [s.active]: active })}>{name}</a>
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
<Menu.Item>
|
||||
{({ active }) => <a className={s.link}>My Purchases</a>}
|
||||
<a
|
||||
className={cn(s.link, 'justify-between')}
|
||||
onClick={() =>
|
||||
theme === 'dark' ? setTheme('light') : setTheme('dark')
|
||||
}
|
||||
>
|
||||
<div>
|
||||
Theme: <strong>{theme}</strong>{' '}
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
{theme == 'dark' ? (
|
||||
<Moon width={20} height={20} />
|
||||
) : (
|
||||
<Sun width="20" height={20} />
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => <a className={s.link}>My Account</a>}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
className={cn(s.link, 'justify-between')}
|
||||
onClick={() =>
|
||||
theme === 'dark' ? setTheme('light') : setTheme('dark')
|
||||
}
|
||||
>
|
||||
<div>
|
||||
Theme: <strong>{theme}</strong>{' '}
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
{theme == 'dark' ? (
|
||||
<Moon width={20} height={20} />
|
||||
) : (
|
||||
<Sun width="20" height={20} />
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a className={cn(s.link, 'border-t border-accents-2 mt-4')}>
|
||||
Logout
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
className={cn(s.link, 'border-t border-accents-2 mt-4')}
|
||||
onClick={() => logout()}
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
|
@@ -2,9 +2,6 @@
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.mainContainer {
|
||||
}
|
||||
|
||||
.list {
|
||||
@apply flex flex-row items-center justify-items-end h-full;
|
||||
}
|
||||
@@ -25,3 +22,11 @@
|
||||
@apply outline-none;
|
||||
}
|
||||
}
|
||||
|
||||
.bagCount {
|
||||
@apply border border-accents-1 bg-secondary text-secondary h-4 w-4 absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs;
|
||||
}
|
||||
|
||||
.avatarButton {
|
||||
@apply inline-flex justify-center rounded-full outline-none focus:outline-none;
|
||||
}
|
||||
|
@@ -1,14 +1,15 @@
|
||||
import Link from 'next/link'
|
||||
import cn from 'classnames'
|
||||
import s from './UserNav.module.css'
|
||||
import { FC, useRef } from 'react'
|
||||
import { FC } from 'react'
|
||||
import { Heart, Bag } from '@components/icons'
|
||||
import { Avatar } from '@components/core'
|
||||
import { Heart, Bag } from '@components/icon'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { LoginView } from '@components/auth'
|
||||
import DropdownMenu from './DropdownMenu'
|
||||
import { Menu } from '@headlessui/react'
|
||||
import useCart from '@lib/bigcommerce/cart/use-cart'
|
||||
|
||||
import useCustomer from '@lib/bigcommerce/use-customer'
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
@@ -19,25 +20,20 @@ const countItems = (count: number, items: any[]) =>
|
||||
|
||||
const UserNav: FC<Props> = ({ className, children, ...props }) => {
|
||||
const { data } = useCart()
|
||||
const { openSidebar, closeSidebar, displaySidebar } = useUI()
|
||||
const { data: customer } = useCustomer()
|
||||
|
||||
const { openSidebar, closeSidebar, displaySidebar, openModal } = useUI()
|
||||
const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0)
|
||||
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
|
||||
|
||||
return (
|
||||
<nav className={cn(s.root, className)}>
|
||||
<div className={s.mainContainer}>
|
||||
<ul className={s.list}>
|
||||
<li
|
||||
className={s.item}
|
||||
onClick={() => (displaySidebar ? closeSidebar() : openSidebar())}
|
||||
onClick={(e) => (displaySidebar ? closeSidebar() : openSidebar())}
|
||||
>
|
||||
<Bag />
|
||||
{itemsCount > 0 && (
|
||||
<span className="border border-accent-1 bg-secondary text-secondary h-4 w-4 absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs">
|
||||
{itemsCount}
|
||||
</span>
|
||||
)}
|
||||
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
||||
</li>
|
||||
<Link href="/wishlist">
|
||||
<li className={s.item}>
|
||||
@@ -45,18 +41,26 @@ const UserNav: FC<Props> = ({ className, children, ...props }) => {
|
||||
</li>
|
||||
</Link>
|
||||
<li className={s.item}>
|
||||
<Menu>
|
||||
{({ open }) => {
|
||||
return (
|
||||
{customer ? (
|
||||
<Menu>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Menu.Button className="inline-flex justify-center rounded-full">
|
||||
<Menu.Button className={s.avatarButton} aria-label="Menu">
|
||||
<Avatar />
|
||||
</Menu.Button>
|
||||
<DropdownMenu open={open} />
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Menu>
|
||||
)}
|
||||
</Menu>
|
||||
) : (
|
||||
<button
|
||||
className={s.avatarButton}
|
||||
aria-label="Menu"
|
||||
onClick={() => openModal()}
|
||||
>
|
||||
<Avatar />
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@@ -9,3 +9,4 @@ export { default as Toggle } from './Toggle'
|
||||
export { default as Head } from './Head'
|
||||
export { default as HTMLContent } from './HTMLContent'
|
||||
export { default as I18nWidget } from './I18nWidget'
|
||||
export { default as EnhancedImage } from './EnhancedImage'
|
||||
|
@@ -1,14 +0,0 @@
|
||||
const Cross = ({ ...props }) => {
|
||||
return (
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" {...props}>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cross
|
21
components/icons/Cross.tsx
Normal file
21
components/icons/Cross.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
const Cross = ({ ...props }) => {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
shape-rendering="geometricPrecision"
|
||||
{...props}
|
||||
>
|
||||
<path d="M18 6L6 18" />
|
||||
<path d="M6 6l12 12" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cross
|
22
components/icons/Info.tsx
Normal file
22
components/icons/Info.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
const Info = ({ ...props }) => {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
shape-rendering="geometricPrecision"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" fill="transparent" />
|
||||
<path d="M12 8v4" stroke="currentColor" />
|
||||
<path d="M12 16h.01" stroke="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default Info
|
@@ -11,3 +11,4 @@ export { default as Moon } from './Moon'
|
||||
export { default as Github } from './Github'
|
||||
export { default as DoubleChevron } from './DoubleChevron'
|
||||
export { default as RightArrow } from './RightArrow'
|
||||
export { default as Info } from './Info'
|
@@ -1,9 +1,10 @@
|
||||
import { FC, ReactNode, Component } from 'react'
|
||||
import React, { FC, ReactNode, Component } from 'react'
|
||||
import cn from 'classnames'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-all-products'
|
||||
import { Heart } from '@components/icon'
|
||||
import usePrice from '@lib/bigcommerce/use-price'
|
||||
import { Heart } from '@components/icons'
|
||||
import { EnhancedImage } from '@components/core'
|
||||
import s from './ProductCard.module.css'
|
||||
|
||||
interface Props {
|
||||
@@ -25,6 +26,11 @@ const ProductCard: FC<Props> = ({
|
||||
priority,
|
||||
}) => {
|
||||
const src = p.images.edges?.[0]?.node.urlOriginal!
|
||||
const { price } = usePrice({
|
||||
amount: p.prices?.price?.value,
|
||||
baseAmount: p.prices?.retailPrice?.value,
|
||||
currencyCode: p.prices?.price?.currencyCode!,
|
||||
})
|
||||
|
||||
if (variant === 'slim') {
|
||||
return (
|
||||
@@ -34,8 +40,9 @@ const ProductCard: FC<Props> = ({
|
||||
{p.name}
|
||||
</span>
|
||||
</div>
|
||||
<Image
|
||||
src={src}
|
||||
<EnhancedImage
|
||||
src={p.images.edges?.[0]?.node.urlOriginal!}
|
||||
alt={p.name}
|
||||
width={imgWidth}
|
||||
height={imgHeight}
|
||||
priority={priority}
|
||||
@@ -56,14 +63,15 @@ const ProductCard: FC<Props> = ({
|
||||
<h3 className={s.productTitle}>
|
||||
<span>{p.name}</span>
|
||||
</h3>
|
||||
<span className={s.productPrice}>${p.prices?.price.value}</span>
|
||||
<span className={s.productPrice}>{price}</span>
|
||||
</div>
|
||||
<div className={s.wishlistButton}>
|
||||
<Heart />
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(s.imageContainer)}>
|
||||
<Image
|
||||
<EnhancedImage
|
||||
alt={p.name}
|
||||
className={cn('w-full object-cover', s['product-image'])}
|
||||
src={src}
|
||||
width={imgWidth}
|
||||
|
@@ -19,10 +19,15 @@ const ProductSlider: FC = ({ children }) => {
|
||||
|
||||
return (
|
||||
<div className={s.root}>
|
||||
<button className={cn(s.leftControl, s.control)} onClick={slider?.prev} />
|
||||
<button
|
||||
className={cn(s.leftControl, s.control)}
|
||||
onClick={slider?.prev}
|
||||
aria-label="Previous Product Image"
|
||||
/>
|
||||
<button
|
||||
className={cn(s.rightControl, s.control)}
|
||||
onClick={slider?.next}
|
||||
aria-label="Next Product Image"
|
||||
/>
|
||||
<div
|
||||
ref={ref}
|
||||
@@ -50,6 +55,7 @@ const ProductSlider: FC = ({ children }) => {
|
||||
{[...Array(slider.details().size).keys()].map((idx) => {
|
||||
return (
|
||||
<button
|
||||
aria-label="Position indicator"
|
||||
key={idx}
|
||||
className={cn(s.positionIndicator, {
|
||||
[s.positionIndicatorActive]: currentSlide === idx,
|
||||
|
@@ -53,10 +53,10 @@
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 w-full;
|
||||
@apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 w-full h-full;
|
||||
|
||||
@screen lg {
|
||||
@apply col-span-6 pt-20;
|
||||
@apply col-span-6 py-24 justify-between;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,10 +4,11 @@ import Image from 'next/image'
|
||||
import { NextSeo } from 'next-seo'
|
||||
|
||||
import s from './ProductView.module.css'
|
||||
import { Heart } from '@components/icon'
|
||||
import { Heart } from '@components/icons'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { Button, Container } from '@components/ui'
|
||||
import { Swatch, ProductSlider } from '@components/product'
|
||||
import { Button, Container } from '@components/ui'
|
||||
import { HTMLContent } from '@components/core'
|
||||
|
||||
import useAddItem from '@lib/bigcommerce/cart/use-add-item'
|
||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
||||
@@ -77,10 +78,11 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
||||
<div className={s.sliderContainer}>
|
||||
<ProductSlider>
|
||||
{product.images.edges?.map((image, i) => (
|
||||
<div key={image?.node.urlXL} className={s.imageContainer}>
|
||||
<div key={image?.node.urlOriginal} className={s.imageContainer}>
|
||||
<Image
|
||||
alt={product.name}
|
||||
className={s.img}
|
||||
src={image?.node.urlXL!}
|
||||
src={image?.node.urlOriginal!}
|
||||
width={1050}
|
||||
height={1050}
|
||||
priority={i === 0}
|
||||
@@ -110,7 +112,6 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
||||
label={v.label}
|
||||
onClick={() => {
|
||||
setChoices((choices) => {
|
||||
console.log(choices)
|
||||
return {
|
||||
...choices,
|
||||
[opt.displayName]: v.label,
|
||||
@@ -123,21 +124,22 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="pb-12">
|
||||
<div
|
||||
className="pb-14 break-words w-full"
|
||||
dangerouslySetInnerHTML={{ __html: product.description }}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
className={s.button}
|
||||
onClick={addToCart}
|
||||
loading={loading}
|
||||
>
|
||||
Add to Cart
|
||||
</Button>
|
||||
|
||||
<div className="pb-14 break-words w-full max-w-xl">
|
||||
<HTMLContent html={product.description} />
|
||||
</div>
|
||||
</section>
|
||||
<div>
|
||||
<Button
|
||||
aria-label="Add to Cart"
|
||||
type="button"
|
||||
className={s.button}
|
||||
onClick={addToCart}
|
||||
loading={loading}
|
||||
>
|
||||
Add to Cart
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TODO make it work */}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import cn from 'classnames'
|
||||
import { FC } from 'react'
|
||||
import s from './Swatch.module.css'
|
||||
import { Check } from '@components/icon'
|
||||
import { Check } from '@components/icons'
|
||||
import Button, { ButtonProps } from '@components/ui/Button'
|
||||
import { isDark } from '@lib/colors'
|
||||
interface Props {
|
||||
@@ -39,6 +39,7 @@ const Swatch: FC<Props & ButtonProps> = ({
|
||||
<Button
|
||||
className={rootClassName}
|
||||
style={color ? { backgroundColor: color } : {}}
|
||||
aria-label="Variant Swatch"
|
||||
{...props}
|
||||
>
|
||||
{variant === 'color' && active && (
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
||||
|
||||
export function getProductOptions(product: ProductNode) {
|
||||
// console.log(product)
|
||||
const options = product.productOptions.edges?.map(({ node }: any) => ({
|
||||
displayName: node.displayName.toLowerCase(),
|
||||
values: node.values.edges?.map(({ node }: any) => node),
|
||||
|
@@ -1 +0,0 @@
|
||||
import { Colors } from '@components/ui/types'
|
@@ -24,3 +24,12 @@
|
||||
.slim {
|
||||
@apply py-2 transform-none normal-case;
|
||||
}
|
||||
|
||||
.disabled,
|
||||
.disabled:hover {
|
||||
@apply text-accents-4 border-accents-2 bg-accents-1 cursor-not-allowed;
|
||||
filter: grayscale(1);
|
||||
-webkit-transform: translateZ(0);
|
||||
-webkit-perspective: 1000;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
Component?: string | JSXElementConstructor<any>
|
||||
width?: string | number
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
||||
@@ -28,10 +29,10 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
||||
children,
|
||||
active,
|
||||
onClick,
|
||||
disabled,
|
||||
width,
|
||||
Component = 'button',
|
||||
loading = false,
|
||||
disabled = false,
|
||||
style = {},
|
||||
...rest
|
||||
} = props
|
||||
@@ -52,6 +53,7 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
||||
{
|
||||
[s.slim]: variant === 'slim',
|
||||
[s.loading]: loading,
|
||||
[s.disabled]: disabled,
|
||||
},
|
||||
className
|
||||
)
|
||||
@@ -64,6 +66,7 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
||||
{...buttonProps}
|
||||
data-active={isPressed ? '' : undefined}
|
||||
className={rootClassName}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
width,
|
||||
...style,
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import React, { FC } from 'react'
|
||||
import { Container } from '@components/ui'
|
||||
import { RightArrow } from '@components/icon'
|
||||
import { RightArrow } from '@components/icons'
|
||||
import s from './Hero.module.css'
|
||||
|
||||
import Link from 'next/link'
|
||||
interface Props {
|
||||
className?: string
|
||||
headline: string
|
||||
@@ -21,10 +21,12 @@ const Hero: FC<Props> = ({ headline, description }) => {
|
||||
<p className="mt-5 text-xl leading-7 text-accent-2 text-white">
|
||||
{description}
|
||||
</p>
|
||||
<a className="text-white pt-3 font-bold hover:underline flex flex-row cursor-pointer w-max-content">
|
||||
<span>Read it here</span>
|
||||
<RightArrow width="20" heigh="20" className="ml-1" />
|
||||
</a>
|
||||
<Link href="/blog">
|
||||
<a className="text-white pt-3 font-bold hover:underline flex flex-row cursor-pointer w-max-content">
|
||||
Read it here
|
||||
<RightArrow width="20" heigh="20" className="ml-1" />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
5
components/ui/Input/Input.module.css
Normal file
5
components/ui/Input/Input.module.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.root {
|
||||
@apply focus:outline-none bg-primary focus:shadow-outline-gray py-2
|
||||
px-6 w-full appearance-none transition duration-150 ease-in-out
|
||||
placeholder-accents-5 pr-10 border border-accents-3 text-accents-6;
|
||||
}
|
35
components/ui/Input/Input.tsx
Normal file
35
components/ui/Input/Input.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import cn from 'classnames'
|
||||
import s from './Input.module.css'
|
||||
import React, { InputHTMLAttributes } from 'react'
|
||||
|
||||
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
||||
className?: string
|
||||
onChange?: (...args: any[]) => any
|
||||
}
|
||||
|
||||
const Input: React.FC<Props> = (props) => {
|
||||
const { className, children, onChange, ...rest } = props
|
||||
|
||||
const rootClassName = cn(s.root, {}, className)
|
||||
|
||||
const handleOnChange = (e: any) => {
|
||||
if (onChange) {
|
||||
onChange(e.target.value)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
className={rootClassName}
|
||||
onChange={handleOnChange}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input
|
1
components/ui/Input/index.ts
Normal file
1
components/ui/Input/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Input'
|
@@ -19,16 +19,6 @@ const M: FC<Props> = ({ className = '', children, variant = 'primary' }) => {
|
||||
className
|
||||
)
|
||||
|
||||
// return (
|
||||
// <div className={rootClassName}>
|
||||
// <div className={s.container}>
|
||||
// {items.map((p: any) => (
|
||||
// <Component {...p} />
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
// )
|
||||
|
||||
return (
|
||||
<div className={rootClassName}>
|
||||
<Ticker offset={80}>
|
||||
|
@@ -6,3 +6,7 @@
|
||||
.modal {
|
||||
@apply bg-primary p-12 border border-accents-2;
|
||||
}
|
||||
|
||||
.modal:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
@@ -2,43 +2,75 @@ import cn from 'classnames'
|
||||
import { FC, useRef } from 'react'
|
||||
import s from './Modal.module.css'
|
||||
import { useDialog } from '@react-aria/dialog'
|
||||
import { useOverlay, useModal } from '@react-aria/overlays'
|
||||
import { FocusScope } from '@react-aria/focus'
|
||||
|
||||
import { Transition } from '@headlessui/react'
|
||||
import { useOverlay, useModal, OverlayContainer } from '@react-aria/overlays'
|
||||
import { Cross } from '@components/icons'
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any
|
||||
show?: boolean
|
||||
close: () => void
|
||||
open?: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const Modal: FC<Props> = ({
|
||||
className,
|
||||
children,
|
||||
show = true,
|
||||
close,
|
||||
open = false,
|
||||
onClose,
|
||||
...props
|
||||
}) => {
|
||||
const rootClassName = cn(s.root, className)
|
||||
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
|
||||
let { modalProps } = useModal()
|
||||
let { overlayProps } = useOverlay(props, ref)
|
||||
let { dialogProps } = useDialog(props, ref)
|
||||
let { dialogProps } = useDialog({}, ref)
|
||||
let { overlayProps } = useOverlay(
|
||||
{
|
||||
isOpen: open,
|
||||
isDismissable: false,
|
||||
onClose: onClose,
|
||||
...props,
|
||||
},
|
||||
ref
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={rootClassName}>
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<div
|
||||
{...overlayProps}
|
||||
{...dialogProps}
|
||||
{...modalProps}
|
||||
ref={ref}
|
||||
className={s.modal}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</FocusScope>
|
||||
</div>
|
||||
<Transition show={open}>
|
||||
<OverlayContainer>
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<div className={rootClassName}>
|
||||
<Transition.Child
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-linear duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
className={s.modal}
|
||||
{...overlayProps}
|
||||
{...dialogProps}
|
||||
{...modalProps}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="h-7 flex items-center justify-end w-full">
|
||||
<button
|
||||
onClick={() => onClose()}
|
||||
aria-label="Close panel"
|
||||
className="hover:text-gray-500 transition ease-in-out duration-150 focus:outline-none"
|
||||
>
|
||||
<Cross className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</FocusScope>
|
||||
</OverlayContainer>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
|
15
components/ui/Text/Text.module.css
Normal file
15
components/ui/Text/Text.module.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.body {
|
||||
@apply text-lg leading-7 font-medium max-w-6xl mx-auto;
|
||||
}
|
||||
|
||||
.heading {
|
||||
@apply text-5xl mb-12;
|
||||
}
|
||||
|
||||
.pageHeading {
|
||||
@apply pt-1 pb-4 text-2xl leading-7 font-bold text-base tracking-wide;
|
||||
}
|
||||
|
||||
.sectionHeading {
|
||||
@apply pt-1 pb-2 text-base font-semibold leading-7 text-base tracking-wider uppercase border-b border-accents-2 mb-3;
|
||||
}
|
58
components/ui/Text/Text.tsx
Normal file
58
components/ui/Text/Text.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
JSXElementConstructor,
|
||||
CSSProperties,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import s from './Text.module.css'
|
||||
|
||||
interface Props {
|
||||
variant?: Variant
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
children: React.ReactNode | any
|
||||
}
|
||||
|
||||
type Variant = 'heading' | 'body' | 'pageHeading' | 'sectionHeading'
|
||||
|
||||
const Text: FunctionComponent<Props> = ({
|
||||
style,
|
||||
className = '',
|
||||
variant = 'body',
|
||||
children,
|
||||
}) => {
|
||||
const componentsMap: {
|
||||
[P in Variant]: React.ComponentType<any> | string
|
||||
} = {
|
||||
body: 'p',
|
||||
heading: 'h1',
|
||||
pageHeading: 'h1',
|
||||
sectionHeading: 'h2',
|
||||
}
|
||||
|
||||
const Component:
|
||||
| JSXElementConstructor<any>
|
||||
| React.ReactElement<any>
|
||||
| React.ComponentType<any>
|
||||
| string = componentsMap![variant!]
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={cn(
|
||||
s.root,
|
||||
{
|
||||
[s.body]: variant === 'body',
|
||||
[s.heading]: variant === 'heading',
|
||||
[s.pageHeading]: variant === 'pageHeading',
|
||||
[s.sectionHeading]: variant === 'sectionHeading',
|
||||
},
|
||||
className
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
|
||||
export default Text
|
1
components/ui/Text/index.ts
Normal file
1
components/ui/Text/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Text'
|
9
components/ui/Toast/Toast.module.css
Normal file
9
components/ui/Toast/Toast.module.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.root {
|
||||
}
|
||||
|
||||
.toast {
|
||||
@apply absolute bg-primary text-primary flex items-center border border-accents-1
|
||||
rounded-md z-50 shadow-2xl top-0 right-0 p-6 my-6 mx-3;
|
||||
width: 420px;
|
||||
z-index: 20000;
|
||||
}
|
73
components/ui/Toast/Toast.tsx
Normal file
73
components/ui/Toast/Toast.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import cn from 'classnames'
|
||||
import { FC, useRef, useEffect, useCallback } from 'react'
|
||||
import s from './Toast.module.css'
|
||||
import { useDialog } from '@react-aria/dialog'
|
||||
import { FocusScope } from '@react-aria/focus'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import { useOverlay, useModal, OverlayContainer } from '@react-aria/overlays'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any
|
||||
open?: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const Toast: FC<Props> = ({
|
||||
className,
|
||||
children,
|
||||
open = false,
|
||||
onClose,
|
||||
...props
|
||||
}) => {
|
||||
const rootClassName = cn(s.root, className)
|
||||
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
|
||||
let { modalProps } = useModal()
|
||||
let { dialogProps } = useDialog({}, ref)
|
||||
let { overlayProps } = useOverlay(
|
||||
{
|
||||
isOpen: open,
|
||||
isDismissable: true,
|
||||
onClose: onClose,
|
||||
...props,
|
||||
},
|
||||
ref
|
||||
)
|
||||
|
||||
// useEffect(() => {
|
||||
// setTimeout(() => {
|
||||
// useCallback(onClose, [])
|
||||
// }, 400)
|
||||
// })
|
||||
|
||||
return (
|
||||
<Transition show={open}>
|
||||
<OverlayContainer>
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<div className={rootClassName}>
|
||||
<Transition.Child
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-linear duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
className={s.toast}
|
||||
{...overlayProps}
|
||||
{...dialogProps}
|
||||
{...modalProps}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</FocusScope>
|
||||
</OverlayContainer>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
export default Toast
|
1
components/ui/Toast/index.ts
Normal file
1
components/ui/Toast/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Toast'
|
@@ -5,11 +5,19 @@ import { SSRProvider, OverlayProvider } from 'react-aria'
|
||||
export interface State {
|
||||
displaySidebar: boolean
|
||||
displayDropdown: boolean
|
||||
displayModal: boolean
|
||||
displayToast: boolean
|
||||
modalView: string
|
||||
toastText: string
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
displaySidebar: false,
|
||||
displayDropdown: false,
|
||||
displayModal: false,
|
||||
modalView: 'LOGIN_VIEW',
|
||||
displayToast: false,
|
||||
toastText: '',
|
||||
}
|
||||
|
||||
type Action =
|
||||
@@ -19,12 +27,39 @@ type Action =
|
||||
| {
|
||||
type: 'CLOSE_SIDEBAR'
|
||||
}
|
||||
| {
|
||||
type: 'OPEN_TOAST'
|
||||
}
|
||||
| {
|
||||
type: 'CLOSE_TOAST'
|
||||
}
|
||||
| {
|
||||
type: 'SET_TOAST_TEXT'
|
||||
text: ToastText
|
||||
}
|
||||
| {
|
||||
type: 'OPEN_DROPDOWN'
|
||||
}
|
||||
| {
|
||||
type: 'CLOSE_DROPDOWN'
|
||||
}
|
||||
| {
|
||||
type: 'OPEN_MODAL'
|
||||
}
|
||||
| {
|
||||
type: 'CLOSE_MODAL'
|
||||
}
|
||||
| {
|
||||
type: 'SET_MODAL_VIEW'
|
||||
view: 'LOGIN_VIEW'
|
||||
}
|
||||
| {
|
||||
type: 'SET_MODAL_VIEW'
|
||||
view: 'SIGNUP_VIEW'
|
||||
}
|
||||
|
||||
type MODAL_VIEWS = 'SIGNUP_VIEW' | 'LOGIN_VIEW' | 'FORGOT_VIEW'
|
||||
type ToastText = string
|
||||
|
||||
export const UIContext = React.createContext<State | any>(initialState)
|
||||
|
||||
@@ -56,6 +91,42 @@ function uiReducer(state: State, action: Action) {
|
||||
displayDropdown: false,
|
||||
}
|
||||
}
|
||||
case 'OPEN_MODAL': {
|
||||
return {
|
||||
...state,
|
||||
displayModal: true,
|
||||
}
|
||||
}
|
||||
case 'CLOSE_MODAL': {
|
||||
return {
|
||||
...state,
|
||||
displayModal: false,
|
||||
}
|
||||
}
|
||||
case 'OPEN_TOAST': {
|
||||
return {
|
||||
...state,
|
||||
displayToast: true,
|
||||
}
|
||||
}
|
||||
case 'CLOSE_TOAST': {
|
||||
return {
|
||||
...state,
|
||||
displayToast: false,
|
||||
}
|
||||
}
|
||||
case 'SET_MODAL_VIEW': {
|
||||
return {
|
||||
...state,
|
||||
modalView: action.view,
|
||||
}
|
||||
}
|
||||
case 'SET_TOAST_TEXT': {
|
||||
return {
|
||||
...state,
|
||||
toastText: action.text,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,14 +139,32 @@ export const UIProvider: FC = (props) => {
|
||||
const openDropdown = () => dispatch({ type: 'OPEN_DROPDOWN' })
|
||||
const closeDropdown = () => dispatch({ type: 'CLOSE_DROPDOWN' })
|
||||
|
||||
const openModal = () => dispatch({ type: 'OPEN_MODAL' })
|
||||
const closeModal = () => dispatch({ type: 'CLOSE_MODAL' })
|
||||
|
||||
const openToast = () => dispatch({ type: 'OPEN_TOAST' })
|
||||
const closeToast = () => dispatch({ type: 'CLOSE_TOAST' })
|
||||
|
||||
const setModalView = (view: MODAL_VIEWS) =>
|
||||
dispatch({ type: 'SET_MODAL_VIEW', view })
|
||||
|
||||
const value = {
|
||||
...state,
|
||||
openSidebar,
|
||||
closeSidebar,
|
||||
openDropdown,
|
||||
closeDropdown,
|
||||
openModal,
|
||||
closeModal,
|
||||
setModalView,
|
||||
openToast,
|
||||
closeToast,
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
openToast()
|
||||
}, 200)
|
||||
|
||||
return <UIContext.Provider value={value} {...props} />
|
||||
}
|
||||
|
||||
|
@@ -8,3 +8,6 @@ export { default as Container } from './Container'
|
||||
export { default as LoadingDots } from './LoadingDots'
|
||||
export { default as Skeleton } from './Skeleton'
|
||||
export { default as Modal } from './Modal'
|
||||
export { default as Text } from './Text'
|
||||
export { default as Input } from './Input'
|
||||
export { default as Toast } from './Toast'
|
||||
|
@@ -1 +0,0 @@
|
||||
export type Colors = 'violet' | 'black' | 'pink' | 'white'
|
@@ -1,5 +1,5 @@
|
||||
import { FC } from 'react'
|
||||
import { Trash } from '@components/icon'
|
||||
import { Trash } from '@components/icons'
|
||||
import s from './WishlistCard.module.css'
|
||||
|
||||
interface Props {
|
||||
|
Reference in New Issue
Block a user