forked from crowetic/commerce
New Release (#371)
* Custom Checkout Progress * Updates to Checkout * Custom Checkout Progress * Adding tabs * Adding Collapse * Adding Collapse * Improving Sidebar Scroll * Modif footer * Changes * More design updates * sidebar cart * More design updates * More design updates * More design updates * More design updates * Types * Types * Design Updates * More changes * More changes * More changes * Changes * Changes * Changes * New tailwind required changes * Sidebar Styling issues with Mobile * Latest changes - Normalizing cart * Styling Fixes * New changes * Changes * latest * Refactor and Renaming some UI Props * Adding Quantity Component * Adding Rating Component * Rating Component * More updates * User Select disabled, plus hidding horizontal scroll bars * Changes * Adding ProductOptions Component and more helpers * Styling updates * Styling updates * Fix for slim tags * Missmatch with RightArrow * Footer updates and some styles * Latest Updates * Latest Updates * Latest Updates * Removing Portal, since it's not needed. We might add it later I'd rather not to. * Removing Portal, since it's not needed. We might add it later I'd rather not to. * Sam backdrop filter * General UI Improvements * General UI Improvements * Search now with Geist Colors * Now with Geist Colors * Changes * Scroll for Mobile on IOs devises * LoadingDots Working (: * Changes * More Changes * Perf changes * More perf changes * Fade to the Nametags in the ProductCard * changes * Search issue ui * Search issue ui * Make sure to only refresh navbar and modals when required * Index revalidate * Fixed image issue * hide album scroll on windows * Fix scrollbar * Changing * Adding 404 with Layout * Removing Toast * Adding Assets * Adding Assets * Progress with LocalProvider * New productTag * Only images for the drop * changes * Empty SWRhooks * Adding Local Provider * Working local * Working view of a LocalProvider * More updates * Changes * Removed react-ticker * default to local if no env available * default to local if no env available * add missing `@` to css import * rewrite search rewrites to multiple pages * allow requests in getStaticProps to execute in parallel * make type import explicit * add a tsconfig.js file * use local provider in tsconfig.js * avoid a circular dependency * Saleor was not in the providers list * avoid circular dependency in bigcommerce * Adding more to the Local Provider (#366) * Adding more data * Adding more data * optimize assets (#370) * Optimize assets (#372) * optimize assets * remove assets * remove assets * cart enabled * Adding saleor * Changes with Webpack * Changes Co-authored-by: Luis Alvarez <luis@vercel.com> Co-authored-by: Tobias Koppers <tobias.koppers@googlemail.com> Co-authored-by: Shu Ding <g@shud.in>
This commit is contained in:
parent
3c9b90f453
commit
78cc378a72
README.mdsearch.tsx
assets
components
auth
cart
checkout
CheckoutSidebarView
PaymentMethodView
PaymentWidget
ShippingView
ShippingWidget
common
Avatar
FeatureBar
Footer
HomeAllProductsGrid
I18nWidget
Layout
Navbar
Searchbar
SidebarLayout
UserNav
icons
product
ProductCard
ProductOptions
ProductSidebar
ProductSlider
ProductSliderControl
ProductTag
ProductView
Swatch
helpers.tsindex.tsui
Button
Collapse
Container
Grid
Hero
Input
LoadingDots
Marquee
Modal
Quantity
Rating
Sidebar
Skeleton
@ -64,6 +64,11 @@ That's it!
|
|||||||
|
|
||||||
Every provider defines the features that it supports under `framework/{provider}/commerce.config.json`
|
Every provider defines the features that it supports under `framework/{provider}/commerce.config.json`
|
||||||
|
|
||||||
|
#### Features Available
|
||||||
|
|
||||||
|
- wishlist
|
||||||
|
- customCheckout
|
||||||
|
|
||||||
#### How to turn Features on and off
|
#### How to turn Features on and off
|
||||||
|
|
||||||
> NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out the box)
|
> NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out the box)
|
||||||
@ -73,7 +78,8 @@ Every provider defines the features that it supports under `framework/{provider}
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"features": {
|
"features": {
|
||||||
"wishlist": false
|
"wishlist": false,
|
||||||
|
"customCheckout": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -15,21 +15,28 @@
|
|||||||
--cyan: #22b8cf;
|
--cyan: #22b8cf;
|
||||||
--green: #37b679;
|
--green: #37b679;
|
||||||
--red: #da3c3c;
|
--red: #da3c3c;
|
||||||
--pink: #e64980;
|
|
||||||
--purple: #f81ce5;
|
--purple: #f81ce5;
|
||||||
--blue: #0070f3;
|
--blue: #0070f3;
|
||||||
--violet: #5f3dc4;
|
|
||||||
--violet-light: #7048e8;
|
--pink: #ff0080;
|
||||||
--accents-0: #f8f9fa;
|
--pink-light: #ff379c;
|
||||||
--accents-1: #f1f3f5;
|
|
||||||
--accents-2: #e9ecef;
|
--magenta: #eb367f;
|
||||||
--accents-3: #dee2e6;
|
|
||||||
--accents-4: #ced4da;
|
--violet: #7928ca;
|
||||||
--accents-5: #adb5bd;
|
--violet-dark: #4c2889;
|
||||||
--accents-6: #868e96;
|
|
||||||
--accents-7: #495057;
|
--accent-0: #fff;
|
||||||
--accents-8: #343a40;
|
--accent-1: #fafafa;
|
||||||
--accents-9: #212529;
|
--accent-2: #eaeaea;
|
||||||
|
--accent-3: #999999;
|
||||||
|
--accent-4: #888888;
|
||||||
|
--accent-5: #666666;
|
||||||
|
--accent-6: #444444;
|
||||||
|
--accent-7: #333333;
|
||||||
|
--accent-8: #111111;
|
||||||
|
--accent-9: #000;
|
||||||
|
|
||||||
--font-sans: -apple-system, system-ui, BlinkMacSystemFont, 'Helvetica Neue',
|
--font-sans: -apple-system, system-ui, BlinkMacSystemFont, 'Helvetica Neue',
|
||||||
'Helvetica', sans-serif;
|
'Helvetica', sans-serif;
|
||||||
}
|
}
|
||||||
@ -48,16 +55,16 @@
|
|||||||
--text-primary: white;
|
--text-primary: white;
|
||||||
--text-secondary: black;
|
--text-secondary: black;
|
||||||
|
|
||||||
--accents-0: #212529;
|
--accent-9: #fff;
|
||||||
--accents-1: #343a40;
|
--accent-8: #fafafa;
|
||||||
--accents-2: #495057;
|
--accent-7: #eaeaea;
|
||||||
--accents-3: #868e96;
|
--accent-6: #999999;
|
||||||
--accents-4: #adb5bd;
|
--accent-5: #888888;
|
||||||
--accents-5: #ced4da;
|
--accent-4: #666666;
|
||||||
--accents-6: #dee2e6;
|
--accent-3: #444444;
|
||||||
--accents-7: #e9ecef;
|
--accent-2: #333333;
|
||||||
--accents-8: #f1f3f5;
|
--accent-1: #111111;
|
||||||
--accents-9: #f8f9fa;
|
--accent-0: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
@ -84,6 +91,7 @@ body {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
background-color: var(--primary);
|
background-color: var(--primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
overscroll-behavior-x: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -61,7 +61,7 @@ const ForgotPassword: FC<Props> = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="pt-3 text-center text-sm">
|
<span className="pt-3 text-center text-sm">
|
||||||
<span className="text-accents-7">Do you have an account?</span>
|
<span className="text-accent-7">Do you have an account?</span>
|
||||||
{` `}
|
{` `}
|
||||||
<a
|
<a
|
||||||
className="text-accent-9 font-bold hover:underline cursor-pointer"
|
className="text-accent-9 font-bold hover:underline cursor-pointer"
|
||||||
|
@ -87,7 +87,7 @@ const LoginView: FC<Props> = () => {
|
|||||||
Log In
|
Log In
|
||||||
</Button>
|
</Button>
|
||||||
<div className="pt-1 text-center text-sm">
|
<div className="pt-1 text-center text-sm">
|
||||||
<span className="text-accents-7">Don't have an account?</span>
|
<span className="text-accent-7">Don't have an account?</span>
|
||||||
{` `}
|
{` `}
|
||||||
<a
|
<a
|
||||||
className="text-accent-9 font-bold hover:underline cursor-pointer"
|
className="text-accent-9 font-bold hover:underline cursor-pointer"
|
||||||
|
@ -76,7 +76,7 @@ const SignUpView: FC<Props> = () => {
|
|||||||
<Input placeholder="Last Name" onChange={setLastName} />
|
<Input placeholder="Last Name" onChange={setLastName} />
|
||||||
<Input type="email" placeholder="Email" onChange={setEmail} />
|
<Input type="email" placeholder="Email" onChange={setEmail} />
|
||||||
<Input type="password" placeholder="Password" onChange={setPassword} />
|
<Input type="password" placeholder="Password" onChange={setPassword} />
|
||||||
<span className="text-accents-8">
|
<span className="text-accent-8">
|
||||||
<span className="inline-block align-middle ">
|
<span className="inline-block align-middle ">
|
||||||
<Info width="15" height="15" />
|
<Info width="15" height="15" />
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
@ -97,7 +97,7 @@ const SignUpView: FC<Props> = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="pt-1 text-center text-sm">
|
<span className="pt-1 text-center text-sm">
|
||||||
<span className="text-accents-7">Do you have an account?</span>
|
<span className="text-accent-7">Do you have an account?</span>
|
||||||
{` `}
|
{` `}
|
||||||
<a
|
<a
|
||||||
className="text-accent-9 font-bold hover:underline cursor-pointer"
|
className="text-accent-9 font-bold hover:underline cursor-pointer"
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
|
.root {
|
||||||
|
@apply flex flex-col py-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.quantity {
|
.quantity {
|
||||||
appearance: textfield;
|
appearance: textfield;
|
||||||
@apply w-8 border-accents-2 border mx-3 rounded text-center text-sm text-black;
|
@apply w-8 border-accent-2 border mx-3 rounded text-center text-sm text-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quantity::-webkit-outer-spin-button,
|
.quantity::-webkit-outer-spin-button,
|
||||||
@ -15,4 +23,10 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
left: 30% !important;
|
left: 30% !important;
|
||||||
top: 30% !important;
|
top: 30% !important;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.productName {
|
||||||
|
@apply font-medium cursor-pointer pb-1;
|
||||||
|
margin-top: -4px;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { ChangeEvent, useEffect, useState } from 'react'
|
import { ChangeEvent, FocusEventHandler, useEffect, useState } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import s from './CartItem.module.css'
|
import s from './CartItem.module.css'
|
||||||
import { Trash, Plus, Minus } from '@components/icons'
|
import { Trash, Plus, Minus, Cross } from '@components/icons'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import type { LineItem } from '@commerce/types/cart'
|
import type { LineItem } from '@commerce/types/cart'
|
||||||
import usePrice from '@framework/product/use-price'
|
import usePrice from '@framework/product/use-price'
|
||||||
import useUpdateItem from '@framework/cart/use-update-item'
|
import useUpdateItem from '@framework/cart/use-update-item'
|
||||||
import useRemoveItem from '@framework/cart/use-remove-item'
|
import useRemoveItem from '@framework/cart/use-remove-item'
|
||||||
|
import Quantity from '@components/ui/Quantity'
|
||||||
|
|
||||||
type ItemOption = {
|
type ItemOption = {
|
||||||
name: string
|
name: string
|
||||||
@ -19,13 +20,19 @@ type ItemOption = {
|
|||||||
|
|
||||||
const CartItem = ({
|
const CartItem = ({
|
||||||
item,
|
item,
|
||||||
|
variant = 'default',
|
||||||
currencyCode,
|
currencyCode,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
|
variant?: 'default' | 'display'
|
||||||
item: LineItem
|
item: LineItem
|
||||||
currencyCode: string
|
currencyCode: string
|
||||||
}) => {
|
}) => {
|
||||||
const { closeSidebarIfPresent } = useUI()
|
const { closeSidebarIfPresent } = useUI()
|
||||||
|
const [removing, setRemoving] = useState(false)
|
||||||
|
const [quantity, setQuantity] = useState<number>(item.quantity)
|
||||||
|
const removeItem = useRemoveItem()
|
||||||
|
const updateItem = useUpdateItem({ item })
|
||||||
|
|
||||||
const { price } = usePrice({
|
const { price } = usePrice({
|
||||||
amount: item.variant.price * item.quantity,
|
amount: item.variant.price * item.quantity,
|
||||||
@ -33,48 +40,28 @@ const CartItem = ({
|
|||||||
currencyCode,
|
currencyCode,
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateItem = useUpdateItem({ item })
|
const handleChange = async ({
|
||||||
const removeItem = useRemoveItem()
|
target: { value },
|
||||||
const [quantity, setQuantity] = useState<number | ''>(item.quantity)
|
}: ChangeEvent<HTMLInputElement>) => {
|
||||||
const [removing, setRemoving] = useState(false)
|
setQuantity(Number(value))
|
||||||
|
await updateItem({ quantity: Number(value) })
|
||||||
|
}
|
||||||
|
|
||||||
const updateQuantity = async (val: number) => {
|
const increaseQuantity = async (n = 1) => {
|
||||||
|
const val = Number(quantity) + n
|
||||||
|
setQuantity(val)
|
||||||
await updateItem({ quantity: val })
|
await updateItem({ quantity: val })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleQuantity = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const val = !e.target.value ? '' : Number(e.target.value)
|
|
||||||
|
|
||||||
if (!val || (Number.isInteger(val) && val >= 0)) {
|
|
||||||
setQuantity(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const handleBlur = () => {
|
|
||||||
const val = Number(quantity)
|
|
||||||
|
|
||||||
if (val !== item.quantity) {
|
|
||||||
updateQuantity(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const increaseQuantity = (n = 1) => {
|
|
||||||
const val = Number(quantity) + n
|
|
||||||
|
|
||||||
if (Number.isInteger(val) && val >= 0) {
|
|
||||||
setQuantity(val)
|
|
||||||
updateQuantity(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const handleRemove = async () => {
|
const handleRemove = async () => {
|
||||||
setRemoving(true)
|
setRemoving(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// If this action succeeds then there's no need to do `setRemoving(true)`
|
|
||||||
// because the component will be removed from the view
|
|
||||||
await removeItem(item)
|
await removeItem(item)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setRemoving(false)
|
setRemoving(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add a type for this
|
// TODO: Add a type for this
|
||||||
const options = (item as any).options
|
const options = (item as any).options
|
||||||
|
|
||||||
@ -87,79 +74,76 @@ const CartItem = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={cn('flex flex-row space-x-8 py-8', {
|
className={cn(s.root, {
|
||||||
'opacity-75 pointer-events-none': removing,
|
'opacity-50 pointer-events-none': removing,
|
||||||
})}
|
})}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer">
|
<div className="flex flex-row space-x-4 py-4">
|
||||||
<Link href={`/product/${item.path}`}>
|
<div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer z-0">
|
||||||
<Image
|
<Link href={`/product/${item.path}`}>
|
||||||
onClick={() => closeSidebarIfPresent()}
|
<Image
|
||||||
className={s.productImage}
|
onClick={() => closeSidebarIfPresent()}
|
||||||
width={150}
|
className={s.productImage}
|
||||||
height={150}
|
width={150}
|
||||||
src={item.variant.image!.url}
|
height={150}
|
||||||
alt={item.variant.image!.altText}
|
src={item.variant.image!.url}
|
||||||
unoptimized
|
alt={item.variant.image!.altText}
|
||||||
/>
|
unoptimized
|
||||||
</Link>
|
/>
|
||||||
</div>
|
</Link>
|
||||||
<div className="flex-1 flex flex-col text-base">
|
</div>
|
||||||
<Link href={`/product/${item.path}`}>
|
<div className="flex-1 flex flex-col text-base">
|
||||||
<span
|
<Link href={`/product/${item.path}`}>
|
||||||
onClick={() => closeSidebarIfPresent()}
|
<span
|
||||||
>
|
className={s.productName}
|
||||||
<div
|
onClick={() => closeSidebarIfPresent()}
|
||||||
className="font-bold text-lg cursor-pointer leading-6"
|
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
{options && options.length > 0 && (
|
||||||
|
<div className="flex items-center pb-1">
|
||||||
|
{options.map((option: ItemOption, i: number) => (
|
||||||
|
<div
|
||||||
|
key={`${item.id}-${option.name}`}
|
||||||
|
className="text-sm font-semibold text-accent-7 inline-flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
{option.name === 'Color' ? (
|
||||||
|
<span
|
||||||
|
className="mx-2 rounded-full bg-transparent border w-5 h-5 p-1 text-accent-9 inline-flex items-center justify-center overflow-hidden"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${option.value}`,
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
) : (
|
||||||
|
<span className="mx-2 rounded-full bg-transparent border h-5 p-1 text-accent-9 inline-flex items-center justify-center overflow-hidden">
|
||||||
|
{option.value}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{i === options.length - 1 ? '' : <span className="mr-3" />}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{item.variant ? <span> {item.variant.name}</span> : ""}
|
)}
|
||||||
</span>
|
{variant === 'display' && (
|
||||||
</Link>
|
<div className="text-sm tracking-wider">{quantity}x</div>
|
||||||
{options && options.length > 0 ? (
|
)}
|
||||||
<div className="">
|
</div>
|
||||||
{options.map((option: ItemOption, i: number) => (
|
<div className="flex flex-col justify-between space-y-2 text-sm">
|
||||||
<span
|
<span>{price}</span>
|
||||||
key={`${item.id}-${option.name}`}
|
|
||||||
className="text-sm font-semibold text-accents-7"
|
|
||||||
>
|
|
||||||
{option.value}
|
|
||||||
{i === options.length - 1 ? '' : ', '}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="flex items-center mt-3">
|
|
||||||
<button type="button" onClick={() => increaseQuantity(-1)}>
|
|
||||||
<Minus width={18} height={18} />
|
|
||||||
</button>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
max={99}
|
|
||||||
min={0}
|
|
||||||
className={s.quantity}
|
|
||||||
value={quantity}
|
|
||||||
onChange={handleQuantity}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button type="button" onClick={() => increaseQuantity(1)}>
|
|
||||||
<Plus width={18} height={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-between space-y-2 text-base">
|
{variant === 'default' && (
|
||||||
<span>{price}</span>
|
<Quantity
|
||||||
<button
|
value={quantity}
|
||||||
className="flex justify-end outline-none"
|
handleRemove={handleRemove}
|
||||||
onClick={handleRemove}
|
handleChange={handleChange}
|
||||||
>
|
increase={() => increaseQuantity(1)}
|
||||||
<Trash />
|
decrease={() => increaseQuantity(-1)}
|
||||||
</button>
|
/>
|
||||||
</div>
|
)}
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply h-full flex flex-col;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root.empty {
|
.root.empty {
|
||||||
@apply bg-secondary text-secondary;
|
@apply bg-secondary text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root.success {
|
.lineItemsList {
|
||||||
@apply bg-green text-white;
|
@apply py-4 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accent-2 border-accent-2;
|
||||||
}
|
|
||||||
|
|
||||||
.root.error {
|
|
||||||
@apply bg-red text-white;
|
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { FC } from 'react'
|
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import CartItem from '../CartItem'
|
import { FC } from 'react'
|
||||||
import s from './CartSidebarView.module.css'
|
import s from './CartSidebarView.module.css'
|
||||||
import { Button } from '@components/ui'
|
import CartItem from '../CartItem'
|
||||||
import { UserNav } from '@components/common'
|
import { Button, Text } from '@components/ui'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import { Bag, Cross, Check } from '@components/icons'
|
import { Bag, Cross, Check } from '@components/icons'
|
||||||
import useCart from '@framework/cart/use-cart'
|
import useCart from '@framework/cart/use-cart'
|
||||||
import usePrice from '@framework/product/use-price'
|
import usePrice from '@framework/product/use-price'
|
||||||
|
import SidebarLayout from '@components/common/SidebarLayout'
|
||||||
|
|
||||||
const CartSidebarView: FC = () => {
|
const CartSidebarView: FC = () => {
|
||||||
const { closeSidebar } = useUI()
|
const { closeSidebar, setSidebarView } = useUI()
|
||||||
const { data, isLoading, isEmpty } = useCart()
|
const { data, isLoading, isEmpty } = useCart()
|
||||||
|
|
||||||
const { price: subTotal } = usePrice(
|
const { price: subTotal } = usePrice(
|
||||||
@ -27,33 +27,18 @@ const CartSidebarView: FC = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
const handleClose = () => closeSidebar()
|
const handleClose = () => closeSidebar()
|
||||||
|
const goToCheckout = () => setSidebarView('CHECKOUT_VIEW')
|
||||||
|
|
||||||
const error = null
|
const error = null
|
||||||
const success = null
|
const success = null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<SidebarLayout
|
||||||
className={cn(s.root, {
|
className={cn({
|
||||||
[s.empty]: error || success || isLoading || isEmpty,
|
[s.empty]: error || success || isLoading || isEmpty,
|
||||||
})}
|
})}
|
||||||
|
handleClose={handleClose}
|
||||||
>
|
>
|
||||||
<header className="px-4 pt-6 pb-4 sm:px-6">
|
|
||||||
<div className="flex items-start justify-between space-x-3">
|
|
||||||
<div className="h-7 flex items-center">
|
|
||||||
<button
|
|
||||||
onClick={handleClose}
|
|
||||||
aria-label="Close panel"
|
|
||||||
className="hover:text-gray-500 transition ease-in-out duration-150"
|
|
||||||
>
|
|
||||||
<Cross className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<UserNav />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{isLoading || isEmpty ? (
|
{isLoading || isEmpty ? (
|
||||||
<div className="flex-1 px-4 flex flex-col justify-center items-center">
|
<div className="flex-1 px-4 flex flex-col justify-center items-center">
|
||||||
<span className="border border-dashed border-primary rounded-full flex items-center justify-center w-16 h-16 p-12 bg-secondary text-secondary">
|
<span className="border border-dashed border-primary rounded-full flex items-center justify-center w-16 h-16 p-12 bg-secondary text-secondary">
|
||||||
@ -62,7 +47,7 @@ const CartSidebarView: FC = () => {
|
|||||||
<h2 className="pt-6 text-2xl font-bold tracking-wide text-center">
|
<h2 className="pt-6 text-2xl font-bold tracking-wide text-center">
|
||||||
Your cart is empty
|
Your cart is empty
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-accents-3 px-10 text-center pt-2">
|
<p className="text-accent-3 px-10 text-center pt-2">
|
||||||
Biscuit oat cake wafer icing ice cream tiramisu pudding cupcake.
|
Biscuit oat cake wafer icing ice cream tiramisu pudding cupcake.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -89,14 +74,11 @@ const CartSidebarView: FC = () => {
|
|||||||
<>
|
<>
|
||||||
<div className="px-4 sm:px-6 flex-1">
|
<div className="px-4 sm:px-6 flex-1">
|
||||||
<Link href="/cart">
|
<Link href="/cart">
|
||||||
<h2
|
<Text variant="sectionHeading" onClick={handleClose}>
|
||||||
className="pt-1 pb-4 text-2xl leading-7 font-bold text-base tracking-wide cursor-pointer inline-block"
|
|
||||||
onClick={handleClose}
|
|
||||||
>
|
|
||||||
My Cart
|
My Cart
|
||||||
</h2>
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
<ul className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accents-3 border-t border-accents-3">
|
<ul className={s.lineItemsList}>
|
||||||
{data!.lineItems.map((item: any) => (
|
{data!.lineItems.map((item: any) => (
|
||||||
<CartItem
|
<CartItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@ -107,34 +89,40 @@ const CartSidebarView: FC = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 py-5 sm:px-6">
|
<div className="flex-shrink-0 px-6 py-6 sm:px-6 sticky z-20 bottom-0 w-full right-0 left-0 bg-accent-0 border-t text-sm">
|
||||||
<div className="border-t border-accents-3">
|
<ul className="pb-2">
|
||||||
<ul className="py-3">
|
<li className="flex justify-between py-1">
|
||||||
<li className="flex justify-between py-1">
|
<span>Subtotal</span>
|
||||||
<span>Subtotal</span>
|
<span>{subTotal}</span>
|
||||||
<span>{subTotal}</span>
|
</li>
|
||||||
</li>
|
<li className="flex justify-between py-1">
|
||||||
<li className="flex justify-between py-1">
|
<span>Taxes</span>
|
||||||
<span>Taxes</span>
|
<span>Calculated at checkout</span>
|
||||||
<span>Calculated at checkout</span>
|
</li>
|
||||||
</li>
|
<li className="flex justify-between py-1">
|
||||||
<li className="flex justify-between py-1">
|
<span>Shipping</span>
|
||||||
<span>Estimated Shipping</span>
|
<span className="font-bold tracking-wide">FREE</span>
|
||||||
<span className="font-bold tracking-wide">FREE</span>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
</ul>
|
<div className="flex justify-between border-t border-accent-2 py-3 font-bold mb-2">
|
||||||
<div className="flex justify-between border-t border-accents-3 py-3 font-bold mb-10">
|
<span>Total</span>
|
||||||
<span>Total</span>
|
<span>{total}</span>
|
||||||
<span>{total}</span>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
{process.env.COMMERCE_CUSTOMCHECKOUT_ENABLED ? (
|
||||||
|
<Button Component="a" width="100%" onClick={goToCheckout}>
|
||||||
|
Proceed to Checkout ({total})
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button href="/checkout" Component="a" width="100%">
|
||||||
|
Proceed to Checkout
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button href="/checkout" Component="a" width="100%">
|
|
||||||
Proceed to Checkout
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</SidebarLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
.root {
|
||||||
|
min-height: calc(100vh - 322px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lineItemsList {
|
||||||
|
@apply py-4 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accent-2 border-accent-2;
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
import cn from 'classnames'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import CartItem from '@components/cart/CartItem'
|
||||||
|
import { Button, Text } from '@components/ui'
|
||||||
|
import { useUI } from '@components/ui/context'
|
||||||
|
import useCart from '@framework/cart/use-cart'
|
||||||
|
import usePrice from '@framework/product/use-price'
|
||||||
|
import ShippingWidget from '../ShippingWidget'
|
||||||
|
import PaymentWidget from '../PaymentWidget'
|
||||||
|
import SidebarLayout from '@components/common/SidebarLayout'
|
||||||
|
import s from './CheckoutSidebarView.module.css'
|
||||||
|
|
||||||
|
const CheckoutSidebarView: FC = () => {
|
||||||
|
const { setSidebarView } = useUI()
|
||||||
|
const { data } = useCart()
|
||||||
|
|
||||||
|
const { price: subTotal } = usePrice(
|
||||||
|
data && {
|
||||||
|
amount: Number(data.subtotalPrice),
|
||||||
|
currencyCode: data.currency.code,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const { price: total } = usePrice(
|
||||||
|
data && {
|
||||||
|
amount: Number(data.totalPrice),
|
||||||
|
currencyCode: data.currency.code,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarLayout
|
||||||
|
className={s.root}
|
||||||
|
handleBack={() => setSidebarView('CART_VIEW')}
|
||||||
|
>
|
||||||
|
<div className="px-4 sm:px-6 flex-1">
|
||||||
|
<Link href="/cart">
|
||||||
|
<Text variant="sectionHeading">Checkout</Text>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<PaymentWidget onClick={() => setSidebarView('PAYMENT_VIEW')} />
|
||||||
|
<ShippingWidget onClick={() => setSidebarView('SHIPPING_VIEW')} />
|
||||||
|
|
||||||
|
<ul className={s.lineItemsList}>
|
||||||
|
{data!.lineItems.map((item: any) => (
|
||||||
|
<CartItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
currencyCode={data!.currency.code}
|
||||||
|
variant="display"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0 px-6 py-6 sm:px-6 sticky z-20 bottom-0 w-full right-0 left-0 bg-accent-0 border-t text-sm">
|
||||||
|
<ul className="pb-2">
|
||||||
|
<li className="flex justify-between py-1">
|
||||||
|
<span>Subtotal</span>
|
||||||
|
<span>{subTotal}</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex justify-between py-1">
|
||||||
|
<span>Taxes</span>
|
||||||
|
<span>Calculated at checkout</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex justify-between py-1">
|
||||||
|
<span>Shipping</span>
|
||||||
|
<span className="font-bold tracking-wide">FREE</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div className="flex justify-between border-t border-accent-2 py-3 font-bold mb-2">
|
||||||
|
<span>Total</span>
|
||||||
|
<span>{total}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{/* Once data is correcly filled */}
|
||||||
|
{/* <Button Component="a" width="100%">
|
||||||
|
Confirm Purchase
|
||||||
|
</Button> */}
|
||||||
|
<Button Component="a" width="100%" variant="ghost" disabled>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SidebarLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CheckoutSidebarView
|
1
components/checkout/CheckoutSidebarView/index.ts
Normal file
1
components/checkout/CheckoutSidebarView/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './CheckoutSidebarView'
|
@ -0,0 +1,17 @@
|
|||||||
|
.fieldset {
|
||||||
|
@apply flex flex-col my-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldset .label {
|
||||||
|
@apply text-accent-7 uppercase text-xs font-medium mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldset .input,
|
||||||
|
.fieldset .select {
|
||||||
|
@apply p-2 border border-accent-2 w-full text-sm font-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldset .input:focus,
|
||||||
|
.fieldset .select:focus {
|
||||||
|
@apply outline-none shadow-outline-normal;
|
||||||
|
}
|
84
components/checkout/PaymentMethodView/PaymentMethodView.tsx
Normal file
84
components/checkout/PaymentMethodView/PaymentMethodView.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import cn from 'classnames'
|
||||||
|
import { Button, Text } from '@components/ui'
|
||||||
|
import { useUI } from '@components/ui/context'
|
||||||
|
import s from './PaymentMethodView.module.css'
|
||||||
|
import SidebarLayout from '@components/common/SidebarLayout'
|
||||||
|
|
||||||
|
const PaymentMethodView: FC = () => {
|
||||||
|
const { setSidebarView } = useUI()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
|
||||||
|
<div className="px-4 sm:px-6 flex-1">
|
||||||
|
<Text variant="sectionHeading"> Payment Method</Text>
|
||||||
|
<div>
|
||||||
|
<div className={s.fieldset}>
|
||||||
|
<label className={s.label}>Cardholder Name</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
||||||
|
<div className={cn(s.fieldset, 'col-span-7')}>
|
||||||
|
<label className={s.label}>Card Number</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={cn(s.fieldset, 'col-span-3')}>
|
||||||
|
<label className={s.label}>Expires</label>
|
||||||
|
<input className={s.input} placeholder="MM/YY" />
|
||||||
|
</div>
|
||||||
|
<div className={cn(s.fieldset, 'col-span-2')}>
|
||||||
|
<label className={s.label}>CVC</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr className="border-accent-2 my-6" />
|
||||||
|
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
||||||
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
|
<label className={s.label}>First Name</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
|
<label className={s.label}>Last Name</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={s.fieldset}>
|
||||||
|
<label className={s.label}>Company (Optional)</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={s.fieldset}>
|
||||||
|
<label className={s.label}>Street and House Number</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={s.fieldset}>
|
||||||
|
<label className={s.label}>Apartment, Suite, Etc. (Optional)</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
||||||
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
|
<label className={s.label}>Postal Code</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
|
<label className={s.label}>City</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={s.fieldset}>
|
||||||
|
<label className={s.label}>Country/Region</label>
|
||||||
|
<select className={s.select}>
|
||||||
|
<option>Hong Kong</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="sticky z-20 bottom-0 w-full right-0 left-0 py-12 bg-accent-0 border-t border-accent-2 px-6">
|
||||||
|
<Button Component="a" width="100%" variant="ghost">
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</SidebarLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PaymentMethodView
|
1
components/checkout/PaymentMethodView/index.ts
Normal file
1
components/checkout/PaymentMethodView/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './PaymentMethodView'
|
@ -0,0 +1,4 @@
|
|||||||
|
.root {
|
||||||
|
@apply border border-accent-2 px-6 py-5 mb-4 text-center
|
||||||
|
flex items-center cursor-pointer hover:border-accent-4;
|
||||||
|
}
|
29
components/checkout/PaymentWidget/PaymentWidget.tsx
Normal file
29
components/checkout/PaymentWidget/PaymentWidget.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import s from './PaymentWidget.module.css'
|
||||||
|
import { ChevronRight, CreditCard } from '@components/icons'
|
||||||
|
|
||||||
|
interface ComponentProps {
|
||||||
|
onClick?: () => any
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaymentWidget: FC<ComponentProps> = ({ onClick }) => {
|
||||||
|
/* Shipping Address
|
||||||
|
Only available with checkout set to true -
|
||||||
|
This means that the provider does offer checkout functionality. */
|
||||||
|
return (
|
||||||
|
<div onClick={onClick} className={s.root}>
|
||||||
|
<div className="flex flex-1 items-center">
|
||||||
|
<CreditCard className="w-5 flex" />
|
||||||
|
<span className="ml-5 text-sm text-center font-medium">
|
||||||
|
Add Payment Method
|
||||||
|
</span>
|
||||||
|
{/* <span>VISA #### #### #### 2345</span> */}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ChevronRight />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PaymentWidget
|
1
components/checkout/PaymentWidget/index.ts
Normal file
1
components/checkout/PaymentWidget/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './PaymentWidget'
|
21
components/checkout/ShippingView/ShippingView.module.css
Normal file
21
components/checkout/ShippingView/ShippingView.module.css
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.fieldset {
|
||||||
|
@apply flex flex-col my-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldset .label {
|
||||||
|
@apply text-accent-7 uppercase text-xs font-medium mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldset .input,
|
||||||
|
.fieldset .select {
|
||||||
|
@apply p-2 border border-accent-2 w-full text-sm font-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldset .input:focus,
|
||||||
|
.fieldset .select:focus {
|
||||||
|
@apply outline-none shadow-outline-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio {
|
||||||
|
@apply bg-black;
|
||||||
|
}
|
78
components/checkout/ShippingView/ShippingView.tsx
Normal file
78
components/checkout/ShippingView/ShippingView.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import cn from 'classnames'
|
||||||
|
import s from './ShippingView.module.css'
|
||||||
|
import Button from '@components/ui/Button'
|
||||||
|
import { useUI } from '@components/ui/context'
|
||||||
|
import SidebarLayout from '@components/common/SidebarLayout'
|
||||||
|
|
||||||
|
const PaymentMethodView: FC = () => {
|
||||||
|
const { setSidebarView } = useUI()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
|
||||||
|
<div className="px-4 sm:px-6 flex-1">
|
||||||
|
<h2 className="pt-1 pb-8 text-2xl font-semibold tracking-wide cursor-pointer inline-block">
|
||||||
|
Shipping
|
||||||
|
</h2>
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-row my-3 items-center">
|
||||||
|
<input className={s.radio} type="radio" />
|
||||||
|
<span className="ml-3 text-sm">Same as billing address</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row my-3 items-center">
|
||||||
|
<input className={s.radio} type="radio" />
|
||||||
|
<span className="ml-3 text-sm">
|
||||||
|
Use a different shipping address
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<hr className="border-accent-2 my-6" />
|
||||||
|
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
||||||
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
|
<label className={s.label}>First Name</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
|
<label className={s.label}>Last Name</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={s.fieldset}>
|
||||||
|
<label className={s.label}>Company (Optional)</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={s.fieldset}>
|
||||||
|
<label className={s.label}>Street and House Number</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={s.fieldset}>
|
||||||
|
<label className={s.label}>Apartment, Suite, Etc. (Optional)</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
||||||
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
|
<label className={s.label}>Postal Code</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||||
|
<label className={s.label}>City</label>
|
||||||
|
<input className={s.input} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={s.fieldset}>
|
||||||
|
<label className={s.label}>Country/Region</label>
|
||||||
|
<select className={s.select}>
|
||||||
|
<option>Hong Kong</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="sticky z-20 bottom-0 w-full right-0 left-0 py-12 bg-accent-0 border-t border-accent-2 px-6">
|
||||||
|
<Button Component="a" width="100%" variant="ghost">
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</SidebarLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PaymentMethodView
|
1
components/checkout/ShippingView/index.ts
Normal file
1
components/checkout/ShippingView/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ShippingView'
|
@ -0,0 +1,4 @@
|
|||||||
|
.root {
|
||||||
|
@apply border border-accent-2 px-6 py-5 mb-4 text-center
|
||||||
|
flex items-center cursor-pointer hover:border-accent-4;
|
||||||
|
}
|
33
components/checkout/ShippingWidget/ShippingWidget.tsx
Normal file
33
components/checkout/ShippingWidget/ShippingWidget.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import s from './ShippingWidget.module.css'
|
||||||
|
import { ChevronRight, MapPin } from '@components/icons'
|
||||||
|
import cn from 'classnames'
|
||||||
|
|
||||||
|
interface ComponentProps {
|
||||||
|
onClick?: () => any
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShippingWidget: FC<ComponentProps> = ({ onClick }) => {
|
||||||
|
/* Shipping Address
|
||||||
|
Only available with checkout set to true -
|
||||||
|
This means that the provider does offer checkout functionality. */
|
||||||
|
return (
|
||||||
|
<div onClick={onClick} className={s.root}>
|
||||||
|
<div className="flex flex-1 items-center">
|
||||||
|
<MapPin className="w-5 flex" />
|
||||||
|
<span className="ml-5 text-sm text-center font-medium">
|
||||||
|
Add Shipping Address
|
||||||
|
</span>
|
||||||
|
{/* <span>
|
||||||
|
1046 Kearny Street.<br/>
|
||||||
|
San Franssisco, California
|
||||||
|
</span> */}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ChevronRight />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShippingWidget
|
1
components/checkout/ShippingWidget/index.ts
Normal file
1
components/checkout/ShippingWidget/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ShippingWidget'
|
@ -14,7 +14,7 @@ const Avatar: FC<Props> = ({}) => {
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={{ backgroundImage: userAvatar }}
|
style={{ backgroundImage: userAvatar }}
|
||||||
className="inline-block h-8 w-8 rounded-full border-2 border-primary hover:border-secondary focus:border-secondary transition linear-out duration-150"
|
className="inline-block h-8 w-8 rounded-full border-2 border-primary hover:border-secondary focus:border-secondary transition-colors ease-linear"
|
||||||
>
|
>
|
||||||
{/* Add an image - We're generating a gradient as placeholder <img></img> */}
|
{/* Add an image - We're generating a gradient as placeholder <img></img> */}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply text-center p-6 bg-primary text-sm flex-row justify-center items-center font-medium fixed bottom-0 w-full z-30 transition-all duration-300 ease-out;
|
@apply text-center p-6 bg-primary text-sm flex-row justify-center items-center font-medium fixed bottom-0 w-full z-30 transition-all duration-300 ease-out;
|
||||||
}
|
|
||||||
|
|
||||||
@screen md {
|
@screen md {
|
||||||
.root {
|
|
||||||
@apply flex text-left;
|
@apply flex text-left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
.root {
|
||||||
|
@apply border-t border-accent-2;
|
||||||
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
& > svg {
|
& > svg {
|
||||||
@apply transform duration-75 ease-linear;
|
@apply transform duration-75 ease-linear;
|
||||||
|
@ -15,65 +15,50 @@ interface Props {
|
|||||||
pages?: Page[]
|
pages?: Page[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEGAL_PAGES = ['terms-of-use', 'shipping-returns', 'privacy-policy']
|
const links = [
|
||||||
|
{
|
||||||
|
name: 'Home',
|
||||||
|
url: '/',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const Footer: FC<Props> = ({ className, pages }) => {
|
const Footer: FC<Props> = ({ className, pages }) => {
|
||||||
const { sitePages, legalPages } = usePages(pages)
|
const { sitePages } = usePages(pages)
|
||||||
const rootClassName = cn(className)
|
const rootClassName = cn(s.root, className)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className={rootClassName}>
|
<footer className={rootClassName}>
|
||||||
<Container>
|
<Container>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 border-b border-accents-2 py-12 text-primary bg-primary transition-colors duration-150">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 border-b border-accent-2 py-12 text-primary bg-primary transition-colors duration-150">
|
||||||
<div className="col-span-1 lg:col-span-2">
|
<div className="col-span-1 lg:col-span-2">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a className="flex flex-initial items-center font-bold md:mr-24">
|
<a className="flex flex-initial items-center font-bold md:mr-24">
|
||||||
<span className="rounded-full border border-gray-700 mr-2">
|
<span className="rounded-full border border-accent-6 mr-2">
|
||||||
<Logo />
|
<Logo />
|
||||||
</span>
|
</span>
|
||||||
<span>ACME</span>
|
<span>ACME</span>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 lg:col-span-2">
|
<div className="col-span-1 lg:col-span-8">
|
||||||
<ul className="flex flex-initial flex-col md:flex-1">
|
<div className="grid md:grid-rows-4 md:grid-cols-3 md:grid-flow-col">
|
||||||
<li className="py-3 md:py-0 md:pb-4">
|
{[...links, ...sitePages].map((page) => (
|
||||||
<Link href="/">
|
<span key={page.url} className="py-3 md:py-0 md:pb-4">
|
||||||
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
|
|
||||||
Home
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{sitePages.map((page) => (
|
|
||||||
<li key={page.url} className="py-3 md:py-0 md:pb-4">
|
|
||||||
<Link href={page.url!}>
|
<Link href={page.url!}>
|
||||||
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
|
<a className="text-accent-9 hover:text-accent-6 transition ease-in-out duration-150">
|
||||||
{page.name}
|
{page.name}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</span>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 lg:col-span-2">
|
<div className="col-span-1 lg:col-span-2 flex items-start lg:justify-end text-primary">
|
||||||
<ul className="flex flex-initial flex-col md:flex-1">
|
|
||||||
{legalPages.map((page) => (
|
|
||||||
<li key={page.url} className="py-3 md:py-0 md:pb-4">
|
|
||||||
<Link href={page.url!}>
|
|
||||||
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
|
|
||||||
{page.name}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-1 lg:col-span-6 flex items-start lg:justify-end text-primary">
|
|
||||||
<div className="flex space-x-6 items-center h-10">
|
<div className="flex space-x-6 items-center h-10">
|
||||||
<a
|
<a
|
||||||
|
className={s.link}
|
||||||
aria-label="Github Repository"
|
aria-label="Github Repository"
|
||||||
href="https://github.com/vercel/commerce"
|
href="https://github.com/vercel/commerce"
|
||||||
className={s.link}
|
|
||||||
>
|
>
|
||||||
<Github />
|
<Github />
|
||||||
</a>
|
</a>
|
||||||
@ -81,12 +66,12 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-12 flex flex-col md:flex-row justify-between items-center space-y-4">
|
<div className="pt-6 pb-10 flex flex-col md:flex-row justify-between items-center space-y-4 text-accent-6 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<span>© 2020 ACME, Inc. All rights reserved.</span>
|
<span>© 2020 ACME, Inc. All rights reserved.</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-primary">
|
<div className="flex items-center text-primary text-sm">
|
||||||
<span className="text-primary">Crafted by</span>
|
<span className="text-primary">Created by</span>
|
||||||
<a
|
<a
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
href="https://vercel.com"
|
href="https://vercel.com"
|
||||||
@ -95,7 +80,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
className="text-primary"
|
className="text-primary"
|
||||||
>
|
>
|
||||||
<Vercel
|
<Vercel
|
||||||
className="inline-block h-6 ml-4 text-primary"
|
className="inline-block h-6 ml-3 text-primary"
|
||||||
alt="Vercel.com Logo"
|
alt="Vercel.com Logo"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
@ -109,34 +94,21 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
function usePages(pages?: Page[]) {
|
function usePages(pages?: Page[]) {
|
||||||
const { locale } = useRouter()
|
const { locale } = useRouter()
|
||||||
const sitePages: Page[] = []
|
const sitePages: Page[] = []
|
||||||
const legalPages: Page[] = []
|
|
||||||
|
|
||||||
if (pages) {
|
if (pages) {
|
||||||
pages.forEach((page) => {
|
pages.forEach((page) => {
|
||||||
const slug = page.url && getSlug(page.url)
|
const slug = page.url && getSlug(page.url)
|
||||||
|
|
||||||
if (!slug) return
|
if (!slug) return
|
||||||
if (locale && !slug.startsWith(`${locale}/`)) return
|
if (locale && !slug.startsWith(`${locale}/`)) return
|
||||||
|
sitePages.push(page)
|
||||||
if (isLegalPage(slug, locale)) {
|
|
||||||
legalPages.push(page)
|
|
||||||
} else {
|
|
||||||
sitePages.push(page)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sitePages: sitePages.sort(bySortOrder),
|
sitePages: sitePages.sort(bySortOrder),
|
||||||
legalPages: legalPages.sort(bySortOrder),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLegalPage = (slug: string, locale?: string) =>
|
|
||||||
locale
|
|
||||||
? LEGAL_PAGES.some((p) => `${locale}/${p}` === slug)
|
|
||||||
: LEGAL_PAGES.includes(slug)
|
|
||||||
|
|
||||||
// Sort pages by the sort order assigned in the BC dashboard
|
// Sort pages by the sort order assigned in the BC dashboard
|
||||||
function bySortOrder(a: Page, b: Page) {
|
function bySortOrder(a: Page, b: Page) {
|
||||||
return (a.sort_order ?? 0) - (b.sort_order ?? 0)
|
return (a.sort_order ?? 0) - (b.sort_order ?? 0)
|
||||||
|
@ -28,7 +28,7 @@ const HomeAllProductsGrid: FC<Props> = ({
|
|||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{categories.map((cat: any) => (
|
{categories.map((cat: any) => (
|
||||||
<li key={cat.path} className="py-1 text-accents-8 text-base">
|
<li key={cat.path} className="py-1 text-accent-8 text-base">
|
||||||
<Link href={getCategoryPath(cat.path)}>
|
<Link href={getCategoryPath(cat.path)}>
|
||||||
<a>{cat.name}</a>
|
<a>{cat.name}</a>
|
||||||
</Link>
|
</Link>
|
||||||
@ -42,7 +42,7 @@ const HomeAllProductsGrid: FC<Props> = ({
|
|||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{brands.flatMap(({ node }: any) => (
|
{brands.flatMap(({ node }: any) => (
|
||||||
<li key={node.path} className="py-1 text-accents-8 text-base">
|
<li key={node.path} className="py-1 text-accent-8 text-base">
|
||||||
<Link href={getDesignerPath(node.path)}>
|
<Link href={getDesignerPath(node.path)}>
|
||||||
<a>{node.name}</a>
|
<a>{node.name}</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@apply h-10 px-2 rounded-md border border-accents-2 flex items-center justify-center;
|
@apply h-10 px-2 rounded-md border border-accent-2 flex items-center justify-center transition-colors ease-linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
@apply border-accents-4 shadow-sm;
|
@apply border-accent-3 shadow-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:focus {
|
.button:focus {
|
||||||
@ -16,16 +16,14 @@
|
|||||||
|
|
||||||
.dropdownMenu {
|
.dropdownMenu {
|
||||||
@apply fixed right-0 top-12 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
|
@apply fixed right-0 top-12 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
|
||||||
}
|
|
||||||
|
|
||||||
@screen lg {
|
@screen lg {
|
||||||
.dropdownMenu {
|
@apply absolute border border-accent-1 shadow-lg w-56 h-auto;
|
||||||
@apply absolute border border-accents-1 shadow-lg w-56 h-auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@screen md {
|
.closeButton {
|
||||||
.closeButton {
|
@screen md {
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -36,9 +34,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item:hover {
|
.item:hover {
|
||||||
@apply bg-accents-1;
|
@apply bg-accent-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon.active {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ const I18nWidget: FC = () => {
|
|||||||
/>
|
/>
|
||||||
{options && (
|
{options && (
|
||||||
<span className="cursor-pointer">
|
<span className="cursor-pointer">
|
||||||
<ChevronUp className={cn({ [s.icon]: display })} />
|
<ChevronUp className={cn(s.icon, { [s.active]: display })} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
import React, { FC } from 'react'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import dynamic from 'next/dynamic'
|
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import type { Page } from '@commerce/types/page'
|
import React, { FC } from 'react'
|
||||||
import type { Category } from '@commerce/types/site'
|
import dynamic from 'next/dynamic'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
import { CommerceProvider } from '@framework'
|
import { CommerceProvider } from '@framework'
|
||||||
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
|
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
|
import type { Page } from '@commerce/types/page'
|
||||||
import { Navbar, Footer } from '@components/common'
|
import { Navbar, Footer } from '@components/common'
|
||||||
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
|
import type { Category } from '@commerce/types/site'
|
||||||
|
import ShippingView from '@components/checkout/ShippingView'
|
||||||
import CartSidebarView from '@components/cart/CartSidebarView'
|
import CartSidebarView from '@components/cart/CartSidebarView'
|
||||||
|
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
|
||||||
|
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
|
||||||
|
import PaymentMethodView from '@components/checkout/PaymentMethodView'
|
||||||
|
import CheckoutSidebarView from '@components/checkout/CheckoutSidebarView'
|
||||||
|
|
||||||
import LoginView from '@components/auth/LoginView'
|
import LoginView from '@components/auth/LoginView'
|
||||||
import s from './Layout.module.css'
|
import s from './Layout.module.css'
|
||||||
|
|
||||||
@ -45,15 +49,53 @@ interface Props {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ModalView: FC<{ modalView: string; closeModal(): any }> = ({
|
||||||
|
modalView,
|
||||||
|
closeModal,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Modal onClose={closeModal}>
|
||||||
|
{modalView === 'LOGIN_VIEW' && <LoginView />}
|
||||||
|
{modalView === 'SIGNUP_VIEW' && <SignUpView />}
|
||||||
|
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalUI: FC = () => {
|
||||||
|
const { displayModal, closeModal, modalView } = useUI()
|
||||||
|
return displayModal ? (
|
||||||
|
<ModalView modalView={modalView} closeModal={closeModal} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarView: FC<{ sidebarView: string; closeSidebar(): any }> = ({
|
||||||
|
sidebarView,
|
||||||
|
closeSidebar,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Sidebar onClose={closeSidebar}>
|
||||||
|
{sidebarView === 'CART_VIEW' && <CartSidebarView />}
|
||||||
|
{sidebarView === 'CHECKOUT_VIEW' && <CheckoutSidebarView />}
|
||||||
|
{sidebarView === 'PAYMENT_VIEW' && <PaymentMethodView />}
|
||||||
|
{sidebarView === 'SHIPPING_VIEW' && <ShippingView />}
|
||||||
|
</Sidebar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarUI: FC = () => {
|
||||||
|
const { displaySidebar, closeSidebar, sidebarView } = useUI()
|
||||||
|
return displaySidebar ? (
|
||||||
|
<SidebarView sidebarView={sidebarView} closeSidebar={closeSidebar} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
const Layout: FC<Props> = ({
|
const Layout: FC<Props> = ({
|
||||||
children,
|
children,
|
||||||
pageProps: { categories = [], ...pageProps },
|
pageProps: { categories = [], ...pageProps },
|
||||||
}) => {
|
}) => {
|
||||||
const { displaySidebar, displayModal, closeSidebar, closeModal, modalView } =
|
|
||||||
useUI()
|
|
||||||
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
|
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
|
||||||
const { locale = 'en-US' } = useRouter()
|
const { locale = 'en-US' } = useRouter()
|
||||||
|
|
||||||
const navBarlinks = categories.slice(0, 2).map((c) => ({
|
const navBarlinks = categories.slice(0, 2).map((c) => ({
|
||||||
label: c.name,
|
label: c.name,
|
||||||
href: `/search/${c.slug}`,
|
href: `/search/${c.slug}`,
|
||||||
@ -65,17 +107,8 @@ const Layout: FC<Props> = ({
|
|||||||
<Navbar links={navBarlinks} />
|
<Navbar links={navBarlinks} />
|
||||||
<main className="fit">{children}</main>
|
<main className="fit">{children}</main>
|
||||||
<Footer pages={pageProps.pages} />
|
<Footer pages={pageProps.pages} />
|
||||||
|
<ModalUI />
|
||||||
<Modal open={displayModal} onClose={closeModal}>
|
<SidebarUI />
|
||||||
{modalView === 'LOGIN_VIEW' && <LoginView />}
|
|
||||||
{modalView === 'SIGNUP_VIEW' && <SignUpView />}
|
|
||||||
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Sidebar open={displaySidebar} onClose={closeSidebar}>
|
|
||||||
<CartSidebarView />
|
|
||||||
</Sidebar>
|
|
||||||
|
|
||||||
<FeatureBar
|
<FeatureBar
|
||||||
title="This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy."
|
title="This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy."
|
||||||
hide={acceptedCookies}
|
hide={acceptedCookies}
|
||||||
|
@ -2,16 +2,26 @@
|
|||||||
@apply sticky top-0 bg-primary z-40 transition-all duration-150;
|
@apply sticky top-0 bg-primary z-40 transition-all duration-150;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
@apply relative flex flex-row justify-between py-4 md:py-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navMenu {
|
||||||
|
@apply hidden ml-6 space-x-4 lg:block;
|
||||||
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
@apply inline-flex items-center text-primary leading-6 font-medium transition ease-in-out duration-75 cursor-pointer text-accents-6;
|
@apply inline-flex items-center leading-6
|
||||||
|
transition ease-in-out duration-75 cursor-pointer
|
||||||
|
text-accent-5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link:hover {
|
.link:hover {
|
||||||
@apply text-accents-9;
|
@apply text-accent-9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link:focus {
|
.link:focus {
|
||||||
@apply outline-none text-accents-8;
|
@apply outline-none text-accent-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import s from './Navbar.module.css'
|
||||||
|
import NavbarRoot from './NavbarRoot'
|
||||||
import { Logo, Container } from '@components/ui'
|
import { Logo, Container } from '@components/ui'
|
||||||
import { Searchbar, UserNav } from '@components/common'
|
import { Searchbar, UserNav } from '@components/common'
|
||||||
import NavbarRoot from './NavbarRoot'
|
|
||||||
import s from './Navbar.module.css'
|
|
||||||
|
|
||||||
interface Link {
|
interface Link {
|
||||||
href: string
|
href: string
|
||||||
@ -16,14 +16,14 @@ interface NavbarProps {
|
|||||||
const Navbar: FC<NavbarProps> = ({ links }) => (
|
const Navbar: FC<NavbarProps> = ({ links }) => (
|
||||||
<NavbarRoot>
|
<NavbarRoot>
|
||||||
<Container>
|
<Container>
|
||||||
<div className="relative flex flex-row justify-between py-4 align-center md:py-6">
|
<div className={s.nav}>
|
||||||
<div className="flex items-center flex-1">
|
<div className="flex items-center flex-1">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a className={s.logo} aria-label="Logo">
|
<a className={s.logo} aria-label="Logo">
|
||||||
<Logo />
|
<Logo />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="hidden ml-6 space-x-4 lg:block">
|
<nav className={s.navMenu}>
|
||||||
<Link href="/search">
|
<Link href="/search">
|
||||||
<a className={s.link}>All</a>
|
<a className={s.link}>All</a>
|
||||||
</Link>
|
</Link>
|
||||||
@ -34,16 +34,13 @@ const Navbar: FC<NavbarProps> = ({ links }) => (
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="justify-center flex-1 hidden lg:flex">
|
<div className="justify-center flex-1 hidden lg:flex">
|
||||||
<Searchbar />
|
<Searchbar />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-end flex-1 space-x-8">
|
||||||
<div className="flex justify-end flex-1 space-x-8">
|
|
||||||
<UserNav />
|
<UserNav />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex pb-4 lg:px-6 lg:hidden">
|
<div className="flex pb-4 lg:px-6 lg:hidden">
|
||||||
<Searchbar id="mobile-search" />
|
<Searchbar id="mobile-search" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
|
.root {
|
||||||
|
@apply relative text-sm bg-accent-0 text-base w-full transition-colors duration-150 border border-accent-2;
|
||||||
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
@apply bg-transparent px-3 py-2 appearance-none w-full transition duration-150 ease-in-out pr-10;
|
@apply bg-transparent px-3 py-2 appearance-none w-full transition duration-150 ease-in-out pr-10;
|
||||||
|
}
|
||||||
|
|
||||||
@screen sm {
|
.input::placeholder {
|
||||||
min-width: 300px;
|
@apply text-accent-3;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.input:focus {
|
.input:focus {
|
||||||
@ -17,3 +21,9 @@
|
|||||||
.icon {
|
.icon {
|
||||||
@apply h-5 w-5;
|
@apply h-5 w-5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@screen sm {
|
||||||
|
.input {
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, useEffect, useMemo } from 'react'
|
import { FC, InputHTMLAttributes, useEffect, useMemo } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import s from './Searchbar.module.css'
|
import s from './Searchbar.module.css'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
@ -15,14 +15,26 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
|||||||
router.prefetch('/search')
|
router.prefetch('/search')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const q = e.currentTarget.value
|
||||||
|
|
||||||
|
router.push(
|
||||||
|
{
|
||||||
|
pathname: `/search`,
|
||||||
|
query: q ? { q } : {},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ shallow: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => (
|
() => (
|
||||||
<div
|
<div className={cn(s.root, className)}>
|
||||||
className={cn(
|
|
||||||
'relative text-sm bg-accents-1 text-base w-full transition-colors duration-150',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<label className="hidden" htmlFor={id}>
|
<label className="hidden" htmlFor={id}>
|
||||||
Search
|
Search
|
||||||
</label>
|
</label>
|
||||||
@ -31,22 +43,7 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
|||||||
className={s.input}
|
className={s.input}
|
||||||
placeholder="Search for products..."
|
placeholder="Search for products..."
|
||||||
defaultValue={router.query.q}
|
defaultValue={router.query.q}
|
||||||
onKeyUp={(e) => {
|
onKeyUp={handleKeyUp}
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
const q = e.currentTarget.value
|
|
||||||
|
|
||||||
router.push(
|
|
||||||
{
|
|
||||||
pathname: `/search`,
|
|
||||||
query: q ? { q } : {},
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{ shallow: true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div className={s.iconContainer}>
|
<div className={s.iconContainer}>
|
||||||
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
|
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
20
components/common/SidebarLayout/SidebarLayout.module.css
Normal file
20
components/common/SidebarLayout/SidebarLayout.module.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.root {
|
||||||
|
@apply relative h-full flex flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
@apply sticky top-0 pl-4 py-4 pr-6
|
||||||
|
flex items-center justify-between
|
||||||
|
bg-accent-0 box-border w-full z-10;
|
||||||
|
min-height: 66px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
@apply flex flex-col flex-1 box-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen lg {
|
||||||
|
.header {
|
||||||
|
min-height: 74px;
|
||||||
|
}
|
||||||
|
}
|
50
components/common/SidebarLayout/SidebarLayout.tsx
Normal file
50
components/common/SidebarLayout/SidebarLayout.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import { Cross, ChevronLeft } from '@components/icons'
|
||||||
|
import { UserNav } from '@components/common'
|
||||||
|
import cn from 'classnames'
|
||||||
|
import s from './SidebarLayout.module.css'
|
||||||
|
|
||||||
|
type ComponentProps = { className?: string } & (
|
||||||
|
| { handleClose: () => any; handleBack?: never }
|
||||||
|
| { handleBack: () => any; handleClose?: never }
|
||||||
|
)
|
||||||
|
|
||||||
|
const SidebarLayout: FC<ComponentProps> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
handleClose,
|
||||||
|
handleBack,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={cn(s.root, className)}>
|
||||||
|
<header className={s.header}>
|
||||||
|
{handleClose && (
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
aria-label="Close"
|
||||||
|
className="hover:text-accent-5 transition ease-in-out duration-150 flex items-center focus:outline-none"
|
||||||
|
>
|
||||||
|
<Cross className="h-6 w-6 hover:text-accent-3" />
|
||||||
|
<span className="ml-2 text-accent-7 text-sm ">Close</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{handleBack && (
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
aria-label="Go back"
|
||||||
|
className="hover:text-accent-5 transition ease-in-out duration-150 flex items-center focus:outline-none"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-6 w-6 hover:text-accent-3" />
|
||||||
|
<span className="ml-2 text-accent-7 text-xs">Back</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<span className={s.nav}>
|
||||||
|
<UserNav />
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<div className={s.container}>{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SidebarLayout
|
1
components/common/SidebarLayout/index.ts
Normal file
1
components/common/SidebarLayout/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './SidebarLayout'
|
@ -1,10 +1,8 @@
|
|||||||
.dropdownMenu {
|
.dropdownMenu {
|
||||||
@apply fixed right-0 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
|
@apply fixed right-0 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
|
||||||
}
|
|
||||||
|
|
||||||
@screen lg {
|
@screen lg {
|
||||||
.dropdownMenu {
|
@apply absolute top-10 border border-accent-1 shadow-lg w-56 h-auto;
|
||||||
@apply absolute top-10 border border-accents-1 shadow-lg w-56 h-auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,11 +12,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.link:hover {
|
.link:hover {
|
||||||
@apply bg-accents-1;
|
@apply bg-accent-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link.active {
|
.link.active {
|
||||||
@apply font-bold bg-accents-2;
|
@apply font-bold bg-accent-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.off {
|
.off {
|
||||||
|
@ -109,7 +109,7 @@ const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
className={cn(s.link, 'border-t border-accents-2 mt-4')}
|
className={cn(s.link, 'border-t border-accent-2 mt-4')}
|
||||||
onClick={() => logout()}
|
onClick={() => logout()}
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
@apply mr-6 cursor-pointer relative transition ease-in-out duration-100 flex items-center outline-none text-primary;
|
@apply mr-6 cursor-pointer relative transition ease-in-out duration-100 flex items-center outline-none text-primary;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply text-accents-6 transition scale-110 duration-100;
|
@apply text-accent-6 transition scale-110 duration-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@ -24,7 +24,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bagCount {
|
.bagCount {
|
||||||
@apply border border-accents-1 bg-secondary text-secondary absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs;
|
@apply border border-accent-1 bg-secondary text-secondary absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs;
|
||||||
padding-left: 2.5px;
|
padding-left: 2.5px;
|
||||||
padding-right: 2.5px;
|
padding-right: 2.5px;
|
||||||
min-width: 1.25rem;
|
min-width: 1.25rem;
|
||||||
|
@ -24,21 +24,21 @@ const UserNav: FC<Props> = ({ className }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={cn(s.root, className)}>
|
<nav className={cn(s.root, className)}>
|
||||||
<div className={s.mainContainer}>
|
<ul className={s.list}>
|
||||||
<ul className={s.list}>
|
<li className={s.item} onClick={toggleSidebar}>
|
||||||
<li className={s.item} onClick={toggleSidebar}>
|
<Bag />
|
||||||
<Bag />
|
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
||||||
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
</li>
|
||||||
|
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||||
|
<li className={s.item}>
|
||||||
|
<Link href="/wishlist">
|
||||||
|
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
|
||||||
|
<Heart />
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
)}
|
||||||
<li className={s.item}>
|
{process.env.COMMERCE_CUSTOMER_ENABLED && (
|
||||||
<Link href="/wishlist">
|
|
||||||
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
|
|
||||||
<Heart />
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
<li className={s.item}>
|
<li className={s.item}>
|
||||||
{customer ? (
|
{customer ? (
|
||||||
<DropdownMenu />
|
<DropdownMenu />
|
||||||
@ -52,8 +52,8 @@ const UserNav: FC<Props> = ({ className }) => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
)}
|
||||||
</div>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,22 @@
|
|||||||
const RightArrow = ({ ...props }) => {
|
const ArrowRight = ({ ...props }) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M5 12H19"
|
d="M5 12H19"
|
||||||
stroke="white"
|
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M12 5L19 12L12 19"
|
d="M12 5L19 12L12 19"
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
strokeWidth="1.5"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@ -26,4 +25,4 @@ const RightArrow = ({ ...props }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RightArrow
|
export default ArrowRight
|
20
components/icons/ChevronDown.tsx
Normal file
20
components/icons/ChevronDown.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const ChevronDown = ({ ...props }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
fill="none"
|
||||||
|
shapeRendering="geometricPrecision"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M6 9l6 6 6-6" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChevronDown
|
20
components/icons/ChevronLeft.tsx
Normal file
20
components/icons/ChevronLeft.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const ChevronLeft = ({ ...props }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
fill="none"
|
||||||
|
shapeRendering="geometricPrecision"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M15 18l-6-6 6-6" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChevronLeft
|
20
components/icons/ChevronRight.tsx
Normal file
20
components/icons/ChevronRight.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const ChevronUp = ({ ...props }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
fill="none"
|
||||||
|
shapeRendering="geometricPrecision"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M9 18l6-6-6-6" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChevronUp
|
@ -10,6 +10,7 @@ const CreditCard = ({ ...props }) => {
|
|||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
shapeRendering="geometricPrecision"
|
shapeRendering="geometricPrecision"
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
|
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
|
||||||
<path d="M1 10h22" />
|
<path d="M1 10h22" />
|
||||||
|
16
components/icons/Star.tsx
Normal file
16
components/icons/Star.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const Star = ({ ...props }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M12.43 8L10 0L7.57 8H0L6.18 12.41L3.83 20L10 15.31L16.18 20L13.83 12.41L20 8H12.43Z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Star
|
@ -2,17 +2,21 @@ export { default as Bag } from './Bag'
|
|||||||
export { default as Heart } from './Heart'
|
export { default as Heart } from './Heart'
|
||||||
export { default as Trash } from './Trash'
|
export { default as Trash } from './Trash'
|
||||||
export { default as Cross } from './Cross'
|
export { default as Cross } from './Cross'
|
||||||
export { default as ArrowLeft } from './ArrowLeft'
|
|
||||||
export { default as Plus } from './Plus'
|
export { default as Plus } from './Plus'
|
||||||
export { default as Minus } from './Minus'
|
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 Github } from './Github'
|
||||||
export { default as DoubleChevron } from './DoubleChevron'
|
|
||||||
export { default as RightArrow } from './RightArrow'
|
|
||||||
export { default as Info } from './Info'
|
export { default as Info } from './Info'
|
||||||
export { default as ChevronUp } from './ChevronUp'
|
|
||||||
export { default as Vercel } from './Vercel'
|
export { default as Vercel } from './Vercel'
|
||||||
export { default as MapPin } from './MapPin'
|
export { default as MapPin } from './MapPin'
|
||||||
|
export { default as Star } from './Star'
|
||||||
|
export { default as ArrowLeft } from './ArrowLeft'
|
||||||
|
export { default as ArrowRight } from './ArrowRight'
|
||||||
export { default as CreditCard } from './CreditCard'
|
export { default as CreditCard } from './CreditCard'
|
||||||
|
export { default as ChevronUp } from './ChevronUp'
|
||||||
|
export { default as ChevronLeft } from './ChevronLeft'
|
||||||
|
export { default as ChevronDown } from './ChevronDown'
|
||||||
|
export { default as ChevronRight } from './ChevronRight'
|
||||||
|
export { default as DoubleChevron } from './DoubleChevron'
|
||||||
|
@ -1,136 +1,114 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply relative max-h-full w-full box-border overflow-hidden
|
@apply relative max-h-full w-full box-border overflow-hidden
|
||||||
bg-no-repeat bg-center bg-cover transition-transform
|
bg-no-repeat bg-center bg-cover transition-transform
|
||||||
ease-linear cursor-pointer;
|
ease-linear cursor-pointer inline-block bg-accent-1;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
.root:hover {
|
||||||
& .squareBg:before {
|
& .productImage {
|
||||||
transform: scale(0.98);
|
transform: scale(1.2625);
|
||||||
}
|
|
||||||
|
|
||||||
& .productImage {
|
|
||||||
transform: scale(1.2625);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .productTitle > span,
|
|
||||||
& .productPrice,
|
|
||||||
& .wishlistButton {
|
|
||||||
@apply bg-secondary text-secondary;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(6n + 1) .productTitle > span,
|
|
||||||
&:nth-child(6n + 1) .productPrice,
|
|
||||||
&:nth-child(6n + 1) .wishlistButton {
|
|
||||||
@apply bg-violet text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(6n + 5) .productTitle > span,
|
|
||||||
&:nth-child(6n + 5) .productPrice,
|
|
||||||
&:nth-child(6n + 5) .wishlistButton {
|
|
||||||
@apply bg-blue text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(6n + 3) .productTitle > span,
|
|
||||||
&:nth-child(6n + 3) .productPrice,
|
|
||||||
&:nth-child(6n + 3) .wishlistButton {
|
|
||||||
@apply bg-pink text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(6n + 6) .productTitle > span,
|
|
||||||
&:nth-child(6n + 6) .productPrice,
|
|
||||||
&:nth-child(6n + 6) .wishlistButton {
|
|
||||||
@apply bg-cyan text-white;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(6n + 1) .squareBg {
|
& .header .name span,
|
||||||
@apply bg-violet;
|
& .header .price,
|
||||||
|
& .wishlistButton {
|
||||||
|
@apply bg-secondary text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(6n + 5) .squareBg {
|
&:nth-child(6n + 1) .header .name span,
|
||||||
@apply bg-blue;
|
&:nth-child(6n + 1) .header .price,
|
||||||
|
&:nth-child(6n + 1) .wishlistButton {
|
||||||
|
@apply bg-violet text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(6n + 3) .squareBg {
|
&:nth-child(6n + 5) .header .name span,
|
||||||
@apply bg-pink;
|
&:nth-child(6n + 5) .header .price,
|
||||||
|
&:nth-child(6n + 5) .wishlistButton {
|
||||||
|
@apply bg-blue text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(6n + 6) .squareBg {
|
&:nth-child(6n + 3) .header .name span,
|
||||||
@apply bg-cyan;
|
&:nth-child(6n + 3) .header .price,
|
||||||
|
&:nth-child(6n + 3) .wishlistButton {
|
||||||
|
@apply bg-pink text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(6n + 6) .header .name span,
|
||||||
|
&:nth-child(6n + 6) .header .price,
|
||||||
|
&:nth-child(6n + 6) .wishlistButton {
|
||||||
|
@apply bg-cyan text-white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.squareBg,
|
.header {
|
||||||
.productTitle > span,
|
@apply transition-colors ease-in-out duration-500
|
||||||
.productPrice,
|
absolute top-0 left-0 z-20 pr-16;
|
||||||
.wishlistButton {
|
|
||||||
@apply transition-colors ease-in-out duration-500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.squareBg {
|
.header .name {
|
||||||
@apply transition-colors absolute inset-0 z-0;
|
@apply pt-0 max-w-full w-full leading-extra-loose
|
||||||
background-color: #212529;
|
transition-colors ease-in-out duration-500;
|
||||||
}
|
|
||||||
|
|
||||||
.squareBg:before {
|
|
||||||
@apply transition ease-in-out duration-500 bg-repeat-space w-full h-full block;
|
|
||||||
background-image: url('/bg-products.svg');
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
.simple {
|
|
||||||
& .squareBg {
|
|
||||||
@apply bg-accents-0 !important;
|
|
||||||
background-image: url('/bg-products.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
& .productTitle {
|
|
||||||
@apply pt-2;
|
|
||||||
font-size: 1rem;
|
|
||||||
|
|
||||||
& span {
|
|
||||||
@apply leading-extra-loose;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .productPrice {
|
|
||||||
@apply text-sm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.productTitle {
|
|
||||||
@apply pt-0 max-w-full w-full leading-extra-loose;
|
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
letter-spacing: 0.4px;
|
letter-spacing: 0.4px;
|
||||||
|
|
||||||
& span {
|
|
||||||
@apply py-4 px-6 bg-primary text-primary font-bold;
|
|
||||||
font-size: inherit;
|
|
||||||
letter-spacing: inherit;
|
|
||||||
box-decoration-break: clone;
|
|
||||||
-webkit-box-decoration-break: clone;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.productPrice {
|
.header .name span {
|
||||||
@apply py-4 px-6 bg-primary text-primary font-semibold inline-block text-sm leading-6;
|
@apply py-4 px-6 bg-primary text-primary font-bold
|
||||||
letter-spacing: 0.4px;
|
transition-colors ease-in-out duration-500;
|
||||||
|
font-size: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
box-decoration-break: clone;
|
||||||
|
-webkit-box-decoration-break: clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wishlistButton {
|
.header .price {
|
||||||
@apply w-10 h-10 flex ml-auto items-center justify-center bg-primary text-primary font-semibold text-xs leading-6 cursor-pointer;
|
@apply pt-2 px-6 pb-4 text-sm bg-primary text-accent-9
|
||||||
|
font-semibold inline-block tracking-wide
|
||||||
|
transition-colors ease-in-out duration-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.imageContainer {
|
.imageContainer {
|
||||||
@apply flex items-center justify-center;
|
@apply flex items-center justify-center overflow-hidden;
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
& > div {
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.productImage {
|
.imageContainer > div {
|
||||||
@apply transform transition-transform duration-500 object-cover scale-120;
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer .productImage {
|
||||||
|
@apply transform transition-transform duration-500
|
||||||
|
object-cover scale-120;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root .wishlistButton {
|
||||||
|
@apply top-0 right-0 z-30 absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variant Simple */
|
||||||
|
.simple .header .name {
|
||||||
|
@apply pt-2 text-lg leading-10 -mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple .header .price {
|
||||||
|
@apply text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variant Slim */
|
||||||
|
.slim {
|
||||||
|
@apply bg-transparent relative overflow-hidden
|
||||||
|
box-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slim .header {
|
||||||
|
@apply absolute inset-0 flex items-center justify-end mr-8 z-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slim span {
|
||||||
|
@apply bg-accent-9 text-accent-0 inline-block p-3
|
||||||
|
font-bold text-xl break-words;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root:global(.secondary) .header span {
|
||||||
|
@apply bg-accent-0 text-accent-9;
|
||||||
}
|
}
|
||||||
|
@ -5,58 +5,98 @@ import type { Product } from '@commerce/types/product'
|
|||||||
import s from './ProductCard.module.css'
|
import s from './ProductCard.module.css'
|
||||||
import Image, { ImageProps } from 'next/image'
|
import Image, { ImageProps } from 'next/image'
|
||||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||||
|
import usePrice from '@framework/product/use-price'
|
||||||
|
import ProductTag from '../ProductTag'
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
product: Product
|
product: Product
|
||||||
variant?: 'slim' | 'simple'
|
noNameTag?: boolean
|
||||||
imgProps?: Omit<any, 'src'>
|
imgProps?: Omit<ImageProps, 'src'>
|
||||||
|
variant?: 'default' | 'slim' | 'simple'
|
||||||
}
|
}
|
||||||
|
|
||||||
const placeholderImg = '/product-img-placeholder.svg'
|
const placeholderImg = '/product-img-placeholder.svg'
|
||||||
|
|
||||||
const ProductCard: FC<Props> = ({
|
const ProductCard: FC<Props> = ({
|
||||||
className,
|
|
||||||
product,
|
product,
|
||||||
variant,
|
|
||||||
imgProps,
|
imgProps,
|
||||||
|
className,
|
||||||
|
noNameTag = false,
|
||||||
|
variant = 'default',
|
||||||
...props
|
...props
|
||||||
}) => (
|
}) => {
|
||||||
<Link href={`/product/${product.slug}`} {...props}>
|
const { price } = usePrice({
|
||||||
<a className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}>
|
amount: product.price.value,
|
||||||
{variant === 'slim' ? (
|
baseAmount: product.price.retailPrice,
|
||||||
<div className="relative overflow-hidden box-border">
|
currencyCode: product.price.currencyCode!,
|
||||||
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
|
})
|
||||||
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
|
|
||||||
{product.name}
|
const rootClassName = cn(
|
||||||
</span>
|
s.root,
|
||||||
</div>
|
{ [s.slim]: variant === 'slim', [s.simple]: variant === 'simple' },
|
||||||
{product?.images && (
|
className
|
||||||
<Image
|
)
|
||||||
quality="85"
|
|
||||||
src={product.images[0]?.url || placeholderImg}
|
return (
|
||||||
alt={product.name || 'Product Image'}
|
<Link href={`/product/${product.slug}`} {...props}>
|
||||||
height={320}
|
<a className={rootClassName}>
|
||||||
width={320}
|
{variant === 'slim' && (
|
||||||
layout="fixed"
|
<>
|
||||||
{...imgProps}
|
<div className={s.header}>
|
||||||
/>
|
<span>{product.name}</span>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className={s.squareBg} />
|
|
||||||
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
|
|
||||||
<div className="absolute top-0 left-0 pr-16 max-w-full">
|
|
||||||
<h3 className={s.productTitle}>
|
|
||||||
<span>{product.name}</span>
|
|
||||||
</h3>
|
|
||||||
<span className={s.productPrice}>
|
|
||||||
{product.price.value}
|
|
||||||
|
|
||||||
{product.price.currencyCode}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
{product?.images && (
|
||||||
|
<Image
|
||||||
|
quality="85"
|
||||||
|
src={product.images[0]?.url || placeholderImg}
|
||||||
|
alt={product.name || 'Product Image'}
|
||||||
|
height={320}
|
||||||
|
width={320}
|
||||||
|
layout="fixed"
|
||||||
|
{...imgProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{variant === 'simple' && (
|
||||||
|
<>
|
||||||
|
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||||
|
<WishlistButton
|
||||||
|
className={s.wishlistButton}
|
||||||
|
productId={product.id}
|
||||||
|
variant={product.variants[0]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!noNameTag && (
|
||||||
|
<div className={s.header}>
|
||||||
|
<h3 className={s.name}>
|
||||||
|
<span>{product.name}</span>
|
||||||
|
</h3>
|
||||||
|
<div className={s.price}>
|
||||||
|
{`${price} ${product.price?.currencyCode}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={s.imageContainer}>
|
||||||
|
{product?.images && (
|
||||||
|
<Image
|
||||||
|
alt={product.name || 'Product Image'}
|
||||||
|
className={s.productImage}
|
||||||
|
src={product.images[0].url || placeholderImg}
|
||||||
|
height={540}
|
||||||
|
width={540}
|
||||||
|
quality="85"
|
||||||
|
layout="responsive"
|
||||||
|
{...imgProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{variant === 'default' && (
|
||||||
|
<>
|
||||||
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||||
<WishlistButton
|
<WishlistButton
|
||||||
className={s.wishlistButton}
|
className={s.wishlistButton}
|
||||||
@ -64,25 +104,29 @@ const ProductCard: FC<Props> = ({
|
|||||||
variant={product.variants[0] as any}
|
variant={product.variants[0] as any}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
<ProductTag
|
||||||
<div className={s.imageContainer}>
|
name={product.name}
|
||||||
{product?.images && (
|
price={`${price} ${product.price?.currencyCode}`}
|
||||||
<Image
|
/>
|
||||||
alt={product.name || 'Product Image'}
|
<div className={s.imageContainer}>
|
||||||
className={s.productImage}
|
{product?.images && (
|
||||||
src={product.images[0]?.url || placeholderImg}
|
<Image
|
||||||
height={540}
|
alt={product.name || 'Product Image'}
|
||||||
width={540}
|
className={s.productImage}
|
||||||
quality="85"
|
src={product.images[0]?.url || placeholderImg}
|
||||||
layout="responsive"
|
height={540}
|
||||||
{...imgProps}
|
width={540}
|
||||||
/>
|
quality="85"
|
||||||
)}
|
layout="responsive"
|
||||||
</div>
|
{...imgProps}
|
||||||
</>
|
/>
|
||||||
)}
|
)}
|
||||||
</a>
|
</div>
|
||||||
</Link>
|
</>
|
||||||
)
|
)}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default ProductCard
|
export default ProductCard
|
||||||
|
50
components/product/ProductOptions/ProductOptions.tsx
Normal file
50
components/product/ProductOptions/ProductOptions.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Swatch } from '@components/product'
|
||||||
|
import type { ProductOption } from '@commerce/types/product'
|
||||||
|
import { SelectedOptions } from '../helpers'
|
||||||
|
import React from 'react'
|
||||||
|
interface ProductOptionsProps {
|
||||||
|
options: ProductOption[]
|
||||||
|
selectedOptions: SelectedOptions
|
||||||
|
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductOptions: React.FC<ProductOptionsProps> = React.memo(
|
||||||
|
({ options, selectedOptions, setSelectedOptions }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<div className="pb-4" key={opt.displayName}>
|
||||||
|
<h2 className="uppercase font-medium text-sm tracking-wide">
|
||||||
|
{opt.displayName}
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-row py-4">
|
||||||
|
{opt.values.map((v, i: number) => {
|
||||||
|
const active = selectedOptions[opt.displayName.toLowerCase()]
|
||||||
|
return (
|
||||||
|
<Swatch
|
||||||
|
key={`${opt.id}-${i}`}
|
||||||
|
active={v.label.toLowerCase() === active}
|
||||||
|
variant={opt.displayName}
|
||||||
|
color={v.hexColors ? v.hexColors[0] : ''}
|
||||||
|
label={v.label}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedOptions((selectedOptions) => {
|
||||||
|
return {
|
||||||
|
...selectedOptions,
|
||||||
|
[opt.displayName.toLowerCase()]:
|
||||||
|
v.label.toLowerCase(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ProductOptions
|
1
components/product/ProductOptions/index.ts
Normal file
1
components/product/ProductOptions/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ProductOptions'
|
84
components/product/ProductSidebar/ProductSidebar.module.css
Normal file
84
components/product/ProductSidebar/ProductSidebar.module.css
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
.root {
|
||||||
|
@apply relative grid items-start gap-1 grid-cols-1 overflow-x-hidden;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
@apply relative px-0 pb-0 box-border flex flex-col col-span-1;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
@apply transition-colors ease-in-out duration-500
|
||||||
|
absolute top-0 left-0 z-20 pr-16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .name {
|
||||||
|
@apply pt-0 max-w-full w-full leading-extra-loose;
|
||||||
|
font-size: 2rem;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .name span {
|
||||||
|
@apply py-4 px-6 bg-primary text-primary font-bold;
|
||||||
|
font-size: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
box-decoration-break: clone;
|
||||||
|
-webkit-box-decoration-break: clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .price {
|
||||||
|
@apply pt-2 px-6 pb-4 text-sm bg-primary text-accent-9
|
||||||
|
font-semibold inline-block tracking-wide;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
@apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 py-6 w-full h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliderContainer {
|
||||||
|
@apply flex items-center justify-center overflow-x-hidden bg-violet;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
@apply text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer > div,
|
||||||
|
.imageContainer > div > div {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliderContainer .img {
|
||||||
|
@apply w-full h-auto max-h-full object-cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wishlistButton {
|
||||||
|
@apply absolute z-30 top-0 right-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relatedProductsGrid {
|
||||||
|
@apply grid grid-cols-2 py-2 gap-2 md:grid-cols-4 md:gap-7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen lg {
|
||||||
|
.root {
|
||||||
|
@apply grid-cols-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
@apply mx-0 col-span-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
@apply col-span-4 py-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
max-height: 600px;
|
||||||
|
}
|
||||||
|
}
|
87
components/product/ProductSidebar/ProductSidebar.tsx
Normal file
87
components/product/ProductSidebar/ProductSidebar.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import s from './ProductSidebar.module.css'
|
||||||
|
import { useAddItem } from '@framework/cart'
|
||||||
|
import { FC, useEffect, useState } from 'react'
|
||||||
|
import { ProductOptions } from '@components/product'
|
||||||
|
import type { Product } from '@commerce/types/product'
|
||||||
|
import { Button, Text, Rating, Collapse, useUI } from '@components/ui'
|
||||||
|
import {
|
||||||
|
getProductVariant,
|
||||||
|
selectDefaultOptionFromProduct,
|
||||||
|
SelectedOptions,
|
||||||
|
} from '../helpers'
|
||||||
|
|
||||||
|
interface ProductSidebarProps {
|
||||||
|
product: Product
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
||||||
|
const addItem = useAddItem()
|
||||||
|
const { openSidebar } = useUI()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [selectedOptions, setSelectedOptions] = useState<SelectedOptions>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectDefaultOptionFromProduct(product, setSelectedOptions)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const variant = getProductVariant(product, selectedOptions)
|
||||||
|
const addToCart = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
await addItem({
|
||||||
|
productId: String(product.id),
|
||||||
|
variantId: String(variant ? variant.id : product.variants[0].id),
|
||||||
|
})
|
||||||
|
openSidebar()
|
||||||
|
setLoading(false)
|
||||||
|
} catch (err) {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<ProductOptions
|
||||||
|
options={product.options}
|
||||||
|
selectedOptions={selectedOptions}
|
||||||
|
setSelectedOptions={setSelectedOptions}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
className="pb-4 break-words w-full max-w-xl"
|
||||||
|
html={product.descriptionHtml || product.description}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row justify-between items-center">
|
||||||
|
<Rating value={4} />
|
||||||
|
<div className="text-accent-6 pr-1 font-medium text-sm">36 reviews</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
aria-label="Add to Cart"
|
||||||
|
type="button"
|
||||||
|
className={s.button}
|
||||||
|
onClick={addToCart}
|
||||||
|
loading={loading}
|
||||||
|
disabled={variant?.availableForSale === false}
|
||||||
|
>
|
||||||
|
{variant?.availableForSale === false
|
||||||
|
? 'Not Available'
|
||||||
|
: 'Add To Cart'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Collapse title="Care">
|
||||||
|
This is a limited edition production run. Printing starts when the
|
||||||
|
drop ends.
|
||||||
|
</Collapse>
|
||||||
|
<Collapse title="Details">
|
||||||
|
This is a limited edition production run. Printing starts when the
|
||||||
|
drop ends. Reminder: Bad Boys For Life. Shipping may take 10+ days due
|
||||||
|
to COVID-19.
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductSidebar
|
1
components/product/ProductSidebar/index.ts
Normal file
1
components/product/ProductSidebar/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ProductSidebar'
|
@ -1,82 +1,64 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply relative w-full h-full;
|
@apply relative w-full h-full select-none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
@apply relative h-full transition-opacity duration-150;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
@apply transition-transform transition-colors
|
||||||
|
ease-linear duration-75 overflow-hidden inline-block
|
||||||
|
cursor-pointer h-full;
|
||||||
|
width: 125px;
|
||||||
|
width: calc(100% / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb.selected {
|
||||||
|
@apply bg-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb img {
|
||||||
|
height: 85% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
@apply bg-violet-dark;
|
||||||
|
box-sizing: content-box;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
height: 125px;
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leftControl,
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
.rightControl {
|
.album::-webkit-scrollbar {
|
||||||
@apply absolute top-1/2 -translate-x-1/2 z-20 w-16 h-16 flex items-center justify-center bg-hover-1 rounded-full;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leftControl:hover,
|
@screen md {
|
||||||
.rightControl:hover {
|
.thumb:hover {
|
||||||
@apply bg-hover-2;
|
transform: scale(1.02);
|
||||||
}
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.leftControl:hover,
|
.thumb.selected {
|
||||||
.rightControl:hover {
|
@apply bg-white;
|
||||||
@apply outline-none shadow-outline-normal;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.leftControl {
|
.album {
|
||||||
@apply bg-cover left-10;
|
height: 182px;
|
||||||
background-image: url('public/cursor-left.png');
|
}
|
||||||
|
.thumb {
|
||||||
@screen md {
|
width: 235px;
|
||||||
@apply left-6;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rightControl {
|
|
||||||
@apply bg-cover right-10;
|
|
||||||
background-image: url('public/cursor-right.png');
|
|
||||||
|
|
||||||
@screen md {
|
|
||||||
@apply right-6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.control {
|
|
||||||
@apply opacity-0 transition duration-150;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root:hover .control {
|
|
||||||
@apply opacity-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.positionIndicatorsContainer {
|
|
||||||
@apply hidden;
|
|
||||||
|
|
||||||
@screen sm {
|
|
||||||
@apply block absolute bottom-6 left-1/2;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.positionIndicator {
|
|
||||||
@apply rounded-full p-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
@apply bg-hover-1 transition w-3 h-3 rounded-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.positionIndicator:hover .dot {
|
|
||||||
@apply bg-hover-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.positionIndicator:focus {
|
|
||||||
@apply outline-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.positionIndicator:focus .dot {
|
|
||||||
@apply shadow-outline-normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.positionIndicatorActive .dot {
|
|
||||||
@apply bg-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.positionIndicatorActive:hover .dot {
|
|
||||||
@apply bg-white;
|
|
||||||
}
|
|
||||||
|
@ -8,20 +8,42 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
import { a } from '@react-spring/web'
|
||||||
import s from './ProductSlider.module.css'
|
import s from './ProductSlider.module.css'
|
||||||
|
import ProductSliderControl from '../ProductSliderControl'
|
||||||
|
|
||||||
const ProductSlider: FC = ({ children }) => {
|
interface ProductSliderProps {
|
||||||
|
children: React.ReactNode[]
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductSlider: React.FC<ProductSliderProps> = ({
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
const [currentSlide, setCurrentSlide] = useState(0)
|
const [currentSlide, setCurrentSlide] = useState(0)
|
||||||
const [isMounted, setIsMounted] = useState(false)
|
const [isMounted, setIsMounted] = useState(false)
|
||||||
const sliderContainerRef = useRef<HTMLDivElement>(null)
|
const sliderContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const thumbsContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const [ref, slider] = useKeenSlider<HTMLDivElement>({
|
const [ref, slider] = useKeenSlider<HTMLDivElement>({
|
||||||
loop: true,
|
loop: true,
|
||||||
slidesPerView: 1,
|
slidesPerView: 1,
|
||||||
mounted: () => setIsMounted(true),
|
mounted: () => setIsMounted(true),
|
||||||
slideChanged(s) {
|
slideChanged(s) {
|
||||||
setCurrentSlide(s.details().relativeSlide)
|
const slideNumber = s.details().relativeSlide
|
||||||
|
setCurrentSlide(slideNumber)
|
||||||
|
|
||||||
|
if (thumbsContainerRef.current) {
|
||||||
|
const $el = document.getElementById(
|
||||||
|
`thumb-${s.details().relativeSlide}`
|
||||||
|
)
|
||||||
|
if (slideNumber >= 3) {
|
||||||
|
thumbsContainerRef.current.scrollLeft = $el!.offsetLeft
|
||||||
|
} else {
|
||||||
|
thumbsContainerRef.current.scrollLeft = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -59,23 +81,16 @@ const ProductSlider: FC = ({ children }) => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const onPrev = React.useCallback(() => slider.prev(), [slider])
|
||||||
|
const onNext = React.useCallback(() => slider.next(), [slider])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={s.root} ref={sliderContainerRef}>
|
<div className={cn(s.root, className)} ref={sliderContainerRef}>
|
||||||
<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
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="keen-slider h-full transition-opacity duration-150"
|
className={cn(s.slider, { [s.show]: isMounted }, 'keen-slider')}
|
||||||
style={{ opacity: isMounted ? 1 : 0 }}
|
|
||||||
>
|
>
|
||||||
|
{slider && <ProductSliderControl onPrev={onPrev} onNext={onNext} />}
|
||||||
{Children.map(children, (child) => {
|
{Children.map(children, (child) => {
|
||||||
// Add the keen-slider__slide className to children
|
// Add the keen-slider__slide className to children
|
||||||
if (isValidElement(child)) {
|
if (isValidElement(child)) {
|
||||||
@ -92,26 +107,28 @@ const ProductSlider: FC = ({ children }) => {
|
|||||||
return child
|
return child
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{slider && (
|
|
||||||
<div className={cn(s.positionIndicatorsContainer)}>
|
<a.div className={s.album} ref={thumbsContainerRef}>
|
||||||
{[...Array(slider.details().size).keys()].map((idx) => {
|
{slider &&
|
||||||
return (
|
Children.map(children, (child, idx) => {
|
||||||
<button
|
if (isValidElement(child)) {
|
||||||
aria-label="Position indicator"
|
return {
|
||||||
key={idx}
|
...child,
|
||||||
className={cn(s.positionIndicator, {
|
props: {
|
||||||
[s.positionIndicatorActive]: currentSlide === idx,
|
...child.props,
|
||||||
})}
|
className: cn(child.props.className, s.thumb, {
|
||||||
onClick={() => {
|
[s.selected]: currentSlide === idx,
|
||||||
slider.moveToSlideRelative(idx)
|
}),
|
||||||
}}
|
id: `thumb-${idx}`,
|
||||||
>
|
onClick: () => {
|
||||||
<div className={s.dot} />
|
slider.moveToSlideRelative(idx)
|
||||||
</button>
|
},
|
||||||
)
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return child
|
||||||
})}
|
})}
|
||||||
</div>
|
</a.div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
.control {
|
||||||
|
@apply bg-violet absolute bottom-10 right-10 flex flex-row
|
||||||
|
border-accent-0 border text-accent-0 z-30 shadow-xl select-none;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftControl,
|
||||||
|
.rightControl {
|
||||||
|
@apply px-9 cursor-pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftControl:hover,
|
||||||
|
.rightControl:hover {
|
||||||
|
background-color: var(--violet-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftControl:focus,
|
||||||
|
.rightControl:focus {
|
||||||
|
@apply outline-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightControl {
|
||||||
|
@apply border-l border-accent-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftControl {
|
||||||
|
margin-right: -1px;
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import cn from 'classnames'
|
||||||
|
import React from 'react'
|
||||||
|
import s from './ProductSliderControl.module.css'
|
||||||
|
import { ArrowLeft, ArrowRight } from '@components/icons'
|
||||||
|
|
||||||
|
interface ProductSliderControl {
|
||||||
|
onPrev: React.MouseEventHandler<HTMLButtonElement>
|
||||||
|
onNext: React.MouseEventHandler<HTMLButtonElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductSliderControl: React.FC<ProductSliderControl> = React.memo(
|
||||||
|
({ onPrev, onNext }) => (
|
||||||
|
<div className={s.control}>
|
||||||
|
<button
|
||||||
|
className={cn(s.leftControl)}
|
||||||
|
onClick={onPrev}
|
||||||
|
aria-label="Previous Product Image"
|
||||||
|
>
|
||||||
|
<ArrowLeft />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={cn(s.rightControl)}
|
||||||
|
onClick={onNext}
|
||||||
|
aria-label="Next Product Image"
|
||||||
|
>
|
||||||
|
<ArrowRight />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
export default ProductSliderControl
|
1
components/product/ProductSliderControl/index.ts
Normal file
1
components/product/ProductSliderControl/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ProductSliderControl'
|
30
components/product/ProductTag/ProductTag.module.css
Normal file
30
components/product/ProductTag/ProductTag.module.css
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.root {
|
||||||
|
@apply transition-colors ease-in-out duration-500
|
||||||
|
absolute top-0 left-0 z-20 pr-16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root .name {
|
||||||
|
@apply pt-0 max-w-full w-full leading-extra-loose;
|
||||||
|
font-size: 2rem;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
line-height: 2.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root .name span {
|
||||||
|
@apply py-4 px-6 bg-primary text-primary font-bold;
|
||||||
|
min-height: 70px;
|
||||||
|
font-size: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
box-decoration-break: clone;
|
||||||
|
-webkit-box-decoration-break: clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root .name span.fontsizing {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root .price {
|
||||||
|
@apply pt-2 px-6 pb-4 text-sm bg-primary text-accent-9
|
||||||
|
font-semibold inline-block tracking-wide;
|
||||||
|
}
|
36
components/product/ProductTag/ProductTag.tsx
Normal file
36
components/product/ProductTag/ProductTag.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import cn from 'classnames'
|
||||||
|
import { inherits } from 'util'
|
||||||
|
import s from './ProductTag.module.css'
|
||||||
|
|
||||||
|
interface ProductTagProps {
|
||||||
|
className?: string
|
||||||
|
name: string
|
||||||
|
price: string
|
||||||
|
fontSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductTag: React.FC<ProductTagProps> = ({
|
||||||
|
name,
|
||||||
|
price,
|
||||||
|
className = '',
|
||||||
|
fontSize = 32,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={cn(s.root, className)}>
|
||||||
|
<h3 className={s.name}>
|
||||||
|
<span
|
||||||
|
className={cn({ [s.fontsizing]: fontSize < 32 })}
|
||||||
|
style={{
|
||||||
|
fontSize: `${fontSize}px`,
|
||||||
|
lineHeight: `${fontSize}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<div className={s.price}>{price}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductTag
|
1
components/product/ProductTag/index.ts
Normal file
1
components/product/ProductTag/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ProductTag'
|
@ -1,96 +1,60 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply relative grid items-start gap-8 grid-cols-1 overflow-x-hidden;
|
@apply relative grid items-start gap-1 grid-cols-1 overflow-x-hidden;
|
||||||
|
min-height: auto;
|
||||||
@screen lg {
|
|
||||||
@apply grid-cols-12;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.productDisplay {
|
.main {
|
||||||
@apply relative flex px-0 pb-0 box-border col-span-1 bg-violet;
|
@apply relative px-0 pb-0 box-border flex flex-col col-span-1;
|
||||||
min-height: 600px;
|
min-height: 500px;
|
||||||
|
|
||||||
@screen md {
|
|
||||||
min-height: 700px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@screen lg {
|
|
||||||
margin-right: -2rem;
|
|
||||||
margin-left: -2rem;
|
|
||||||
@apply mx-0 col-span-6;
|
|
||||||
min-height: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.squareBg {
|
|
||||||
@apply absolute inset-0 bg-violet z-0 h-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nameBox {
|
|
||||||
@apply absolute top-6 left-0 z-20 pr-16;
|
|
||||||
|
|
||||||
@screen lg {
|
|
||||||
@apply left-6 pr-16;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .name {
|
|
||||||
@apply px-6 py-2 bg-primary text-primary font-bold;
|
|
||||||
font-size: 2rem;
|
|
||||||
letter-spacing: 0.4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .price {
|
|
||||||
@apply px-6 py-2 pb-4 bg-primary text-primary font-bold inline-block tracking-wide;
|
|
||||||
}
|
|
||||||
|
|
||||||
@screen lg {
|
|
||||||
& .name,
|
|
||||||
& .price {
|
|
||||||
@apply bg-violet-light text-white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
@apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 w-full h-full;
|
@apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 py-6 w-full h-full;
|
||||||
|
|
||||||
@screen lg {
|
|
||||||
@apply col-span-6 py-24 justify-between;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sliderContainer {
|
.sliderContainer {
|
||||||
@apply absolute z-10 inset-0 flex items-center justify-center overflow-x-hidden;
|
@apply flex items-center justify-center overflow-x-hidden bg-violet;
|
||||||
}
|
}
|
||||||
|
|
||||||
.imageContainer {
|
.imageContainer {
|
||||||
& > div {
|
@apply text-center;
|
||||||
@apply h-full;
|
|
||||||
& > div {
|
|
||||||
@apply h-full;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.img {
|
.imageContainer > div,
|
||||||
|
.imageContainer > div > div {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliderContainer .img {
|
||||||
@apply w-full h-auto max-h-full object-cover;
|
@apply w-full h-auto max-h-full object-cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 300px;
|
|
||||||
|
|
||||||
@screen sm {
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wishlistButton {
|
.wishlistButton {
|
||||||
@apply absolute z-30 top-6 right-0 bg-primary text-primary w-10 h-10 flex items-center justify-center font-semibold leading-6 cursor-pointer;
|
@apply absolute z-30 top-0 right-0;
|
||||||
|
}
|
||||||
|
|
||||||
@screen lg {
|
.relatedProductsGrid {
|
||||||
@apply right-12 text-white bg-violet;
|
@apply grid grid-cols-2 py-2 gap-2 md:grid-cols-4 md:gap-7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen lg {
|
||||||
|
.root {
|
||||||
|
@apply grid-cols-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
@apply mx-0 col-span-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
@apply col-span-4 py-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
max-height: 600px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,64 +1,90 @@
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
import { FC, useEffect, useState } from 'react'
|
|
||||||
import s from './ProductView.module.css'
|
import s from './ProductView.module.css'
|
||||||
import { Swatch, ProductSlider } from '@components/product'
|
import { FC } from 'react'
|
||||||
import { Button, Container, Text, useUI } from '@components/ui'
|
|
||||||
import type { Product } from '@commerce/types/product'
|
import type { Product } from '@commerce/types/product'
|
||||||
import usePrice from '@framework/product/use-price'
|
import usePrice from '@framework/product/use-price'
|
||||||
import { useAddItem } from '@framework/cart'
|
import { WishlistButton } from '@components/wishlist'
|
||||||
import { getVariant, SelectedOptions } from '../helpers'
|
import { ProductSlider, ProductCard } from '@components/product'
|
||||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
import { Container, Text } from '@components/ui'
|
||||||
|
import ProductSidebar from '../ProductSidebar'
|
||||||
interface Props {
|
import ProductTag from '../ProductTag'
|
||||||
children?: any
|
interface ProductViewProps {
|
||||||
product: Product
|
product: Product
|
||||||
className?: string
|
relatedProducts: Product[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductView: FC<Props> = ({ product }) => {
|
const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
|
||||||
// TODO: fix this missing argument issue
|
|
||||||
/* @ts-ignore */
|
|
||||||
const addItem = useAddItem()
|
|
||||||
const { price } = usePrice({
|
const { price } = usePrice({
|
||||||
amount: product.price.value,
|
amount: product.price.value,
|
||||||
baseAmount: product.price.retailPrice,
|
baseAmount: product.price.retailPrice,
|
||||||
currencyCode: product.price.currencyCode!,
|
currencyCode: product.price.currencyCode!,
|
||||||
})
|
})
|
||||||
const { openSidebar } = useUI()
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [choices, setChoices] = useState<SelectedOptions>({})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Selects the default option
|
|
||||||
const options = product.variants[0].options || []
|
|
||||||
options.forEach((v) => {
|
|
||||||
setChoices((choices) => ({
|
|
||||||
...choices,
|
|
||||||
[v.displayName.toLowerCase()]: v.values[0]?.label.toLowerCase(),
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const variant = getVariant(product, choices)
|
|
||||||
|
|
||||||
const addToCart = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
await addItem({
|
|
||||||
productId: String(product.id),
|
|
||||||
variantId: String(variant ? variant.id : product.variants[0].id),
|
|
||||||
})
|
|
||||||
openSidebar()
|
|
||||||
setLoading(false)
|
|
||||||
} catch (err) {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="max-w-none w-full" clean>
|
<>
|
||||||
|
<Container className="max-w-none w-full" clean>
|
||||||
|
<div className={cn(s.root, 'fit')}>
|
||||||
|
<div className={cn(s.main, 'fit')}>
|
||||||
|
<ProductTag
|
||||||
|
name={product.name}
|
||||||
|
price={`${price} ${product.price?.currencyCode}`}
|
||||||
|
fontSize={32}
|
||||||
|
/>
|
||||||
|
<div className={s.sliderContainer}>
|
||||||
|
<ProductSlider key={product.id}>
|
||||||
|
{product.images.map((image, i) => (
|
||||||
|
<div key={image.url} className={s.imageContainer}>
|
||||||
|
<Image
|
||||||
|
className={s.img}
|
||||||
|
src={image.url!}
|
||||||
|
alt={image.alt || 'Product Image'}
|
||||||
|
width={600}
|
||||||
|
height={600}
|
||||||
|
priority={i === 0}
|
||||||
|
quality="85"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ProductSlider>
|
||||||
|
</div>
|
||||||
|
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||||
|
<WishlistButton
|
||||||
|
className={s.wishlistButton}
|
||||||
|
productId={product.id}
|
||||||
|
variant={product.variants[0]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProductSidebar product={product} className={s.sidebar} />
|
||||||
|
</div>
|
||||||
|
<hr className="mt-7 border-accent-2" />
|
||||||
|
<section className="py-12 px-6 mb-10">
|
||||||
|
<Text variant="sectionHeading">Related Products</Text>
|
||||||
|
<div className={s.relatedProductsGrid}>
|
||||||
|
{relatedProducts.map((p) => (
|
||||||
|
<div
|
||||||
|
key={p.path}
|
||||||
|
className="animated fadeIn bg-accent-0 border border-accent-2"
|
||||||
|
>
|
||||||
|
<ProductCard
|
||||||
|
noNameTag
|
||||||
|
product={p}
|
||||||
|
key={p.path}
|
||||||
|
variant="simple"
|
||||||
|
className="animated fadeIn"
|
||||||
|
imgProps={{
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Container>
|
||||||
<NextSeo
|
<NextSeo
|
||||||
title={product.name}
|
title={product.name}
|
||||||
description={product.description}
|
description={product.description}
|
||||||
@ -76,97 +102,7 @@ const ProductView: FC<Props> = ({ product }) => {
|
|||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className={cn(s.root, 'fit')}>
|
</>
|
||||||
<div className={cn(s.productDisplay, 'fit')}>
|
|
||||||
<div className={s.nameBox}>
|
|
||||||
<h1 className={s.name}>{product.name}</h1>
|
|
||||||
<div className={s.price}>
|
|
||||||
{price}
|
|
||||||
{` `}
|
|
||||||
{product.price?.currencyCode}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={s.sliderContainer}>
|
|
||||||
<ProductSlider key={product.id}>
|
|
||||||
{product.images.map((image, i) => (
|
|
||||||
<div key={image.url} className={s.imageContainer}>
|
|
||||||
<Image
|
|
||||||
className={s.img}
|
|
||||||
src={image.url!}
|
|
||||||
alt={image.alt || 'Product Image'}
|
|
||||||
width={1050}
|
|
||||||
height={1050}
|
|
||||||
priority={i === 0}
|
|
||||||
quality="85"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</ProductSlider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={s.sidebar}>
|
|
||||||
<section>
|
|
||||||
{product.options?.map((opt) => (
|
|
||||||
<div className="pb-4" key={opt.displayName}>
|
|
||||||
<h2 className="uppercase font-medium">{opt.displayName}</h2>
|
|
||||||
<div className="flex flex-row py-4">
|
|
||||||
{opt.values.map((v, i: number) => {
|
|
||||||
const active = (choices as any)[
|
|
||||||
opt.displayName.toLowerCase()
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Swatch
|
|
||||||
key={`${opt.id}-${i}`}
|
|
||||||
active={v.label.toLowerCase() === active}
|
|
||||||
variant={opt.displayName}
|
|
||||||
color={v.hexColors ? v.hexColors[0] : ''}
|
|
||||||
label={v.label}
|
|
||||||
onClick={() => {
|
|
||||||
setChoices((choices) => {
|
|
||||||
return {
|
|
||||||
...choices,
|
|
||||||
[opt.displayName.toLowerCase()]:
|
|
||||||
v.label.toLowerCase(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="pb-14 break-words w-full max-w-xl">
|
|
||||||
<Text html={product.descriptionHtml || product.description} />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
aria-label="Add to Cart"
|
|
||||||
type="button"
|
|
||||||
className={s.button}
|
|
||||||
onClick={addToCart}
|
|
||||||
loading={loading}
|
|
||||||
disabled={variant?.availableForSale === false}
|
|
||||||
>
|
|
||||||
{variant?.availableForSale === false
|
|
||||||
? 'Not Available'
|
|
||||||
: 'Add To Cart'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
|
||||||
<WishlistButton
|
|
||||||
className={s.wishlistButton}
|
|
||||||
productId={product.id}
|
|
||||||
variant={product.variants[0]! as any}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
.swatch {
|
.swatch {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
composes: root from 'components/ui/Button/Button.module.css';
|
composes: root from '@components/ui/Button/Button.module.css';
|
||||||
@apply h-12 w-12 bg-primary text-primary rounded-full mr-3 inline-flex
|
@apply h-10 w-10 bg-primary text-primary rounded-full mr-3 inline-flex
|
||||||
items-center justify-center cursor-pointer transition duration-150 ease-in-out
|
items-center justify-center cursor-pointer transition duration-150 ease-in-out
|
||||||
p-0 shadow-none border-gray-200 border box-border;
|
p-0 shadow-none border-accent-3 border box-border select-none;
|
||||||
margin-right: calc(0.75rem - 1px);
|
margin-right: calc(0.75rem - 1px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.swatch::before,
|
.swatch::before,
|
||||||
@ -35,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
@apply border-accents-9 border-2;
|
@apply border-accent-9 border-2;
|
||||||
padding-right: 1px;
|
padding-right: 1px;
|
||||||
padding-left: 1px;
|
padding-left: 1px;
|
||||||
}
|
}
|
||||||
@ -46,7 +48,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.active.textLabel {
|
.active.textLabel {
|
||||||
@apply border-accents-9 border-2;
|
@apply border-accent-9 border-2;
|
||||||
padding-right: calc(1rem - 1px);
|
padding-right: calc(1rem - 1px);
|
||||||
padding-left: calc(1rem - 1px);
|
padding-left: calc(1rem - 1px);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { FC } from 'react'
|
import React from 'react'
|
||||||
import s from './Swatch.module.css'
|
import s from './Swatch.module.css'
|
||||||
import { Check } from '@components/icons'
|
import { Check } from '@components/icons'
|
||||||
import Button, { ButtonProps } from '@components/ui/Button'
|
import Button, { ButtonProps } from '@components/ui/Button'
|
||||||
@ -13,48 +13,50 @@ interface SwatchProps {
|
|||||||
label?: string | null
|
label?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const Swatch: FC<Omit<ButtonProps, 'variant'> & SwatchProps> = ({
|
const Swatch: React.FC<Omit<ButtonProps, 'variant'> & SwatchProps> = React.memo(
|
||||||
className,
|
({
|
||||||
color = '',
|
active,
|
||||||
label = null,
|
className,
|
||||||
variant = 'size',
|
color = '',
|
||||||
active,
|
label = null,
|
||||||
...props
|
variant = 'size',
|
||||||
}) => {
|
...props
|
||||||
variant = variant?.toLowerCase()
|
}) => {
|
||||||
|
variant = variant?.toLowerCase()
|
||||||
|
|
||||||
if (label) {
|
if (label) {
|
||||||
label = label?.toLowerCase()
|
label = label?.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
const swatchClassName = cn(
|
||||||
|
s.swatch,
|
||||||
|
{
|
||||||
|
[s.color]: color,
|
||||||
|
[s.active]: active,
|
||||||
|
[s.size]: variant === 'size',
|
||||||
|
[s.dark]: color ? isDark(color) : false,
|
||||||
|
[s.textLabel]: !color && label && label.length > 3,
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
aria-label="Variant Swatch"
|
||||||
|
className={swatchClassName}
|
||||||
|
{...(label && color && { title: label })}
|
||||||
|
style={color ? { backgroundColor: color } : {}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{color && active && (
|
||||||
|
<span>
|
||||||
|
<Check />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!color ? label : null}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
const swatchClassName = cn(
|
|
||||||
s.swatch,
|
|
||||||
{
|
|
||||||
[s.active]: active,
|
|
||||||
[s.size]: variant === 'size',
|
|
||||||
[s.color]: color,
|
|
||||||
[s.dark]: color ? isDark(color) : false,
|
|
||||||
[s.textLabel]: !color && label && label.length > 3,
|
|
||||||
},
|
|
||||||
className
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={swatchClassName}
|
|
||||||
style={color ? { backgroundColor: color } : {}}
|
|
||||||
aria-label="Variant Swatch"
|
|
||||||
{...(label && color && { title: label })}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{color && active && (
|
|
||||||
<span>
|
|
||||||
<Check />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{!color ? label : null}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Swatch
|
export default Swatch
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import type { Product } from '@commerce/types/product'
|
import type { Product } from '@commerce/types/product'
|
||||||
export type SelectedOptions = Record<string, string | null>
|
export type SelectedOptions = Record<string, string | null>
|
||||||
|
import { Dispatch, SetStateAction } from 'react'
|
||||||
|
|
||||||
export function getVariant(product: Product, opts: SelectedOptions) {
|
export function getProductVariant(product: Product, opts: SelectedOptions) {
|
||||||
const variant = product.variants.find((variant) => {
|
const variant = product.variants.find((variant) => {
|
||||||
return Object.entries(opts).every(([key, value]) =>
|
return Object.entries(opts).every(([key, value]) =>
|
||||||
variant.options.find((option) => {
|
variant.options.find((option) => {
|
||||||
@ -16,3 +17,16 @@ export function getVariant(product: Product, opts: SelectedOptions) {
|
|||||||
})
|
})
|
||||||
return variant
|
return variant
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectDefaultOptionFromProduct(
|
||||||
|
product: Product,
|
||||||
|
updater: Dispatch<SetStateAction<SelectedOptions>>
|
||||||
|
) {
|
||||||
|
// Selects the default option
|
||||||
|
product.variants[0].options?.forEach((v) => {
|
||||||
|
updater((choices) => ({
|
||||||
|
...choices,
|
||||||
|
[v.displayName.toLowerCase()]: v.values[0].label.toLowerCase(),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -2,3 +2,4 @@ export { default as Swatch } from './Swatch'
|
|||||||
export { default as ProductView } from './ProductView'
|
export { default as ProductView } from './ProductView'
|
||||||
export { default as ProductCard } from './ProductCard'
|
export { default as ProductCard } from './ProductCard'
|
||||||
export { default as ProductSlider } from './ProductSlider'
|
export { default as ProductSlider } from './ProductSlider'
|
||||||
|
export { default as ProductOptions } from './ProductOptions'
|
||||||
|
439
components/search.tsx
Normal file
439
components/search.tsx
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
import cn from 'classnames'
|
||||||
|
import type { SearchPropsType } from '@lib/search-props'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
import { Layout } from '@components/common'
|
||||||
|
import { ProductCard } from '@components/product'
|
||||||
|
import type { Product } from '@commerce/types/product'
|
||||||
|
import { Container, Grid, Skeleton } from '@components/ui'
|
||||||
|
|
||||||
|
import useSearch from '@framework/product/use-search'
|
||||||
|
|
||||||
|
import getSlug from '@lib/get-slug'
|
||||||
|
import rangeMap from '@lib/range-map'
|
||||||
|
|
||||||
|
const SORT = Object.entries({
|
||||||
|
'trending-desc': 'Trending',
|
||||||
|
'latest-desc': 'Latest arrivals',
|
||||||
|
'price-asc': 'Price: Low to high',
|
||||||
|
'price-desc': 'Price: High to low',
|
||||||
|
})
|
||||||
|
|
||||||
|
import {
|
||||||
|
filterQuery,
|
||||||
|
getCategoryPath,
|
||||||
|
getDesignerPath,
|
||||||
|
useSearchMeta,
|
||||||
|
} from '@lib/search'
|
||||||
|
|
||||||
|
export default function Search({ categories, brands }: SearchPropsType) {
|
||||||
|
const [activeFilter, setActiveFilter] = useState('')
|
||||||
|
const [toggleFilter, setToggleFilter] = useState(false)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { asPath, locale } = router
|
||||||
|
const { q, sort } = router.query
|
||||||
|
// `q` can be included but because categories and designers can't be searched
|
||||||
|
// in the same way of products, it's better to ignore the search input if one
|
||||||
|
// of those is selected
|
||||||
|
const query = filterQuery({ sort })
|
||||||
|
|
||||||
|
const { pathname, category, brand } = useSearchMeta(asPath)
|
||||||
|
const activeCategory = categories.find((cat: any) => cat.slug === category)
|
||||||
|
const activeBrand = brands.find(
|
||||||
|
(b: any) => getSlug(b.node.path) === `brands/${brand}`
|
||||||
|
)?.node
|
||||||
|
|
||||||
|
const { data } = useSearch({
|
||||||
|
search: typeof q === 'string' ? q : '',
|
||||||
|
categoryId: activeCategory?.id,
|
||||||
|
brandId: (activeBrand as any)?.entityId,
|
||||||
|
sort: typeof sort === 'string' ? sort : '',
|
||||||
|
locale,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClick = (event: any, filter: string) => {
|
||||||
|
if (filter !== activeFilter) {
|
||||||
|
setToggleFilter(true)
|
||||||
|
} else {
|
||||||
|
setToggleFilter(!toggleFilter)
|
||||||
|
}
|
||||||
|
setActiveFilter(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 mt-3 mb-20">
|
||||||
|
<div className="col-span-8 lg:col-span-2 order-1 lg:order-none">
|
||||||
|
{/* Categories */}
|
||||||
|
<div className="relative inline-block w-full">
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<span className="rounded-md shadow-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => handleClick(e, 'categories')}
|
||||||
|
className="flex justify-between w-full rounded-sm border border-accent-3 px-4 py-3 bg-accent-0 text-sm leading-5 font-medium text-accent-4 hover:text-accent-5 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-accent-1 active:text-accent-8 transition ease-in-out duration-150"
|
||||||
|
id="options-menu"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="true"
|
||||||
|
>
|
||||||
|
{activeCategory?.name
|
||||||
|
? `Category: ${activeCategory?.name}`
|
||||||
|
: 'All Categories'}
|
||||||
|
<svg
|
||||||
|
className="-mr-1 ml-2 h-5 w-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`origin-top-left absolute lg:relative left-0 mt-2 w-full rounded-md shadow-lg lg:shadow-none z-10 mb-10 lg:block ${
|
||||||
|
activeFilter !== 'categories' || toggleFilter !== true
|
||||||
|
? 'hidden'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="rounded-sm bg-accent-0 shadow-xs lg:bg-none lg:shadow-none">
|
||||||
|
<div
|
||||||
|
role="menu"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-labelledby="options-menu"
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li
|
||||||
|
className={cn(
|
||||||
|
'block text-sm leading-5 text-accent-4 lg:text-base lg:no-underline lg:font-bold lg:tracking-wide hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
|
||||||
|
{
|
||||||
|
underline: !activeCategory?.name,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={{ pathname: getCategoryPath('', brand), query }}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={(e) => handleClick(e, 'categories')}
|
||||||
|
className={
|
||||||
|
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
All Categories
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
{categories.map((cat: any) => (
|
||||||
|
<li
|
||||||
|
key={cat.path}
|
||||||
|
className={cn(
|
||||||
|
'block text-sm leading-5 text-accent-4 hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
|
||||||
|
{
|
||||||
|
underline: activeCategory?.id === cat.id,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: getCategoryPath(cat.path, brand),
|
||||||
|
query,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={(e) => handleClick(e, 'categories')}
|
||||||
|
className={
|
||||||
|
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{cat.name}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Designs */}
|
||||||
|
<div className="relative inline-block w-full">
|
||||||
|
<div className="lg:hidden mt-3">
|
||||||
|
<span className="rounded-md shadow-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => handleClick(e, 'brands')}
|
||||||
|
className="flex justify-between w-full rounded-sm border border-accent-3 px-4 py-3 bg-accent-0 text-sm leading-5 font-medium text-accent-8 hover:text-accent-5 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-accent-1 active:text-accent-8 transition ease-in-out duration-150"
|
||||||
|
id="options-menu"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="true"
|
||||||
|
>
|
||||||
|
{activeBrand?.name
|
||||||
|
? `Design: ${activeBrand?.name}`
|
||||||
|
: 'All Designs'}
|
||||||
|
<svg
|
||||||
|
className="-mr-1 ml-2 h-5 w-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`origin-top-left absolute lg:relative left-0 mt-2 w-full rounded-md shadow-lg lg:shadow-none z-10 mb-10 lg:block ${
|
||||||
|
activeFilter !== 'brands' || toggleFilter !== true
|
||||||
|
? 'hidden'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="rounded-sm bg-accent-0 shadow-xs lg:bg-none lg:shadow-none">
|
||||||
|
<div
|
||||||
|
role="menu"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-labelledby="options-menu"
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li
|
||||||
|
className={cn(
|
||||||
|
'block text-sm leading-5 text-accent-4 lg:text-base lg:no-underline lg:font-bold lg:tracking-wide hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
|
||||||
|
{
|
||||||
|
underline: !activeBrand?.name,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: getDesignerPath('', category),
|
||||||
|
query,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={(e) => handleClick(e, 'brands')}
|
||||||
|
className={
|
||||||
|
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
All Designers
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
{brands.flatMap(({ node }: { node: any }) => (
|
||||||
|
<li
|
||||||
|
key={node.path}
|
||||||
|
className={cn(
|
||||||
|
'block text-sm leading-5 text-accent-4 hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
|
||||||
|
{
|
||||||
|
// @ts-ignore Shopify - Fix this types
|
||||||
|
underline: activeBrand?.entityId === node.entityId,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname: getDesignerPath(node.path, category),
|
||||||
|
query,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={(e) => handleClick(e, 'brands')}
|
||||||
|
className={
|
||||||
|
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{node.name}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Products */}
|
||||||
|
<div className="col-span-8 order-3 lg:order-none">
|
||||||
|
{(q || activeCategory || activeBrand) && (
|
||||||
|
<div className="mb-12 transition ease-in duration-75">
|
||||||
|
{data ? (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={cn('animated', {
|
||||||
|
fadeIn: data.found,
|
||||||
|
hidden: !data.found,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Showing {data.products.length} results{' '}
|
||||||
|
{q && (
|
||||||
|
<>
|
||||||
|
for "<strong>{q}</strong>"
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn('animated', {
|
||||||
|
fadeIn: !data.found,
|
||||||
|
hidden: data.found,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{q ? (
|
||||||
|
<>
|
||||||
|
There are no products that match "<strong>{q}</strong>"
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
There are no products that match the selected category.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : q ? (
|
||||||
|
<>
|
||||||
|
Searching for: "<strong>{q}</strong>"
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>Searching...</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{data ? (
|
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{data.products.map((product: Product) => (
|
||||||
|
<ProductCard
|
||||||
|
variant="simple"
|
||||||
|
key={product.path}
|
||||||
|
className="animated fadeIn"
|
||||||
|
product={product}
|
||||||
|
imgProps={{
|
||||||
|
width: 480,
|
||||||
|
height: 480,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{rangeMap(12, (i) => (
|
||||||
|
<Skeleton key={i}>
|
||||||
|
<div className="w-60 h-60" />
|
||||||
|
</Skeleton>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}{' '}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sort */}
|
||||||
|
<div className="col-span-8 lg:col-span-2 order-2 lg:order-none">
|
||||||
|
<div className="relative inline-block w-full">
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<span className="rounded-md shadow-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => handleClick(e, 'sort')}
|
||||||
|
className="flex justify-between w-full rounded-sm border border-accent-3 px-4 py-3 bg-accent-0 text-sm leading-5 font-medium text-accent-4 hover:text-accent-5 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-accent-1 active:text-accent-8 transition ease-in-out duration-150"
|
||||||
|
id="options-menu"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="true"
|
||||||
|
>
|
||||||
|
{sort ? `Sort: ${sort}` : 'Relevance'}
|
||||||
|
<svg
|
||||||
|
className="-mr-1 ml-2 h-5 w-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`origin-top-left absolute lg:relative left-0 mt-2 w-full rounded-md shadow-lg lg:shadow-none z-10 mb-10 lg:block ${
|
||||||
|
activeFilter !== 'sort' || toggleFilter !== true ? 'hidden' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="rounded-sm bg-accent-0 shadow-xs lg:bg-none lg:shadow-none">
|
||||||
|
<div
|
||||||
|
role="menu"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-labelledby="options-menu"
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li
|
||||||
|
className={cn(
|
||||||
|
'block text-sm leading-5 text-accent-4 lg:text-base lg:no-underline lg:font-bold lg:tracking-wide hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
|
||||||
|
{
|
||||||
|
underline: !sort,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link href={{ pathname, query: filterQuery({ q }) }}>
|
||||||
|
<a
|
||||||
|
onClick={(e) => handleClick(e, 'sort')}
|
||||||
|
className={
|
||||||
|
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Relevance
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
{SORT.map(([key, text]) => (
|
||||||
|
<li
|
||||||
|
key={key}
|
||||||
|
className={cn(
|
||||||
|
'block text-sm leading-5 text-accent-4 hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
|
||||||
|
{
|
||||||
|
underline: sort === key,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={{
|
||||||
|
pathname,
|
||||||
|
query: filterQuery({ q, sort: key }),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={(e) => handleClick(e, 'sort')}
|
||||||
|
className={
|
||||||
|
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Search.Layout = Layout
|
@ -1,9 +1,14 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply bg-secondary text-accents-1 cursor-pointer inline-flex px-10 rounded-sm leading-6 transition ease-in-out duration-150 shadow-sm font-semibold text-center justify-center uppercase py-4 border border-transparent items-center;
|
@apply bg-accent-9 text-accent-0 cursor-pointer inline-flex
|
||||||
|
px-10 py-5 leading-6 transition ease-in-out duration-150
|
||||||
|
shadow-sm text-center justify-center uppercase
|
||||||
|
border border-transparent items-center text-sm font-semibold
|
||||||
|
tracking-wide;
|
||||||
|
max-height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root:hover {
|
.root:hover {
|
||||||
@apply bg-accents-0 text-primary border border-secondary;
|
@apply border-accent-9 bg-accent-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root:focus {
|
.root:focus {
|
||||||
@ -11,22 +16,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.root[data-active] {
|
.root[data-active] {
|
||||||
@apply bg-gray-600;
|
@apply bg-accent-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
@apply bg-accents-1 text-accents-3 border-accents-2 cursor-not-allowed;
|
@apply bg-accent-1 text-accent-3 border-accent-2 cursor-not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slim {
|
.slim {
|
||||||
@apply py-2 transform-none normal-case;
|
@apply py-2 transform-none normal-case;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ghost {
|
||||||
|
@apply border border-accent-2 bg-accent-0 text-accent-9 text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost:hover {
|
||||||
|
@apply border-accent-9 bg-accent-9 text-accent-0;
|
||||||
|
}
|
||||||
|
|
||||||
.disabled,
|
.disabled,
|
||||||
.disabled:hover {
|
.disabled:hover {
|
||||||
@apply text-accents-4 border-accents-2 bg-accents-1 cursor-not-allowed;
|
@apply text-accent-4 border-accent-2 bg-accent-1 cursor-not-allowed;
|
||||||
filter: grayscale(1);
|
filter: grayscale(1);
|
||||||
-webkit-transform: translateZ(0);
|
-webkit-transform: translateZ(0);
|
||||||
-webkit-perspective: 1000;
|
-webkit-perspective: 1000;
|
||||||
-webkit-backface-visibility: hidden;
|
-webkit-backface-visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@ import { LoadingDots } from '@components/ui'
|
|||||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
href?: string
|
href?: string
|
||||||
className?: string
|
className?: string
|
||||||
variant?: 'flat' | 'slim'
|
variant?: 'flat' | 'slim' | 'ghost'
|
||||||
active?: boolean
|
active?: boolean
|
||||||
type?: 'submit' | 'reset' | 'button'
|
type?: 'submit' | 'reset' | 'button'
|
||||||
Component?: string | JSXElementConstructor<any>
|
Component?: string | JSXElementConstructor<any>
|
||||||
@ -39,6 +39,7 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
|||||||
const rootClassName = cn(
|
const rootClassName = cn(
|
||||||
s.root,
|
s.root,
|
||||||
{
|
{
|
||||||
|
[s.ghost]: variant === 'ghost',
|
||||||
[s.slim]: variant === 'slim',
|
[s.slim]: variant === 'slim',
|
||||||
[s.loading]: loading,
|
[s.loading]: loading,
|
||||||
[s.disabled]: disabled,
|
[s.disabled]: disabled,
|
||||||
|
25
components/ui/Collapse/Collapse.module.css
Normal file
25
components/ui/Collapse/Collapse.module.css
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
.root {
|
||||||
|
@apply border-b border-accent-2 py-4 flex flex-col outline-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
@apply flex flex-row items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .label {
|
||||||
|
@apply text-base font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
@apply pt-3 overflow-hidden pl-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
@apply mr-3 text-accent-6;
|
||||||
|
margin-left: -6px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon.open {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
46
components/ui/Collapse/Collapse.tsx
Normal file
46
components/ui/Collapse/Collapse.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import cn from 'classnames'
|
||||||
|
import React, { FC, ReactNode, useState } from 'react'
|
||||||
|
import s from './Collapse.module.css'
|
||||||
|
import { ChevronRight } from '@components/icons'
|
||||||
|
import { useSpring, a } from '@react-spring/web'
|
||||||
|
import useMeasure from 'react-use-measure'
|
||||||
|
|
||||||
|
export interface CollapseProps {
|
||||||
|
title: string
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const Collapse: FC<CollapseProps> = React.memo(({ title, children }) => {
|
||||||
|
const [isActive, setActive] = useState(false)
|
||||||
|
const [ref, { height: viewHeight }] = useMeasure()
|
||||||
|
|
||||||
|
const animProps = useSpring({
|
||||||
|
height: isActive ? viewHeight : 0,
|
||||||
|
config: { tension: 250, friction: 32, clamp: true, duration: 150 },
|
||||||
|
opacity: isActive ? 1 : 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggle = () => setActive((x) => !x)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={s.root}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-expanded={isActive}
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<div className={s.header}>
|
||||||
|
<ChevronRight className={cn(s.icon, { [s.open]: isActive })} />
|
||||||
|
<span className={s.label}>{title}</span>
|
||||||
|
</div>
|
||||||
|
<a.div style={{ overflow: 'hidden', ...animProps }}>
|
||||||
|
<div ref={ref} className={s.content}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</a.div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Collapse
|
2
components/ui/Collapse/index.ts
Normal file
2
components/ui/Collapse/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './Collapse'
|
||||||
|
export * from './Collapse'
|
@ -1,14 +1,19 @@
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import React, { FC } from 'react'
|
import React, { FC } from 'react'
|
||||||
|
|
||||||
interface Props {
|
interface ContainerProps {
|
||||||
className?: string
|
className?: string
|
||||||
children?: any
|
children?: any
|
||||||
el?: HTMLElement
|
el?: HTMLElement
|
||||||
clean?: boolean
|
clean?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container: FC<Props> = ({ children, className, el = 'div', clean }) => {
|
const Container: FC<ContainerProps> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
el = 'div',
|
||||||
|
clean,
|
||||||
|
}) => {
|
||||||
const rootClassName = cn(className, {
|
const rootClassName = cn(className, {
|
||||||
'mx-auto max-w-8xl px-6': !clean,
|
'mx-auto max-w-8xl px-6': !clean,
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
.root {
|
.root {
|
||||||
--row-height: calc(100vh - 88px);
|
|
||||||
@apply grid grid-cols-1 gap-0;
|
@apply grid grid-cols-1 gap-0;
|
||||||
min-height: var(--row-height);
|
|
||||||
|
@screen lg {
|
||||||
|
@apply grid-cols-3 grid-rows-2;
|
||||||
|
}
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
@apply row-span-1 bg-transparent box-border overflow-hidden;
|
@apply row-span-1 bg-transparent box-border overflow-hidden;
|
||||||
@ -15,17 +17,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@screen lg {
|
|
||||||
.root {
|
|
||||||
@apply grid-cols-3 grid-rows-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root & > * {
|
|
||||||
@apply col-span-1;
|
|
||||||
height: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.default {
|
.default {
|
||||||
& > * {
|
& > * {
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
@ -34,9 +25,17 @@
|
|||||||
|
|
||||||
.layoutNormal {
|
.layoutNormal {
|
||||||
@apply gap-3;
|
@apply gap-3;
|
||||||
|
}
|
||||||
|
|
||||||
& > * {
|
@screen md {
|
||||||
min-height: 325px;
|
.layoutNormal > * {
|
||||||
|
max-height: min-content !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen lg {
|
||||||
|
.layoutNormal > * {
|
||||||
|
max-height: 400px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,13 +51,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.filled {
|
&.filled {
|
||||||
& > *:nth-child(6n + 1),
|
& > *:nth-child(6n + 1) {
|
||||||
& > *:nth-child(6n + 5) {
|
|
||||||
@apply bg-violet;
|
@apply bg-violet;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > *:nth-child(6n + 5) {
|
& > *:nth-child(6n + 2) {
|
||||||
@apply bg-blue;
|
@apply bg-accent-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > *:nth-child(6n + 3) {
|
& > *:nth-child(6n + 3) {
|
||||||
@ -83,12 +81,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.filled {
|
&.filled {
|
||||||
& > *:nth-child(6n + 2) {
|
& > *:nth-child(6n + 1) {
|
||||||
@apply bg-blue;
|
@apply bg-violet;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > *:nth-child(6n + 4) {
|
& > *:nth-child(6n + 2) {
|
||||||
@apply bg-violet;
|
@apply bg-accent-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > *:nth-child(6n + 3) {
|
& > *:nth-child(6n + 3) {
|
||||||
|
@ -2,14 +2,14 @@ import cn from 'classnames'
|
|||||||
import { FC, ReactNode, Component } from 'react'
|
import { FC, ReactNode, Component } from 'react'
|
||||||
import s from './Grid.module.css'
|
import s from './Grid.module.css'
|
||||||
|
|
||||||
interface Props {
|
interface GridProps {
|
||||||
className?: string
|
className?: string
|
||||||
children?: ReactNode[] | Component[] | any[]
|
children?: ReactNode[] | Component[] | any[]
|
||||||
layout?: 'A' | 'B' | 'C' | 'D' | 'normal'
|
layout?: 'A' | 'B' | 'C' | 'D' | 'normal'
|
||||||
variant?: 'default' | 'filled'
|
variant?: 'default' | 'filled'
|
||||||
}
|
}
|
||||||
|
|
||||||
const Grid: FC<Props> = ({
|
const Grid: FC<GridProps> = ({
|
||||||
className,
|
className,
|
||||||
layout = 'A',
|
layout = 'A',
|
||||||
children,
|
children,
|
||||||
|
@ -1,9 +1,30 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply mx-auto grid grid-cols-1 py-32 gap-4;
|
@apply flex flex-col py-16 mx-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@screen md {
|
.title {
|
||||||
|
@apply text-accent-0 font-extrabold text-4xl leading-none tracking-tight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
@apply mt-4 text-xl leading-8 text-accent-2 mb-1 lg:max-w-4xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen lg {
|
||||||
.root {
|
.root {
|
||||||
@apply grid-cols-2;
|
@apply flex-row items-start justify-center py-32;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
@apply text-5xl max-w-xl text-right leading-10 -mt-3;
|
||||||
|
line-height: 3.5rem;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
@apply mt-0 ml-6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen xl {
|
||||||
|
.title {
|
||||||
|
@apply text-6xl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,26 @@
|
|||||||
import React, { FC } from 'react'
|
import React, { FC } from 'react'
|
||||||
import { Container } from '@components/ui'
|
import { Container } from '@components/ui'
|
||||||
import { RightArrow } from '@components/icons'
|
import { ArrowRight } from '@components/icons'
|
||||||
import s from './Hero.module.css'
|
import s from './Hero.module.css'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
interface Props {
|
interface HeroProps {
|
||||||
className?: string
|
className?: string
|
||||||
headline: string
|
headline: string
|
||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Hero: FC<Props> = ({ headline, description }) => {
|
const Hero: FC<HeroProps> = ({ headline, description }) => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-black">
|
<div className="bg-accent-9 border-b border-t border-accent-2">
|
||||||
<Container>
|
<Container>
|
||||||
<div className={s.root}>
|
<div className={s.root}>
|
||||||
<h2 className="text-4xl leading-10 font-extrabold text-white sm:text-5xl sm:leading-none sm:tracking-tight lg:text-6xl">
|
<h2 className={s.title}>{headline}</h2>
|
||||||
{headline}
|
<div className={s.description}>
|
||||||
</h2>
|
<p>{description}</p>
|
||||||
<div className="flex flex-col justify-between">
|
|
||||||
<p className="mt-5 text-xl leading-7 text-accent-2 text-white">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a className="text-white pt-3 font-bold hover:underline flex flex-row cursor-pointer w-max-content">
|
<a className="flex items-center text-accent-0 pt-3 font-bold hover:underline cursor-pointer w-max-content">
|
||||||
Read it here
|
Read it here
|
||||||
<RightArrow width="20" heigh="20" className="ml-1" />
|
<ArrowRight width="20" heigh="20" className="ml-1" />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply bg-primary py-2 px-6 w-full appearance-none transition duration-150 ease-in-out pr-10 border border-accents-3 text-accents-6;
|
@apply bg-primary py-2 px-6 w-full appearance-none transition duration-150 ease-in-out pr-10 border border-accent-3 text-accent-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root:focus {
|
.root:focus {
|
||||||
|
@ -2,12 +2,12 @@ import cn from 'classnames'
|
|||||||
import s from './Input.module.css'
|
import s from './Input.module.css'
|
||||||
import React, { InputHTMLAttributes } from 'react'
|
import React, { InputHTMLAttributes } from 'react'
|
||||||
|
|
||||||
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
className?: string
|
className?: string
|
||||||
onChange?: (...args: any[]) => any
|
onChange?: (...args: any[]) => any
|
||||||
}
|
}
|
||||||
|
|
||||||
const Input: React.FC<Props> = (props) => {
|
const Input: React.FC<InputProps> = (props) => {
|
||||||
const { className, children, onChange, ...rest } = props
|
const { className, children, onChange, ...rest } = props
|
||||||
|
|
||||||
const rootClassName = cn(s.root, {}, className)
|
const rootClassName = cn(s.root, {}, className)
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply inline-flex text-center items-center leading-7;
|
@apply inline-flex text-center items-center leading-7;
|
||||||
|
}
|
||||||
|
|
||||||
& span {
|
.root .dot {
|
||||||
@apply bg-accents-6 rounded-full h-2 w-2;
|
@apply rounded-full h-2 w-2;
|
||||||
animation-name: blink;
|
background-color: currentColor;
|
||||||
animation-duration: 1.4s;
|
animation-name: blink;
|
||||||
animation-iteration-count: infinite;
|
animation-duration: 1.4s;
|
||||||
animation-fill-mode: both;
|
animation-iteration-count: infinite;
|
||||||
margin: 0 2px;
|
animation-fill-mode: both;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&:nth-of-type(2) {
|
.root .dot:nth-of-type(2) {
|
||||||
animation-delay: 0.2s;
|
animation-delay: 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-of-type(3) {
|
.root .dot::nth-of-type(3) {
|
||||||
animation-delay: 0.4s;
|
animation-delay: 0.4s;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
|
@ -3,9 +3,9 @@ import s from './LoadingDots.module.css'
|
|||||||
const LoadingDots: React.FC = () => {
|
const LoadingDots: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<span className={s.root}>
|
<span className={s.root}>
|
||||||
<span />
|
<span className={s.dot} key={`dot_1`} />
|
||||||
<span />
|
<span className={s.dot} key={`dot_2`} />
|
||||||
<span />
|
<span className={s.dot} key={`dot_3`} />
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply w-full relative;
|
@apply w-full min-w-full relative flex flex-row items-center overflow-hidden py-0;
|
||||||
height: 320px;
|
max-height: 320px;
|
||||||
min-width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.root > div {
|
||||||
@apply flex flex-row items-center;
|
max-height: 320px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container > * {
|
.root > div > * > *:nth-child(2) * {
|
||||||
@apply relative flex-1 px-16 py-4 h-full;
|
max-height: 100%;
|
||||||
min-height: 320px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
@apply bg-white;
|
@apply bg-accent-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
@apply bg-black;
|
@apply bg-accent-9;
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import s from './Marquee.module.css'
|
import s from './Marquee.module.css'
|
||||||
import { FC, ReactNode, Component } from 'react'
|
import { FC, ReactNode, Component, Children } from 'react'
|
||||||
import Ticker from 'react-ticker'
|
import { default as FastMarquee } from 'react-fast-marquee'
|
||||||
|
|
||||||
interface Props {
|
interface MarqueeProps {
|
||||||
className?: string
|
className?: string
|
||||||
children?: ReactNode[] | Component[] | any[]
|
children?: ReactNode[] | Component[] | any[]
|
||||||
variant?: 'primary' | 'secondary'
|
variant?: 'primary' | 'secondary'
|
||||||
}
|
}
|
||||||
|
|
||||||
const Marquee: FC<Props> = ({
|
const Marquee: FC<MarqueeProps> = ({
|
||||||
className = '',
|
className = '',
|
||||||
children,
|
children,
|
||||||
variant = 'primary',
|
variant = 'primary',
|
||||||
@ -24,11 +24,15 @@ const Marquee: FC<Props> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={rootClassName}>
|
<FastMarquee gradient={false} className={rootClassName}>
|
||||||
<Ticker offset={80}>
|
{Children.map(children, (child) => ({
|
||||||
{() => <div className={s.container}>{children}</div>}
|
...child,
|
||||||
</Ticker>
|
props: {
|
||||||
</div>
|
...child.props,
|
||||||
|
className: cn(child.props.className, `${variant}`),
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
</FastMarquee>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply fixed bg-primary text-primary flex items-center inset-0 z-50 justify-center;
|
@apply fixed bg-black bg-opacity-40 flex items-center inset-0 z-50 justify-center;
|
||||||
background-color: rgba(0, 0, 0, 0.35);
|
backdrop-filter: blur(0.8px);
|
||||||
|
-webkit-backdrop-filter: blur(0.8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
@apply bg-primary p-12 border border-accents-2 relative;
|
@apply bg-primary p-12 border border-accent-2 relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal:focus {
|
.modal:focus {
|
||||||
@apply outline-none;
|
@apply outline-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
@apply hover:text-accent-5 transition ease-in-out duration-150 focus:outline-none absolute right-0 top-0 m-6;
|
||||||
|
}
|
||||||
|
@ -1,22 +1,20 @@
|
|||||||
import { FC, useRef, useEffect, useCallback } from 'react'
|
import { FC, useRef, useEffect, useCallback } from 'react'
|
||||||
import Portal from '@reach/portal'
|
|
||||||
import s from './Modal.module.css'
|
import s from './Modal.module.css'
|
||||||
|
import FocusTrap from '@lib/focus-trap'
|
||||||
import { Cross } from '@components/icons'
|
import { Cross } from '@components/icons'
|
||||||
import {
|
import {
|
||||||
disableBodyScroll,
|
disableBodyScroll,
|
||||||
enableBodyScroll,
|
|
||||||
clearAllBodyScrollLocks,
|
clearAllBodyScrollLocks,
|
||||||
|
enableBodyScroll,
|
||||||
} from 'body-scroll-lock'
|
} from 'body-scroll-lock'
|
||||||
import FocusTrap from '@lib/focus-trap'
|
interface ModalProps {
|
||||||
interface Props {
|
|
||||||
className?: string
|
className?: string
|
||||||
children?: any
|
children?: any
|
||||||
open?: boolean
|
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onEnter?: () => void | null
|
onEnter?: () => void | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal: FC<Props> = ({ children, open, onClose, onEnter = null }) => {
|
const Modal: FC<ModalProps> = ({ children, onClose }) => {
|
||||||
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
||||||
|
|
||||||
const handleKey = useCallback(
|
const handleKey = useCallback(
|
||||||
@ -30,36 +28,31 @@ const Modal: FC<Props> = ({ children, open, onClose, onEnter = null }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
if (open) {
|
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
|
||||||
disableBodyScroll(ref.current)
|
window.addEventListener('keydown', handleKey)
|
||||||
window.addEventListener('keydown', handleKey)
|
|
||||||
} else {
|
|
||||||
enableBodyScroll(ref.current)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleKey)
|
if (ref && ref.current) {
|
||||||
|
enableBodyScroll(ref.current)
|
||||||
|
}
|
||||||
clearAllBodyScrollLocks()
|
clearAllBodyScrollLocks()
|
||||||
|
window.removeEventListener('keydown', handleKey)
|
||||||
}
|
}
|
||||||
}, [open, handleKey])
|
}, [handleKey])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<div className={s.root}>
|
||||||
{open ? (
|
<div className={s.modal} role="dialog" ref={ref}>
|
||||||
<div className={s.root}>
|
<button
|
||||||
<div className={s.modal} role="dialog" ref={ref}>
|
onClick={() => onClose()}
|
||||||
<button
|
aria-label="Close panel"
|
||||||
onClick={() => onClose()}
|
className={s.close}
|
||||||
aria-label="Close panel"
|
>
|
||||||
className="hover:text-gray-500 transition ease-in-out duration-150 focus:outline-none absolute right-0 top-0 m-6"
|
<Cross className="h-6 w-6" />
|
||||||
>
|
</button>
|
||||||
<Cross className="h-6 w-6" />
|
<FocusTrap focusFirst>{children}</FocusTrap>
|
||||||
</button>
|
</div>
|
||||||
<FocusTrap focusFirst>{children}</FocusTrap>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</Portal>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
27
components/ui/Quantity/Quantity.module.css
Normal file
27
components/ui/Quantity/Quantity.module.css
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
.actions {
|
||||||
|
@apply flex p-1 border-accent-2 border items-center justify-center
|
||||||
|
w-12 text-accent-7;
|
||||||
|
transition-property: border-color, background, color, transform, box-shadow;
|
||||||
|
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
transition-timing-function: ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions:hover {
|
||||||
|
@apply border bg-accent-1 border-accent-3 text-accent-9;
|
||||||
|
transition: border-color;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions:focus {
|
||||||
|
@apply outline-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions:disabled {
|
||||||
|
@apply cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
@apply bg-transparent px-4 w-full h-full focus:outline-none select-none pointer-events-auto;
|
||||||
|
}
|
62
components/ui/Quantity/Quantity.tsx
Normal file
62
components/ui/Quantity/Quantity.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import s from './Quantity.module.css'
|
||||||
|
import { Cross, Plus, Minus } from '@components/icons'
|
||||||
|
import cn from 'classnames'
|
||||||
|
export interface QuantityProps {
|
||||||
|
value: number
|
||||||
|
increase: () => any
|
||||||
|
decrease: () => any
|
||||||
|
handleRemove: React.MouseEventHandler<HTMLButtonElement>
|
||||||
|
handleChange: React.ChangeEventHandler<HTMLInputElement>
|
||||||
|
max?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const Quantity: FC<QuantityProps> = ({
|
||||||
|
value,
|
||||||
|
increase,
|
||||||
|
decrease,
|
||||||
|
handleChange,
|
||||||
|
handleRemove,
|
||||||
|
max = 6,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row h-9">
|
||||||
|
<button className={s.actions} onClick={handleRemove}>
|
||||||
|
<Cross width={20} height={20} />
|
||||||
|
</button>
|
||||||
|
<label className="w-full border-accent-2 border ml-2">
|
||||||
|
<input
|
||||||
|
className={s.input}
|
||||||
|
onChange={(e) =>
|
||||||
|
Number(e.target.value) < max + 1 ? handleChange(e) : () => {}
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
type="number"
|
||||||
|
max={max}
|
||||||
|
min="0"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={decrease}
|
||||||
|
className={s.actions}
|
||||||
|
style={{ marginLeft: '-1px' }}
|
||||||
|
disabled={value <= 1}
|
||||||
|
>
|
||||||
|
<Minus width={18} height={18} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={increase}
|
||||||
|
className={cn(s.actions)}
|
||||||
|
style={{ marginLeft: '-1px' }}
|
||||||
|
disabled={value < 1 || value >= max}
|
||||||
|
>
|
||||||
|
<Plus width={18} height={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Quantity
|
2
components/ui/Quantity/index.ts
Normal file
2
components/ui/Quantity/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './Quantity'
|
||||||
|
export * from './Quantity'
|
0
components/ui/Rating/Rating.module.css
Normal file
0
components/ui/Rating/Rating.module.css
Normal file
27
components/ui/Rating/Rating.tsx
Normal file
27
components/ui/Rating/Rating.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import rangeMap from '@lib/range-map'
|
||||||
|
import { Star } from '@components/icons'
|
||||||
|
import cn from 'classnames'
|
||||||
|
|
||||||
|
export interface RatingProps {
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const Quantity: React.FC<RatingProps> = React.memo(({ value = 5 }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row py-6 text-accent-9">
|
||||||
|
{rangeMap(5, (i) => (
|
||||||
|
<span
|
||||||
|
key={`star_${i}`}
|
||||||
|
className={cn('inline-block ml-1 ', {
|
||||||
|
'text-accent-5': i >= Math.floor(value),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Star />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Quantity
|
2
components/ui/Rating/index.ts
Normal file
2
components/ui/Rating/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './Rating'
|
||||||
|
export * from './Rating'
|
@ -1,3 +1,14 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply fixed inset-0 overflow-hidden h-full z-50;
|
@apply fixed inset-0 h-full z-50 box-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
@apply h-full flex flex-col text-base bg-accent-0 shadow-xl overflow-y-auto overflow-x-hidden;
|
||||||
|
-webkit-overflow-scrolling: touch !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
@apply absolute inset-0 bg-black bg-opacity-40 duration-100 ease-linear;
|
||||||
|
backdrop-filter: blur(0.8px);
|
||||||
|
-webkit-backdrop-filter: blur(0.8px);
|
||||||
}
|
}
|
||||||
|
@ -1,54 +1,45 @@
|
|||||||
import s from './Sidebar.module.css'
|
|
||||||
import Portal from '@reach/portal'
|
|
||||||
import { FC, useEffect, useRef } from 'react'
|
import { FC, useEffect, useRef } from 'react'
|
||||||
|
import s from './Sidebar.module.css'
|
||||||
|
import cn from 'classnames'
|
||||||
import {
|
import {
|
||||||
disableBodyScroll,
|
disableBodyScroll,
|
||||||
enableBodyScroll,
|
enableBodyScroll,
|
||||||
clearAllBodyScrollLocks,
|
clearAllBodyScrollLocks,
|
||||||
} from 'body-scroll-lock'
|
} from 'body-scroll-lock'
|
||||||
|
|
||||||
interface Props {
|
interface SidebarProps {
|
||||||
children: any
|
children: any
|
||||||
open: boolean
|
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar: FC<Props> = ({ children, open = false, onClose }) => {
|
const Sidebar: FC<SidebarProps> = ({ children, onClose }) => {
|
||||||
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
if (open) {
|
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
|
||||||
disableBodyScroll(ref.current)
|
|
||||||
} else {
|
|
||||||
enableBodyScroll(ref.current)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
|
if (ref && ref.current) {
|
||||||
|
enableBodyScroll(ref.current)
|
||||||
|
}
|
||||||
clearAllBodyScrollLocks()
|
clearAllBodyScrollLocks()
|
||||||
}
|
}
|
||||||
}, [open])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<div className={cn(s.root)}>
|
||||||
{open ? (
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
<div className={s.root} ref={ref}>
|
<div className={s.backdrop} onClick={onClose} />
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<section className="absolute inset-y-0 right-0 max-w-full flex outline-none pl-10">
|
||||||
<div
|
<div className="h-full w-full md:w-screen md:max-w-md">
|
||||||
className="absolute inset-0 bg-black bg-opacity-50 transition-opacity"
|
<div className={s.sidebar} ref={ref}>
|
||||||
onClick={onClose}
|
{children}
|
||||||
/>
|
</div>
|
||||||
<section className="absolute inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16 outline-none">
|
|
||||||
<div className="h-full md:w-screen md:max-w-md">
|
|
||||||
<div className="h-full flex flex-col text-base bg-accents-1 shadow-xl overflow-y-auto">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
) : null}
|
</div>
|
||||||
</Portal>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
@apply block;
|
@apply block;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
270deg,
|
270deg,
|
||||||
var(--accents-1),
|
var(--accent-0),
|
||||||
var(--accents-2),
|
var(--accent-2),
|
||||||
var(--accents-2),
|
var(--accent-0),
|
||||||
var(--accents-1)
|
var(--accent-1)
|
||||||
);
|
);
|
||||||
background-size: 400% 100%;
|
background-size: 400% 100%;
|
||||||
animation: loading 8s ease-in-out infinite;
|
animation: loading 8s ease-in-out infinite;
|
||||||
@ -28,10 +28,10 @@
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
270deg,
|
270deg,
|
||||||
var(--accents-1),
|
var(--accent-0),
|
||||||
var(--accents-2),
|
var(--accent-2),
|
||||||
var(--accents-2),
|
var(--accent-0),
|
||||||
var(--accents-1)
|
var(--accent-1)
|
||||||
);
|
);
|
||||||
background-size: 400% 100%;
|
background-size: 400% 100%;
|
||||||
animation: loading 8s ease-in-out infinite;
|
animation: loading 8s ease-in-out infinite;
|
||||||
|
@ -3,17 +3,17 @@ import cn from 'classnames'
|
|||||||
import px from '@lib/to-pixels'
|
import px from '@lib/to-pixels'
|
||||||
import s from './Skeleton.module.css'
|
import s from './Skeleton.module.css'
|
||||||
|
|
||||||
interface Props {
|
interface SkeletonProps {
|
||||||
width?: string | number
|
|
||||||
height?: string | number
|
|
||||||
boxHeight?: string | number
|
|
||||||
style?: CSSProperties
|
|
||||||
show?: boolean
|
show?: boolean
|
||||||
block?: boolean
|
block?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
style?: CSSProperties
|
||||||
|
width?: string | number
|
||||||
|
height?: string | number
|
||||||
|
boxHeight?: string | number
|
||||||
}
|
}
|
||||||
|
|
||||||
const Skeleton: React.FC<Props> = ({
|
const Skeleton: React.FC<SkeletonProps> = ({
|
||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user