mirror of
https://github.com/vercel/commerce.git
synced 2025-03-31 17:25:53 +00:00
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
@ -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