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:
B 2021-06-15 20:23:17 -03:00 committed by GitHub
parent 3c9b90f453
commit 78cc378a72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
202 changed files with 15406 additions and 2201 deletions

View File

@ -64,6 +64,11 @@ That's it!
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
> 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
{
"features": {
"wishlist": false
"wishlist": false,
"customCheckout": true
}
}
```

View File

@ -15,21 +15,28 @@
--cyan: #22b8cf;
--green: #37b679;
--red: #da3c3c;
--pink: #e64980;
--purple: #f81ce5;
--blue: #0070f3;
--violet: #5f3dc4;
--violet-light: #7048e8;
--accents-0: #f8f9fa;
--accents-1: #f1f3f5;
--accents-2: #e9ecef;
--accents-3: #dee2e6;
--accents-4: #ced4da;
--accents-5: #adb5bd;
--accents-6: #868e96;
--accents-7: #495057;
--accents-8: #343a40;
--accents-9: #212529;
--pink: #ff0080;
--pink-light: #ff379c;
--magenta: #eb367f;
--violet: #7928ca;
--violet-dark: #4c2889;
--accent-0: #fff;
--accent-1: #fafafa;
--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',
'Helvetica', sans-serif;
}
@ -48,16 +55,16 @@
--text-primary: white;
--text-secondary: black;
--accents-0: #212529;
--accents-1: #343a40;
--accents-2: #495057;
--accents-3: #868e96;
--accents-4: #adb5bd;
--accents-5: #ced4da;
--accents-6: #dee2e6;
--accents-7: #e9ecef;
--accents-8: #f1f3f5;
--accents-9: #f8f9fa;
--accent-9: #fff;
--accent-8: #fafafa;
--accent-7: #eaeaea;
--accent-6: #999999;
--accent-5: #888888;
--accent-4: #666666;
--accent-3: #444444;
--accent-2: #333333;
--accent-1: #111111;
--accent-0: #000;
}
*,
@ -84,6 +91,7 @@ body {
-moz-osx-font-smoothing: grayscale;
background-color: var(--primary);
color: var(--text-primary);
overscroll-behavior-x: none;
}
body {

View File

@ -61,7 +61,7 @@ const ForgotPassword: FC<Props> = () => {
</div>
<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
className="text-accent-9 font-bold hover:underline cursor-pointer"

View File

@ -87,7 +87,7 @@ const LoginView: FC<Props> = () => {
Log In
</Button>
<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
className="text-accent-9 font-bold hover:underline cursor-pointer"

View File

@ -76,7 +76,7 @@ const SignUpView: FC<Props> = () => {
<Input placeholder="Last Name" onChange={setLastName} />
<Input type="email" placeholder="Email" onChange={setEmail} />
<Input type="password" placeholder="Password" onChange={setPassword} />
<span className="text-accents-8">
<span className="text-accent-8">
<span className="inline-block align-middle ">
<Info width="15" height="15" />
</span>{' '}
@ -97,7 +97,7 @@ const SignUpView: FC<Props> = () => {
</div>
<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
className="text-accent-9 font-bold hover:underline cursor-pointer"

View File

@ -1,6 +1,14 @@
.root {
@apply flex flex-col py-4;
}
.root:first-child {
padding-top: 0;
}
.quantity {
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,
@ -15,4 +23,10 @@
height: 100%;
left: 30% !important;
top: 30% !important;
z-index: 1;
}
.productName {
@apply font-medium cursor-pointer pb-1;
margin-top: -4px;
}

View File

@ -1,14 +1,15 @@
import { ChangeEvent, useEffect, useState } from 'react'
import { ChangeEvent, FocusEventHandler, useEffect, useState } from 'react'
import cn from 'classnames'
import Image from 'next/image'
import Link from 'next/link'
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 type { LineItem } from '@commerce/types/cart'
import usePrice from '@framework/product/use-price'
import useUpdateItem from '@framework/cart/use-update-item'
import useRemoveItem from '@framework/cart/use-remove-item'
import Quantity from '@components/ui/Quantity'
type ItemOption = {
name: string
@ -19,13 +20,19 @@ type ItemOption = {
const CartItem = ({
item,
variant = 'default',
currencyCode,
...rest
}: {
variant?: 'default' | 'display'
item: LineItem
currencyCode: string
}) => {
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({
amount: item.variant.price * item.quantity,
@ -33,48 +40,28 @@ const CartItem = ({
currencyCode,
})
const updateItem = useUpdateItem({ item })
const removeItem = useRemoveItem()
const [quantity, setQuantity] = useState<number | ''>(item.quantity)
const [removing, setRemoving] = useState(false)
const handleChange = async ({
target: { value },
}: ChangeEvent<HTMLInputElement>) => {
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 })
}
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 () => {
setRemoving(true)
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)
} catch (error) {
setRemoving(false)
}
}
// TODO: Add a type for this
const options = (item as any).options
@ -87,79 +74,76 @@ const CartItem = ({
return (
<li
className={cn('flex flex-row space-x-8 py-8', {
'opacity-75 pointer-events-none': removing,
className={cn(s.root, {
'opacity-50 pointer-events-none': removing,
})}
{...rest}
>
<div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer">
<Link href={`/product/${item.path}`}>
<Image
onClick={() => closeSidebarIfPresent()}
className={s.productImage}
width={150}
height={150}
src={item.variant.image!.url}
alt={item.variant.image!.altText}
unoptimized
/>
</Link>
</div>
<div className="flex-1 flex flex-col text-base">
<Link href={`/product/${item.path}`}>
<span
onClick={() => closeSidebarIfPresent()}
>
<div
className="font-bold text-lg cursor-pointer leading-6"
<div className="flex flex-row space-x-4 py-4">
<div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer z-0">
<Link href={`/product/${item.path}`}>
<Image
onClick={() => closeSidebarIfPresent()}
className={s.productImage}
width={150}
height={150}
src={item.variant.image!.url}
alt={item.variant.image!.altText}
unoptimized
/>
</Link>
</div>
<div className="flex-1 flex flex-col text-base">
<Link href={`/product/${item.path}`}>
<span
className={s.productName}
onClick={() => closeSidebarIfPresent()}
>
{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>
{item.variant ? <span> {item.variant.name}</span> : ""}
</span>
</Link>
{options && options.length > 0 ? (
<div className="">
{options.map((option: ItemOption, i: number) => (
<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>
)}
{variant === 'display' && (
<div className="text-sm tracking-wider">{quantity}x</div>
)}
</div>
<div className="flex flex-col justify-between space-y-2 text-sm">
<span>{price}</span>
</div>
</div>
<div className="flex flex-col justify-between space-y-2 text-base">
<span>{price}</span>
<button
className="flex justify-end outline-none"
onClick={handleRemove}
>
<Trash />
</button>
</div>
{variant === 'default' && (
<Quantity
value={quantity}
handleRemove={handleRemove}
handleChange={handleChange}
increase={() => increaseQuantity(1)}
decrease={() => increaseQuantity(-1)}
/>
)}
</li>
)
}

View File

@ -1,15 +1,11 @@
.root {
@apply h-full flex flex-col;
min-height: 100vh;
}
.root.empty {
@apply bg-secondary text-secondary;
}
.root.success {
@apply bg-green text-white;
}
.root.error {
@apply bg-red text-white;
.lineItemsList {
@apply py-4 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accent-2 border-accent-2;
}

View File

@ -1,17 +1,17 @@
import { FC } from 'react'
import cn from 'classnames'
import Link from 'next/link'
import CartItem from '../CartItem'
import { FC } from 'react'
import s from './CartSidebarView.module.css'
import { Button } from '@components/ui'
import { UserNav } from '@components/common'
import CartItem from '../CartItem'
import { Button, Text } from '@components/ui'
import { useUI } from '@components/ui/context'
import { Bag, Cross, Check } from '@components/icons'
import useCart from '@framework/cart/use-cart'
import usePrice from '@framework/product/use-price'
import SidebarLayout from '@components/common/SidebarLayout'
const CartSidebarView: FC = () => {
const { closeSidebar } = useUI()
const { closeSidebar, setSidebarView } = useUI()
const { data, isLoading, isEmpty } = useCart()
const { price: subTotal } = usePrice(
@ -27,33 +27,18 @@ const CartSidebarView: FC = () => {
}
)
const handleClose = () => closeSidebar()
const goToCheckout = () => setSidebarView('CHECKOUT_VIEW')
const error = null
const success = null
return (
<div
className={cn(s.root, {
<SidebarLayout
className={cn({
[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 ? (
<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">
@ -62,7 +47,7 @@ const CartSidebarView: FC = () => {
<h2 className="pt-6 text-2xl font-bold tracking-wide text-center">
Your cart is empty
</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.
</p>
</div>
@ -89,14 +74,11 @@ const CartSidebarView: FC = () => {
<>
<div className="px-4 sm:px-6 flex-1">
<Link href="/cart">
<h2
className="pt-1 pb-4 text-2xl leading-7 font-bold text-base tracking-wide cursor-pointer inline-block"
onClick={handleClose}
>
<Text variant="sectionHeading" onClick={handleClose}>
My Cart
</h2>
</Text>
</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) => (
<CartItem
key={item.id}
@ -107,34 +89,40 @@ const CartSidebarView: FC = () => {
</ul>
</div>
<div className="flex-shrink-0 px-4 py-5 sm:px-6">
<div className="border-t border-accents-3">
<ul className="py-3">
<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>Estimated Shipping</span>
<span className="font-bold tracking-wide">FREE</span>
</li>
</ul>
<div className="flex justify-between border-t border-accents-3 py-3 font-bold mb-10">
<span>Total</span>
<span>{total}</span>
</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>
{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>
<Button href="/checkout" Component="a" width="100%">
Proceed to Checkout
</Button>
</div>
</>
)}
</div>
</SidebarLayout>
)
}

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View 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

View File

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

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

View 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

View File

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

View File

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

View 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

View File

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

View File

@ -14,7 +14,7 @@ const Avatar: FC<Props> = ({}) => {
<div
ref={ref}
style={{ backgroundImage: userAvatar }}
className="inline-block h-8 w-8 rounded-full border-2 border-primary hover:border-secondary focus:border-secondary transition 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> */}
</div>

View File

@ -1,9 +1,7 @@
.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;
}
@screen md {
.root {
@screen md {
@apply flex text-left;
}
}

View File

@ -1,3 +1,7 @@
.root {
@apply border-t border-accent-2;
}
.link {
& > svg {
@apply transform duration-75 ease-linear;

View File

@ -15,65 +15,50 @@ interface Props {
pages?: Page[]
}
const LEGAL_PAGES = ['terms-of-use', 'shipping-returns', 'privacy-policy']
const links = [
{
name: 'Home',
url: '/',
},
]
const Footer: FC<Props> = ({ className, pages }) => {
const { sitePages, legalPages } = usePages(pages)
const rootClassName = cn(className)
const { sitePages } = usePages(pages)
const rootClassName = cn(s.root, className)
return (
<footer className={rootClassName}>
<Container>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 border-b border-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">
<Link href="/">
<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 />
</span>
<span>ACME</span>
</a>
</Link>
</div>
<div className="col-span-1 lg:col-span-2">
<ul className="flex flex-initial flex-col md:flex-1">
<li className="py-3 md:py-0 md:pb-4">
<Link href="/">
<a className="text-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">
<div className="col-span-1 lg:col-span-8">
<div className="grid md:grid-rows-4 md:grid-cols-3 md:grid-flow-col">
{[...links, ...sitePages].map((page) => (
<span key={page.url} className="py-3 md:py-0 md:pb-4">
<Link href={page.url!}>
<a className="text-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}
</a>
</Link>
</li>
</span>
))}
</ul>
</div>
</div>
<div className="col-span-1 lg:col-span-2">
<ul className="flex flex-initial flex-col md:flex-1">
{legalPages.map((page) => (
<li key={page.url} className="py-3 md:py-0 md:pb-4">
<Link href={page.url!}>
<a className="text-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="col-span-1 lg:col-span-2 flex items-start lg:justify-end text-primary">
<div className="flex space-x-6 items-center h-10">
<a
className={s.link}
aria-label="Github Repository"
href="https://github.com/vercel/commerce"
className={s.link}
>
<Github />
</a>
@ -81,12 +66,12 @@ const Footer: FC<Props> = ({ className, pages }) => {
</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>
<span>&copy; 2020 ACME, Inc. All rights reserved.</span>
</div>
<div className="flex items-center text-primary">
<span className="text-primary">Crafted by</span>
<div className="flex items-center text-primary text-sm">
<span className="text-primary">Created by</span>
<a
rel="noopener"
href="https://vercel.com"
@ -95,7 +80,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
className="text-primary"
>
<Vercel
className="inline-block h-6 ml-4 text-primary"
className="inline-block h-6 ml-3 text-primary"
alt="Vercel.com Logo"
/>
</a>
@ -109,34 +94,21 @@ const Footer: FC<Props> = ({ className, pages }) => {
function usePages(pages?: Page[]) {
const { locale } = useRouter()
const sitePages: Page[] = []
const legalPages: Page[] = []
if (pages) {
pages.forEach((page) => {
const slug = page.url && getSlug(page.url)
if (!slug) return
if (locale && !slug.startsWith(`${locale}/`)) return
if (isLegalPage(slug, locale)) {
legalPages.push(page)
} else {
sitePages.push(page)
}
sitePages.push(page)
})
}
return {
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
function bySortOrder(a: Page, b: Page) {
return (a.sort_order ?? 0) - (b.sort_order ?? 0)

View File

@ -28,7 +28,7 @@ const HomeAllProductsGrid: FC<Props> = ({
</Link>
</li>
{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)}>
<a>{cat.name}</a>
</Link>
@ -42,7 +42,7 @@ const HomeAllProductsGrid: FC<Props> = ({
</Link>
</li>
{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)}>
<a>{node.name}</a>
</Link>

View File

@ -3,11 +3,11 @@
}
.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 {
@apply border-accents-4 shadow-sm;
@apply border-accent-3 shadow-sm;
}
.button:focus {
@ -16,16 +16,14 @@
.dropdownMenu {
@apply fixed right-0 top-12 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
}
@screen lg {
.dropdownMenu {
@apply absolute border border-accents-1 shadow-lg w-56 h-auto;
@screen lg {
@apply absolute border border-accent-1 shadow-lg w-56 h-auto;
}
}
@screen md {
.closeButton {
.closeButton {
@screen md {
@apply hidden;
}
}
@ -36,9 +34,13 @@
}
.item:hover {
@apply bg-accents-1;
@apply bg-accent-1;
}
.icon {
transition: transform 0.2s ease;
}
.icon.active {
transform: rotate(180deg);
}

View File

@ -59,7 +59,7 @@ const I18nWidget: FC = () => {
/>
{options && (
<span className="cursor-pointer">
<ChevronUp className={cn({ [s.icon]: display })} />
<ChevronUp className={cn(s.icon, { [s.active]: display })} />
</span>
)}
</button>

View File

@ -1,15 +1,19 @@
import React, { FC } from 'react'
import { useRouter } from 'next/router'
import dynamic from 'next/dynamic'
import cn from 'classnames'
import type { Page } from '@commerce/types/page'
import type { Category } from '@commerce/types/site'
import React, { FC } from 'react'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import { CommerceProvider } from '@framework'
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { useUI } from '@components/ui/context'
import type { Page } from '@commerce/types/page'
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 { 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 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> = ({
children,
pageProps: { categories = [], ...pageProps },
}) => {
const { displaySidebar, displayModal, closeSidebar, closeModal, modalView } =
useUI()
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
const { locale = 'en-US' } = useRouter()
const navBarlinks = categories.slice(0, 2).map((c) => ({
label: c.name,
href: `/search/${c.slug}`,
@ -65,17 +107,8 @@ const Layout: FC<Props> = ({
<Navbar links={navBarlinks} />
<main className="fit">{children}</main>
<Footer pages={pageProps.pages} />
<Modal open={displayModal} onClose={closeModal}>
{modalView === 'LOGIN_VIEW' && <LoginView />}
{modalView === 'SIGNUP_VIEW' && <SignUpView />}
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
</Modal>
<Sidebar open={displaySidebar} onClose={closeSidebar}>
<CartSidebarView />
</Sidebar>
<ModalUI />
<SidebarUI />
<FeatureBar
title="This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy."
hide={acceptedCookies}

View File

@ -2,16 +2,26 @@
@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 {
@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 {
@apply text-accents-9;
@apply text-accent-9;
}
.link:focus {
@apply outline-none text-accents-8;
@apply outline-none text-accent-8;
}
.logo {

View File

@ -1,9 +1,9 @@
import { FC } from 'react'
import Link from 'next/link'
import s from './Navbar.module.css'
import NavbarRoot from './NavbarRoot'
import { Logo, Container } from '@components/ui'
import { Searchbar, UserNav } from '@components/common'
import NavbarRoot from './NavbarRoot'
import s from './Navbar.module.css'
interface Link {
href: string
@ -16,14 +16,14 @@ interface NavbarProps {
const Navbar: FC<NavbarProps> = ({ links }) => (
<NavbarRoot>
<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">
<Link href="/">
<a className={s.logo} aria-label="Logo">
<Logo />
</a>
</Link>
<nav className="hidden ml-6 space-x-4 lg:block">
<nav className={s.navMenu}>
<Link href="/search">
<a className={s.link}>All</a>
</Link>
@ -34,16 +34,13 @@ const Navbar: FC<NavbarProps> = ({ links }) => (
))}
</nav>
</div>
<div className="justify-center flex-1 hidden lg:flex">
<Searchbar />
</div>
<div className="flex justify-end flex-1 space-x-8">
<div className="flex items-center justify-end flex-1 space-x-8">
<UserNav />
</div>
</div>
<div className="flex pb-4 lg:px-6 lg:hidden">
<Searchbar id="mobile-search" />
</div>

View File

@ -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 {
@apply bg-transparent px-3 py-2 appearance-none w-full transition duration-150 ease-in-out pr-10;
}
@screen sm {
min-width: 300px;
}
.input::placeholder {
@apply text-accent-3;
}
.input:focus {
@ -17,3 +21,9 @@
.icon {
@apply h-5 w-5;
}
@screen sm {
.input {
min-width: 300px;
}
}

View File

@ -1,4 +1,4 @@
import { FC, useEffect, useMemo } from 'react'
import { FC, InputHTMLAttributes, useEffect, useMemo } from 'react'
import cn from 'classnames'
import s from './Searchbar.module.css'
import { useRouter } from 'next/router'
@ -15,14 +15,26 @@ const Searchbar: FC<Props> = ({ className, id = '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(
() => (
<div
className={cn(
'relative text-sm bg-accents-1 text-base w-full transition-colors duration-150',
className
)}
>
<div className={cn(s.root, className)}>
<label className="hidden" htmlFor={id}>
Search
</label>
@ -31,22 +43,7 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
className={s.input}
placeholder="Search for products..."
defaultValue={router.query.q}
onKeyUp={(e) => {
e.preventDefault()
if (e.key === 'Enter') {
const q = e.currentTarget.value
router.push(
{
pathname: `/search`,
query: q ? { q } : {},
},
undefined,
{ shallow: true }
)
}
}}
onKeyUp={handleKeyUp}
/>
<div className={s.iconContainer}>
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">

View File

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

View File

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

View File

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

View File

@ -1,10 +1,8 @@
.dropdownMenu {
@apply fixed right-0 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
}
@screen lg {
.dropdownMenu {
@apply absolute top-10 border border-accents-1 shadow-lg w-56 h-auto;
@screen lg {
@apply absolute top-10 border border-accent-1 shadow-lg w-56 h-auto;
}
}
@ -14,11 +12,11 @@
}
.link:hover {
@apply bg-accents-1;
@apply bg-accent-1;
}
.link.active {
@apply font-bold bg-accents-2;
@apply font-bold bg-accent-2;
}
.off {

View File

@ -109,7 +109,7 @@ const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
</li>
<li>
<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()}
>
Logout

View File

@ -10,7 +10,7 @@
@apply mr-6 cursor-pointer relative transition ease-in-out duration-100 flex items-center outline-none text-primary;
&:hover {
@apply text-accents-6 transition scale-110 duration-100;
@apply text-accent-6 transition scale-110 duration-100;
}
&:last-child {
@ -24,7 +24,7 @@
}
.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-right: 2.5px;
min-width: 1.25rem;

View File

@ -24,21 +24,21 @@ const UserNav: FC<Props> = ({ className }) => {
return (
<nav className={cn(s.root, className)}>
<div className={s.mainContainer}>
<ul className={s.list}>
<li className={s.item} onClick={toggleSidebar}>
<Bag />
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
<ul className={s.list}>
<li className={s.item} onClick={toggleSidebar}>
<Bag />
{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>
{process.env.COMMERCE_WISHLIST_ENABLED && (
<li className={s.item}>
<Link href="/wishlist">
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
<Heart />
</a>
</Link>
</li>
)}
)}
{process.env.COMMERCE_CUSTOMER_ENABLED && (
<li className={s.item}>
{customer ? (
<DropdownMenu />
@ -52,8 +52,8 @@ const UserNav: FC<Props> = ({ className }) => {
</button>
)}
</li>
</ul>
</div>
)}
</ul>
</nav>
)
}

View File

@ -1,23 +1,22 @@
const RightArrow = ({ ...props }) => {
const ArrowRight = ({ ...props }) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M5 12H19"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12 5L19 12L12 19"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
@ -26,4 +25,4 @@ const RightArrow = ({ ...props }) => {
)
}
export default RightArrow
export default ArrowRight

View 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

View 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

View 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

View File

@ -10,6 +10,7 @@ const CreditCard = ({ ...props }) => {
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
{...props}
>
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
<path d="M1 10h22" />

16
components/icons/Star.tsx Normal file
View 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

View File

@ -2,17 +2,21 @@ export { default as Bag } from './Bag'
export { default as Heart } from './Heart'
export { default as Trash } from './Trash'
export { default as Cross } from './Cross'
export { default as ArrowLeft } from './ArrowLeft'
export { default as Plus } from './Plus'
export { default as Minus } from './Minus'
export { default as Check } from './Check'
export { default as Sun } from './Sun'
export { default as Moon } from './Moon'
export { default as Github } from './Github'
export { default as DoubleChevron } from './DoubleChevron'
export { default as RightArrow } from './RightArrow'
export { default as Info } from './Info'
export { default as ChevronUp } from './ChevronUp'
export { default as Vercel } from './Vercel'
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 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'

View File

@ -1,136 +1,114 @@
.root {
@apply relative max-h-full w-full box-border overflow-hidden
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;
}
&:hover {
& .squareBg:before {
transform: scale(0.98);
}
& .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;
}
.root:hover {
& .productImage {
transform: scale(1.2625);
}
&:nth-child(6n + 1) .squareBg {
@apply bg-violet;
& .header .name span,
& .header .price,
& .wishlistButton {
@apply bg-secondary text-secondary;
}
&:nth-child(6n + 5) .squareBg {
@apply bg-blue;
&:nth-child(6n + 1) .header .name span,
&:nth-child(6n + 1) .header .price,
&:nth-child(6n + 1) .wishlistButton {
@apply bg-violet text-white;
}
&:nth-child(6n + 3) .squareBg {
@apply bg-pink;
&:nth-child(6n + 5) .header .name span,
&:nth-child(6n + 5) .header .price,
&:nth-child(6n + 5) .wishlistButton {
@apply bg-blue text-white;
}
&:nth-child(6n + 6) .squareBg {
@apply bg-cyan;
&:nth-child(6n + 3) .header .name span,
&: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,
.productTitle > span,
.productPrice,
.wishlistButton {
@apply transition-colors ease-in-out duration-500;
.header {
@apply transition-colors ease-in-out duration-500
absolute top-0 left-0 z-20 pr-16;
}
.squareBg {
@apply transition-colors absolute inset-0 z-0;
background-color: #212529;
}
.squareBg:before {
@apply transition ease-in-out duration-500 bg-repeat-space w-full h-full block;
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;
.header .name {
@apply pt-0 max-w-full w-full leading-extra-loose
transition-colors ease-in-out duration-500;
font-size: 2rem;
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 {
@apply py-4 px-6 bg-primary text-primary font-semibold inline-block text-sm leading-6;
letter-spacing: 0.4px;
.header .name span {
@apply py-4 px-6 bg-primary text-primary font-bold
transition-colors ease-in-out duration-500;
font-size: inherit;
letter-spacing: inherit;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
.wishlistButton {
@apply w-10 h-10 flex ml-auto items-center justify-center bg-primary text-primary font-semibold text-xs leading-6 cursor-pointer;
.header .price {
@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 {
@apply flex items-center justify-center;
overflow: hidden;
& > div {
min-width: 100%;
}
@apply flex items-center justify-center overflow-hidden;
}
.productImage {
@apply transform transition-transform duration-500 object-cover scale-120;
.imageContainer > div {
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;
}

View File

@ -5,58 +5,98 @@ import type { Product } from '@commerce/types/product'
import s from './ProductCard.module.css'
import Image, { ImageProps } from 'next/image'
import WishlistButton from '@components/wishlist/WishlistButton'
import usePrice from '@framework/product/use-price'
import ProductTag from '../ProductTag'
interface Props {
className?: string
product: Product
variant?: 'slim' | 'simple'
imgProps?: Omit<any, 'src'>
noNameTag?: boolean
imgProps?: Omit<ImageProps, 'src'>
variant?: 'default' | 'slim' | 'simple'
}
const placeholderImg = '/product-img-placeholder.svg'
const ProductCard: FC<Props> = ({
className,
product,
variant,
imgProps,
className,
noNameTag = false,
variant = 'default',
...props
}) => (
<Link href={`/product/${product.slug}`} {...props}>
<a className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}>
{variant === 'slim' ? (
<div className="relative overflow-hidden box-border">
<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}
</span>
</div>
{product?.images && (
<Image
quality="85"
src={product.images[0]?.url || placeholderImg}
alt={product.name || 'Product Image'}
height={320}
width={320}
layout="fixed"
{...imgProps}
/>
)}
</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}
&nbsp;
{product.price.currencyCode}
</span>
}) => {
const { price } = usePrice({
amount: product.price.value,
baseAmount: product.price.retailPrice,
currencyCode: product.price.currencyCode!,
})
const rootClassName = cn(
s.root,
{ [s.slim]: variant === 'slim', [s.simple]: variant === 'simple' },
className
)
return (
<Link href={`/product/${product.slug}`} {...props}>
<a className={rootClassName}>
{variant === 'slim' && (
<>
<div className={s.header}>
<span>{product.name}</span>
</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 && (
<WishlistButton
className={s.wishlistButton}
@ -64,25 +104,29 @@ const ProductCard: FC<Props> = ({
variant={product.variants[0] as any}
/>
)}
</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>
</>
)}
</a>
</Link>
)
<ProductTag
name={product.name}
price={`${price} ${product.price?.currencyCode}`}
/>
<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>
</>
)}
</a>
</Link>
)
}
export default ProductCard

View 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

View File

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

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

View 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

View File

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

View File

@ -1,82 +1,64 @@
.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-x: auto;
white-space: nowrap;
height: 125px;
scrollbar-width: none;
}
.leftControl,
.rightControl {
@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;
/* Hide scrollbar for Chrome, Safari and Opera */
.album::-webkit-scrollbar {
display: none;
}
.leftControl:hover,
.rightControl:hover {
@apply bg-hover-2;
}
@screen md {
.thumb:hover {
transform: scale(1.02);
background-color: rgba(255, 255, 255, 0.08);
}
.leftControl:hover,
.rightControl:hover {
@apply outline-none shadow-outline-normal;
}
.thumb.selected {
@apply bg-white;
}
.leftControl {
@apply bg-cover left-10;
background-image: url('public/cursor-left.png');
@screen md {
@apply left-6;
.album {
height: 182px;
}
.thumb {
width: 235px;
}
}
.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;
}

View File

@ -8,20 +8,42 @@ import React, {
useEffect,
} from 'react'
import cn from 'classnames'
import { a } from '@react-spring/web'
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 [isMounted, setIsMounted] = useState(false)
const sliderContainerRef = useRef<HTMLDivElement>(null)
const thumbsContainerRef = useRef<HTMLDivElement>(null)
const [ref, slider] = useKeenSlider<HTMLDivElement>({
loop: true,
slidesPerView: 1,
mounted: () => setIsMounted(true),
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 (
<div className={s.root} 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 className={cn(s.root, className)} ref={sliderContainerRef}>
<div
ref={ref}
className="keen-slider h-full transition-opacity duration-150"
style={{ opacity: isMounted ? 1 : 0 }}
className={cn(s.slider, { [s.show]: isMounted }, 'keen-slider')}
>
{slider && <ProductSliderControl onPrev={onPrev} onNext={onNext} />}
{Children.map(children, (child) => {
// Add the keen-slider__slide className to children
if (isValidElement(child)) {
@ -92,26 +107,28 @@ const ProductSlider: FC = ({ children }) => {
return child
})}
</div>
{slider && (
<div className={cn(s.positionIndicatorsContainer)}>
{[...Array(slider.details().size).keys()].map((idx) => {
return (
<button
aria-label="Position indicator"
key={idx}
className={cn(s.positionIndicator, {
[s.positionIndicatorActive]: currentSlide === idx,
})}
onClick={() => {
slider.moveToSlideRelative(idx)
}}
>
<div className={s.dot} />
</button>
)
<a.div className={s.album} ref={thumbsContainerRef}>
{slider &&
Children.map(children, (child, idx) => {
if (isValidElement(child)) {
return {
...child,
props: {
...child.props,
className: cn(child.props.className, s.thumb, {
[s.selected]: currentSlide === idx,
}),
id: `thumb-${idx}`,
onClick: () => {
slider.moveToSlideRelative(idx)
},
},
}
}
return child
})}
</div>
)}
</a.div>
</div>
)
}

View File

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

View File

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

View File

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

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

View 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

View File

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

View File

@ -1,96 +1,60 @@
.root {
@apply relative grid items-start gap-8 grid-cols-1 overflow-x-hidden;
@screen lg {
@apply grid-cols-12;
}
@apply relative grid items-start gap-1 grid-cols-1 overflow-x-hidden;
min-height: auto;
}
.productDisplay {
@apply relative flex px-0 pb-0 box-border col-span-1 bg-violet;
min-height: 600px;
@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;
}
}
.main {
@apply relative px-0 pb-0 box-border flex flex-col col-span-1;
min-height: 500px;
}
.sidebar {
@apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 w-full h-full;
@screen lg {
@apply col-span-6 py-24 justify-between;
}
@apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 py-6 w-full h-full;
}
.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 {
& > div {
@apply h-full;
& > div {
@apply h-full;
}
}
@apply text-center;
}
.img {
.imageContainer > div,
.imageContainer > div > div {
@apply h-full;
}
.sliderContainer .img {
@apply w-full h-auto max-h-full object-cover;
}
.button {
text-align: center;
width: 100%;
max-width: 300px;
@screen sm {
min-width: 300px;
}
}
.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 {
@apply right-12 text-white bg-violet;
.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;
}
}

View File

@ -1,64 +1,90 @@
import cn from 'classnames'
import Image from 'next/image'
import { NextSeo } from 'next-seo'
import { FC, useEffect, useState } from 'react'
import s from './ProductView.module.css'
import { Swatch, ProductSlider } from '@components/product'
import { Button, Container, Text, useUI } from '@components/ui'
import { FC } from 'react'
import type { Product } from '@commerce/types/product'
import usePrice from '@framework/product/use-price'
import { useAddItem } from '@framework/cart'
import { getVariant, SelectedOptions } from '../helpers'
import WishlistButton from '@components/wishlist/WishlistButton'
interface Props {
children?: any
import { WishlistButton } from '@components/wishlist'
import { ProductSlider, ProductCard } from '@components/product'
import { Container, Text } from '@components/ui'
import ProductSidebar from '../ProductSidebar'
import ProductTag from '../ProductTag'
interface ProductViewProps {
product: Product
className?: string
relatedProducts: Product[]
}
const ProductView: FC<Props> = ({ product }) => {
// TODO: fix this missing argument issue
/* @ts-ignore */
const addItem = useAddItem()
const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
const { price } = usePrice({
amount: product.price.value,
baseAmount: product.price.retailPrice,
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 (
<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
title={product.name}
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>
</>
)
}

View File

@ -1,11 +1,13 @@
.swatch {
box-sizing: border-box;
composes: root from 'components/ui/Button/Button.module.css';
@apply h-12 w-12 bg-primary text-primary rounded-full mr-3 inline-flex
composes: root from '@components/ui/Button/Button.module.css';
@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
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);
overflow: hidden;
width: 48px;
height: 48px;
}
.swatch::before,
@ -35,7 +37,7 @@
}
.active {
@apply border-accents-9 border-2;
@apply border-accent-9 border-2;
padding-right: 1px;
padding-left: 1px;
}
@ -46,7 +48,7 @@
}
.active.textLabel {
@apply border-accents-9 border-2;
@apply border-accent-9 border-2;
padding-right: calc(1rem - 1px);
padding-left: calc(1rem - 1px);
}

View File

@ -1,5 +1,5 @@
import cn from 'classnames'
import { FC } from 'react'
import React from 'react'
import s from './Swatch.module.css'
import { Check } from '@components/icons'
import Button, { ButtonProps } from '@components/ui/Button'
@ -13,48 +13,50 @@ interface SwatchProps {
label?: string | null
}
const Swatch: FC<Omit<ButtonProps, 'variant'> & SwatchProps> = ({
className,
color = '',
label = null,
variant = 'size',
active,
...props
}) => {
variant = variant?.toLowerCase()
const Swatch: React.FC<Omit<ButtonProps, 'variant'> & SwatchProps> = React.memo(
({
active,
className,
color = '',
label = null,
variant = 'size',
...props
}) => {
variant = variant?.toLowerCase()
if (label) {
label = label?.toLowerCase()
if (label) {
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

View File

@ -1,7 +1,8 @@
import type { Product } from '@commerce/types/product'
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) => {
return Object.entries(opts).every(([key, value]) =>
variant.options.find((option) => {
@ -16,3 +17,16 @@ export function getVariant(product: Product, opts: SelectedOptions) {
})
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(),
}))
})
}

View File

@ -2,3 +2,4 @@ export { default as Swatch } from './Swatch'
export { default as ProductView } from './ProductView'
export { default as ProductCard } from './ProductCard'
export { default as ProductSlider } from './ProductSlider'
export { default as ProductOptions } from './ProductOptions'

439
components/search.tsx Normal file
View 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

View File

@ -1,9 +1,14 @@
.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 {
@apply bg-accents-0 text-primary border border-secondary;
@apply border-accent-9 bg-accent-6;
}
.root:focus {
@ -11,22 +16,33 @@
}
.root[data-active] {
@apply bg-gray-600;
@apply bg-accent-6;
}
.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 {
@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: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);
-webkit-transform: translateZ(0);
-webkit-perspective: 1000;
-webkit-backface-visibility: hidden;
}
.progress {
}

View File

@ -12,7 +12,7 @@ import { LoadingDots } from '@components/ui'
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
href?: string
className?: string
variant?: 'flat' | 'slim'
variant?: 'flat' | 'slim' | 'ghost'
active?: boolean
type?: 'submit' | 'reset' | 'button'
Component?: string | JSXElementConstructor<any>
@ -39,6 +39,7 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
const rootClassName = cn(
s.root,
{
[s.ghost]: variant === 'ghost',
[s.slim]: variant === 'slim',
[s.loading]: loading,
[s.disabled]: disabled,

View 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);
}

View 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

View File

@ -0,0 +1,2 @@
export { default } from './Collapse'
export * from './Collapse'

View File

@ -1,14 +1,19 @@
import cn from 'classnames'
import React, { FC } from 'react'
interface Props {
interface ContainerProps {
className?: string
children?: any
el?: HTMLElement
clean?: boolean
}
const Container: FC<Props> = ({ children, className, el = 'div', clean }) => {
const Container: FC<ContainerProps> = ({
children,
className,
el = 'div',
clean,
}) => {
const rootClassName = cn(className, {
'mx-auto max-w-8xl px-6': !clean,
})

View File

@ -1,7 +1,9 @@
.root {
--row-height: calc(100vh - 88px);
@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;
@ -15,17 +17,6 @@
}
}
@screen lg {
.root {
@apply grid-cols-3 grid-rows-2;
}
.root & > * {
@apply col-span-1;
height: inherit;
}
}
.default {
& > * {
@apply bg-transparent;
@ -34,9 +25,17 @@
.layoutNormal {
@apply gap-3;
}
& > * {
min-height: 325px;
@screen md {
.layoutNormal > * {
max-height: min-content !important;
}
}
@screen lg {
.layoutNormal > * {
max-height: 400px;
}
}
@ -52,13 +51,12 @@
}
&.filled {
& > *:nth-child(6n + 1),
& > *:nth-child(6n + 5) {
& > *:nth-child(6n + 1) {
@apply bg-violet;
}
& > *:nth-child(6n + 5) {
@apply bg-blue;
& > *:nth-child(6n + 2) {
@apply bg-accent-8;
}
& > *:nth-child(6n + 3) {
@ -83,12 +81,12 @@
}
&.filled {
& > *:nth-child(6n + 2) {
@apply bg-blue;
& > *:nth-child(6n + 1) {
@apply bg-violet;
}
& > *:nth-child(6n + 4) {
@apply bg-violet;
& > *:nth-child(6n + 2) {
@apply bg-accent-8;
}
& > *:nth-child(6n + 3) {

View File

@ -2,14 +2,14 @@ import cn from 'classnames'
import { FC, ReactNode, Component } from 'react'
import s from './Grid.module.css'
interface Props {
interface GridProps {
className?: string
children?: ReactNode[] | Component[] | any[]
layout?: 'A' | 'B' | 'C' | 'D' | 'normal'
variant?: 'default' | 'filled'
}
const Grid: FC<Props> = ({
const Grid: FC<GridProps> = ({
className,
layout = 'A',
children,

View File

@ -1,9 +1,30 @@
.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 {
@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;
}
}

View File

@ -1,30 +1,26 @@
import React, { FC } from 'react'
import { Container } from '@components/ui'
import { RightArrow } from '@components/icons'
import { ArrowRight } from '@components/icons'
import s from './Hero.module.css'
import Link from 'next/link'
interface Props {
interface HeroProps {
className?: string
headline: string
description: string
}
const Hero: FC<Props> = ({ headline, description }) => {
const Hero: FC<HeroProps> = ({ headline, description }) => {
return (
<div className="bg-black">
<div className="bg-accent-9 border-b border-t border-accent-2">
<Container>
<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">
{headline}
</h2>
<div className="flex flex-col justify-between">
<p className="mt-5 text-xl leading-7 text-accent-2 text-white">
{description}
</p>
<h2 className={s.title}>{headline}</h2>
<div className={s.description}>
<p>{description}</p>
<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
<RightArrow width="20" heigh="20" className="ml-1" />
<ArrowRight width="20" heigh="20" className="ml-1" />
</a>
</Link>
</div>

View File

@ -1,5 +1,5 @@
.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 {

View File

@ -2,12 +2,12 @@ import cn from 'classnames'
import s from './Input.module.css'
import React, { InputHTMLAttributes } from 'react'
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
className?: string
onChange?: (...args: any[]) => any
}
const Input: React.FC<Props> = (props) => {
const Input: React.FC<InputProps> = (props) => {
const { className, children, onChange, ...rest } = props
const rootClassName = cn(s.root, {}, className)

View File

@ -1,22 +1,23 @@
.root {
@apply inline-flex text-center items-center leading-7;
}
& span {
@apply bg-accents-6 rounded-full h-2 w-2;
animation-name: blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
margin: 0 2px;
.root .dot {
@apply rounded-full h-2 w-2;
background-color: currentColor;
animation-name: blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
margin: 0 2px;
}
&:nth-of-type(2) {
animation-delay: 0.2s;
}
.root .dot:nth-of-type(2) {
animation-delay: 0.2s;
}
&:nth-of-type(3) {
animation-delay: 0.4s;
}
}
.root .dot::nth-of-type(3) {
animation-delay: 0.4s;
}
@keyframes blink {

View File

@ -3,9 +3,9 @@ import s from './LoadingDots.module.css'
const LoadingDots: React.FC = () => {
return (
<span className={s.root}>
<span />
<span />
<span />
<span className={s.dot} key={`dot_1`} />
<span className={s.dot} key={`dot_2`} />
<span className={s.dot} key={`dot_3`} />
</span>
)
}

View File

@ -1,22 +1,22 @@
.root {
@apply w-full relative;
height: 320px;
min-width: 100%;
@apply w-full min-w-full relative flex flex-row items-center overflow-hidden py-0;
max-height: 320px;
}
.container {
@apply flex flex-row items-center;
.root > div {
max-height: 320px;
padding: 0;
margin: 0;
}
.container > * {
@apply relative flex-1 px-16 py-4 h-full;
min-height: 320px;
.root > div > * > *:nth-child(2) * {
max-height: 100%;
}
.primary {
@apply bg-white;
@apply bg-accent-0;
}
.secondary {
@apply bg-black;
@apply bg-accent-9;
}

View File

@ -1,15 +1,15 @@
import cn from 'classnames'
import s from './Marquee.module.css'
import { FC, ReactNode, Component } from 'react'
import Ticker from 'react-ticker'
import { FC, ReactNode, Component, Children } from 'react'
import { default as FastMarquee } from 'react-fast-marquee'
interface Props {
interface MarqueeProps {
className?: string
children?: ReactNode[] | Component[] | any[]
variant?: 'primary' | 'secondary'
}
const Marquee: FC<Props> = ({
const Marquee: FC<MarqueeProps> = ({
className = '',
children,
variant = 'primary',
@ -24,11 +24,15 @@ const Marquee: FC<Props> = ({
)
return (
<div className={rootClassName}>
<Ticker offset={80}>
{() => <div className={s.container}>{children}</div>}
</Ticker>
</div>
<FastMarquee gradient={false} className={rootClassName}>
{Children.map(children, (child) => ({
...child,
props: {
...child.props,
className: cn(child.props.className, `${variant}`),
},
}))}
</FastMarquee>
)
}

View File

@ -1,12 +1,17 @@
.root {
@apply fixed bg-primary text-primary flex items-center inset-0 z-50 justify-center;
background-color: rgba(0, 0, 0, 0.35);
@apply fixed bg-black bg-opacity-40 flex items-center inset-0 z-50 justify-center;
backdrop-filter: blur(0.8px);
-webkit-backdrop-filter: blur(0.8px);
}
.modal {
@apply bg-primary p-12 border border-accents-2 relative;
@apply bg-primary p-12 border border-accent-2 relative;
}
.modal:focus {
@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;
}

View File

@ -1,22 +1,20 @@
import { FC, useRef, useEffect, useCallback } from 'react'
import Portal from '@reach/portal'
import s from './Modal.module.css'
import FocusTrap from '@lib/focus-trap'
import { Cross } from '@components/icons'
import {
disableBodyScroll,
enableBodyScroll,
clearAllBodyScrollLocks,
enableBodyScroll,
} from 'body-scroll-lock'
import FocusTrap from '@lib/focus-trap'
interface Props {
interface ModalProps {
className?: string
children?: any
open?: boolean
onClose: () => void
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 handleKey = useCallback(
@ -30,36 +28,31 @@ const Modal: FC<Props> = ({ children, open, onClose, onEnter = null }) => {
useEffect(() => {
if (ref.current) {
if (open) {
disableBodyScroll(ref.current)
window.addEventListener('keydown', handleKey)
} else {
enableBodyScroll(ref.current)
}
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
window.addEventListener('keydown', handleKey)
}
return () => {
window.removeEventListener('keydown', handleKey)
if (ref && ref.current) {
enableBodyScroll(ref.current)
}
clearAllBodyScrollLocks()
window.removeEventListener('keydown', handleKey)
}
}, [open, handleKey])
}, [handleKey])
return (
<Portal>
{open ? (
<div className={s.root}>
<div className={s.modal} role="dialog" ref={ref}>
<button
onClick={() => onClose()}
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>
<FocusTrap focusFirst>{children}</FocusTrap>
</div>
</div>
) : null}
</Portal>
<div className={s.root}>
<div className={s.modal} role="dialog" ref={ref}>
<button
onClick={() => onClose()}
aria-label="Close panel"
className={s.close}
>
<Cross className="h-6 w-6" />
</button>
<FocusTrap focusFirst>{children}</FocusTrap>
</div>
</div>
)
}

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

View 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

View File

@ -0,0 +1,2 @@
export { default } from './Quantity'
export * from './Quantity'

View File

View 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

View File

@ -0,0 +1,2 @@
export { default } from './Rating'
export * from './Rating'

View File

@ -1,3 +1,14 @@
.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);
}

View File

@ -1,54 +1,45 @@
import s from './Sidebar.module.css'
import Portal from '@reach/portal'
import { FC, useEffect, useRef } from 'react'
import s from './Sidebar.module.css'
import cn from 'classnames'
import {
disableBodyScroll,
enableBodyScroll,
clearAllBodyScrollLocks,
} from 'body-scroll-lock'
interface Props {
interface SidebarProps {
children: any
open: boolean
onClose: () => void
}
const Sidebar: FC<Props> = ({ children, open = false, onClose }) => {
const Sidebar: FC<SidebarProps> = ({ children, onClose }) => {
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
useEffect(() => {
if (ref.current) {
if (open) {
disableBodyScroll(ref.current)
} else {
enableBodyScroll(ref.current)
}
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
}
return () => {
if (ref && ref.current) {
enableBodyScroll(ref.current)
}
clearAllBodyScrollLocks()
}
}, [open])
}, [])
return (
<Portal>
{open ? (
<div className={s.root} ref={ref}>
<div className="absolute inset-0 overflow-hidden">
<div
className="absolute inset-0 bg-black bg-opacity-50 transition-opacity"
onClick={onClose}
/>
<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 className={cn(s.root)}>
<div className="absolute inset-0 overflow-hidden">
<div className={s.backdrop} onClick={onClose} />
<section className="absolute inset-y-0 right-0 max-w-full flex outline-none pl-10">
<div className="h-full w-full md:w-screen md:max-w-md">
<div className={s.sidebar} ref={ref}>
{children}
</div>
</div>
</div>
) : null}
</Portal>
</section>
</div>
</div>
)
}

View File

@ -2,10 +2,10 @@
@apply block;
background-image: linear-gradient(
270deg,
var(--accents-1),
var(--accents-2),
var(--accents-2),
var(--accents-1)
var(--accent-0),
var(--accent-2),
var(--accent-0),
var(--accent-1)
);
background-size: 400% 100%;
animation: loading 8s ease-in-out infinite;
@ -28,10 +28,10 @@
z-index: 100;
background-image: linear-gradient(
270deg,
var(--accents-1),
var(--accents-2),
var(--accents-2),
var(--accents-1)
var(--accent-0),
var(--accent-2),
var(--accent-0),
var(--accent-1)
);
background-size: 400% 100%;
animation: loading 8s ease-in-out infinite;

View File

@ -3,17 +3,17 @@ import cn from 'classnames'
import px from '@lib/to-pixels'
import s from './Skeleton.module.css'
interface Props {
width?: string | number
height?: string | number
boxHeight?: string | number
style?: CSSProperties
interface SkeletonProps {
show?: boolean
block?: boolean
className?: string
style?: CSSProperties
width?: string | number
height?: string | number
boxHeight?: string | number
}
const Skeleton: React.FC<Props> = ({
const Skeleton: React.FC<SkeletonProps> = ({
style,
width,
height,

Some files were not shown because too many files have changed in this diff Show More