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` Every provider defines the features that it supports under `framework/{provider}/commerce.config.json`
#### Features Available
- wishlist
- customCheckout
#### How to turn Features on and off #### How to turn Features on and off
> NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out the box) > NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out the box)
@ -73,7 +78,8 @@ Every provider defines the features that it supports under `framework/{provider}
```json ```json
{ {
"features": { "features": {
"wishlist": false "wishlist": false,
"customCheckout": true
} }
} }
``` ```

View File

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

View File

@ -61,7 +61,7 @@ const ForgotPassword: FC<Props> = () => {
</div> </div>
<span className="pt-3 text-center text-sm"> <span className="pt-3 text-center text-sm">
<span className="text-accents-7">Do you have an account?</span> <span className="text-accent-7">Do you have an account?</span>
{` `} {` `}
<a <a
className="text-accent-9 font-bold hover:underline cursor-pointer" className="text-accent-9 font-bold hover:underline cursor-pointer"

View File

@ -87,7 +87,7 @@ const LoginView: FC<Props> = () => {
Log In Log In
</Button> </Button>
<div className="pt-1 text-center text-sm"> <div className="pt-1 text-center text-sm">
<span className="text-accents-7">Don't have an account?</span> <span className="text-accent-7">Don't have an account?</span>
{` `} {` `}
<a <a
className="text-accent-9 font-bold hover:underline cursor-pointer" className="text-accent-9 font-bold hover:underline cursor-pointer"

View File

@ -76,7 +76,7 @@ const SignUpView: FC<Props> = () => {
<Input placeholder="Last Name" onChange={setLastName} /> <Input placeholder="Last Name" onChange={setLastName} />
<Input type="email" placeholder="Email" onChange={setEmail} /> <Input type="email" placeholder="Email" onChange={setEmail} />
<Input type="password" placeholder="Password" onChange={setPassword} /> <Input type="password" placeholder="Password" onChange={setPassword} />
<span className="text-accents-8"> <span className="text-accent-8">
<span className="inline-block align-middle "> <span className="inline-block align-middle ">
<Info width="15" height="15" /> <Info width="15" height="15" />
</span>{' '} </span>{' '}
@ -97,7 +97,7 @@ const SignUpView: FC<Props> = () => {
</div> </div>
<span className="pt-1 text-center text-sm"> <span className="pt-1 text-center text-sm">
<span className="text-accents-7">Do you have an account?</span> <span className="text-accent-7">Do you have an account?</span>
{` `} {` `}
<a <a
className="text-accent-9 font-bold hover:underline cursor-pointer" className="text-accent-9 font-bold hover:underline cursor-pointer"

View File

@ -1,6 +1,14 @@
.root {
@apply flex flex-col py-4;
}
.root:first-child {
padding-top: 0;
}
.quantity { .quantity {
appearance: textfield; appearance: textfield;
@apply w-8 border-accents-2 border mx-3 rounded text-center text-sm text-black; @apply w-8 border-accent-2 border mx-3 rounded text-center text-sm text-black;
} }
.quantity::-webkit-outer-spin-button, .quantity::-webkit-outer-spin-button,
@ -15,4 +23,10 @@
height: 100%; height: 100%;
left: 30% !important; left: 30% !important;
top: 30% !important; top: 30% !important;
z-index: 1;
}
.productName {
@apply font-medium cursor-pointer pb-1;
margin-top: -4px;
} }

View File

@ -1,14 +1,15 @@
import { ChangeEvent, useEffect, useState } from 'react' import { ChangeEvent, FocusEventHandler, useEffect, useState } from 'react'
import cn from 'classnames' import cn from 'classnames'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import s from './CartItem.module.css' import s from './CartItem.module.css'
import { Trash, Plus, Minus } from '@components/icons' import { Trash, Plus, Minus, Cross } from '@components/icons'
import { useUI } from '@components/ui/context' import { useUI } from '@components/ui/context'
import type { LineItem } from '@commerce/types/cart' import type { LineItem } from '@commerce/types/cart'
import usePrice from '@framework/product/use-price' import usePrice from '@framework/product/use-price'
import useUpdateItem from '@framework/cart/use-update-item' import useUpdateItem from '@framework/cart/use-update-item'
import useRemoveItem from '@framework/cart/use-remove-item' import useRemoveItem from '@framework/cart/use-remove-item'
import Quantity from '@components/ui/Quantity'
type ItemOption = { type ItemOption = {
name: string name: string
@ -19,13 +20,19 @@ type ItemOption = {
const CartItem = ({ const CartItem = ({
item, item,
variant = 'default',
currencyCode, currencyCode,
...rest ...rest
}: { }: {
variant?: 'default' | 'display'
item: LineItem item: LineItem
currencyCode: string currencyCode: string
}) => { }) => {
const { closeSidebarIfPresent } = useUI() const { closeSidebarIfPresent } = useUI()
const [removing, setRemoving] = useState(false)
const [quantity, setQuantity] = useState<number>(item.quantity)
const removeItem = useRemoveItem()
const updateItem = useUpdateItem({ item })
const { price } = usePrice({ const { price } = usePrice({
amount: item.variant.price * item.quantity, amount: item.variant.price * item.quantity,
@ -33,48 +40,28 @@ const CartItem = ({
currencyCode, currencyCode,
}) })
const updateItem = useUpdateItem({ item }) const handleChange = async ({
const removeItem = useRemoveItem() target: { value },
const [quantity, setQuantity] = useState<number | ''>(item.quantity) }: ChangeEvent<HTMLInputElement>) => {
const [removing, setRemoving] = useState(false) setQuantity(Number(value))
await updateItem({ quantity: Number(value) })
}
const updateQuantity = async (val: number) => { const increaseQuantity = async (n = 1) => {
const val = Number(quantity) + n
setQuantity(val)
await updateItem({ quantity: val }) await updateItem({ quantity: val })
} }
const handleQuantity = (e: ChangeEvent<HTMLInputElement>) => {
const val = !e.target.value ? '' : Number(e.target.value)
if (!val || (Number.isInteger(val) && val >= 0)) {
setQuantity(val)
}
}
const handleBlur = () => {
const val = Number(quantity)
if (val !== item.quantity) {
updateQuantity(val)
}
}
const increaseQuantity = (n = 1) => {
const val = Number(quantity) + n
if (Number.isInteger(val) && val >= 0) {
setQuantity(val)
updateQuantity(val)
}
}
const handleRemove = async () => { const handleRemove = async () => {
setRemoving(true) setRemoving(true)
try { try {
// If this action succeeds then there's no need to do `setRemoving(true)`
// because the component will be removed from the view
await removeItem(item) await removeItem(item)
} catch (error) { } catch (error) {
setRemoving(false) setRemoving(false)
} }
} }
// TODO: Add a type for this // TODO: Add a type for this
const options = (item as any).options const options = (item as any).options
@ -87,79 +74,76 @@ const CartItem = ({
return ( return (
<li <li
className={cn('flex flex-row space-x-8 py-8', { className={cn(s.root, {
'opacity-75 pointer-events-none': removing, 'opacity-50 pointer-events-none': removing,
})} })}
{...rest} {...rest}
> >
<div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer"> <div className="flex flex-row space-x-4 py-4">
<Link href={`/product/${item.path}`}> <div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer z-0">
<Image <Link href={`/product/${item.path}`}>
onClick={() => closeSidebarIfPresent()} <Image
className={s.productImage} onClick={() => closeSidebarIfPresent()}
width={150} className={s.productImage}
height={150} width={150}
src={item.variant.image!.url} height={150}
alt={item.variant.image!.altText} src={item.variant.image!.url}
unoptimized alt={item.variant.image!.altText}
/> unoptimized
</Link> />
</div> </Link>
<div className="flex-1 flex flex-col text-base"> </div>
<Link href={`/product/${item.path}`}> <div className="flex-1 flex flex-col text-base">
<span <Link href={`/product/${item.path}`}>
onClick={() => closeSidebarIfPresent()} <span
> className={s.productName}
<div onClick={() => closeSidebarIfPresent()}
className="font-bold text-lg cursor-pointer leading-6"
> >
{item.name} {item.name}
</span>
</Link>
{options && options.length > 0 && (
<div className="flex items-center pb-1">
{options.map((option: ItemOption, i: number) => (
<div
key={`${item.id}-${option.name}`}
className="text-sm font-semibold text-accent-7 inline-flex items-center justify-center"
>
{option.name}
{option.name === 'Color' ? (
<span
className="mx-2 rounded-full bg-transparent border w-5 h-5 p-1 text-accent-9 inline-flex items-center justify-center overflow-hidden"
style={{
backgroundColor: `${option.value}`,
}}
></span>
) : (
<span className="mx-2 rounded-full bg-transparent border h-5 p-1 text-accent-9 inline-flex items-center justify-center overflow-hidden">
{option.value}
</span>
)}
{i === options.length - 1 ? '' : <span className="mr-3" />}
</div>
))}
</div> </div>
{item.variant ? <span> {item.variant.name}</span> : ""} )}
</span> {variant === 'display' && (
</Link> <div className="text-sm tracking-wider">{quantity}x</div>
{options && options.length > 0 ? ( )}
<div className=""> </div>
{options.map((option: ItemOption, i: number) => ( <div className="flex flex-col justify-between space-y-2 text-sm">
<span <span>{price}</span>
key={`${item.id}-${option.name}`}
className="text-sm font-semibold text-accents-7"
>
{option.value}
{i === options.length - 1 ? '' : ', '}
</span>
))}
</div>
) : null}
<div className="flex items-center mt-3">
<button type="button" onClick={() => increaseQuantity(-1)}>
<Minus width={18} height={18} />
</button>
<label>
<input
type="number"
max={99}
min={0}
className={s.quantity}
value={quantity}
onChange={handleQuantity}
onBlur={handleBlur}
/>
</label>
<button type="button" onClick={() => increaseQuantity(1)}>
<Plus width={18} height={18} />
</button>
</div> </div>
</div> </div>
<div className="flex flex-col justify-between space-y-2 text-base"> {variant === 'default' && (
<span>{price}</span> <Quantity
<button value={quantity}
className="flex justify-end outline-none" handleRemove={handleRemove}
onClick={handleRemove} handleChange={handleChange}
> increase={() => increaseQuantity(1)}
<Trash /> decrease={() => increaseQuantity(-1)}
</button> />
</div> )}
</li> </li>
) )
} }

View File

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

View File

@ -1,17 +1,17 @@
import { FC } from 'react'
import cn from 'classnames' import cn from 'classnames'
import Link from 'next/link' import Link from 'next/link'
import CartItem from '../CartItem' import { FC } from 'react'
import s from './CartSidebarView.module.css' import s from './CartSidebarView.module.css'
import { Button } from '@components/ui' import CartItem from '../CartItem'
import { UserNav } from '@components/common' import { Button, Text } from '@components/ui'
import { useUI } from '@components/ui/context' import { useUI } from '@components/ui/context'
import { Bag, Cross, Check } from '@components/icons' import { Bag, Cross, Check } from '@components/icons'
import useCart from '@framework/cart/use-cart' import useCart from '@framework/cart/use-cart'
import usePrice from '@framework/product/use-price' import usePrice from '@framework/product/use-price'
import SidebarLayout from '@components/common/SidebarLayout'
const CartSidebarView: FC = () => { const CartSidebarView: FC = () => {
const { closeSidebar } = useUI() const { closeSidebar, setSidebarView } = useUI()
const { data, isLoading, isEmpty } = useCart() const { data, isLoading, isEmpty } = useCart()
const { price: subTotal } = usePrice( const { price: subTotal } = usePrice(
@ -27,33 +27,18 @@ const CartSidebarView: FC = () => {
} }
) )
const handleClose = () => closeSidebar() const handleClose = () => closeSidebar()
const goToCheckout = () => setSidebarView('CHECKOUT_VIEW')
const error = null const error = null
const success = null const success = null
return ( return (
<div <SidebarLayout
className={cn(s.root, { className={cn({
[s.empty]: error || success || isLoading || isEmpty, [s.empty]: error || success || isLoading || isEmpty,
})} })}
handleClose={handleClose}
> >
<header className="px-4 pt-6 pb-4 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="h-7 flex items-center">
<button
onClick={handleClose}
aria-label="Close panel"
className="hover:text-gray-500 transition ease-in-out duration-150"
>
<Cross className="h-6 w-6" />
</button>
</div>
<div className="space-y-1">
<UserNav />
</div>
</div>
</header>
{isLoading || isEmpty ? ( {isLoading || isEmpty ? (
<div className="flex-1 px-4 flex flex-col justify-center items-center"> <div className="flex-1 px-4 flex flex-col justify-center items-center">
<span className="border border-dashed border-primary rounded-full flex items-center justify-center w-16 h-16 p-12 bg-secondary text-secondary"> <span className="border border-dashed border-primary rounded-full flex items-center justify-center w-16 h-16 p-12 bg-secondary text-secondary">
@ -62,7 +47,7 @@ const CartSidebarView: FC = () => {
<h2 className="pt-6 text-2xl font-bold tracking-wide text-center"> <h2 className="pt-6 text-2xl font-bold tracking-wide text-center">
Your cart is empty Your cart is empty
</h2> </h2>
<p className="text-accents-3 px-10 text-center pt-2"> <p className="text-accent-3 px-10 text-center pt-2">
Biscuit oat cake wafer icing ice cream tiramisu pudding cupcake. Biscuit oat cake wafer icing ice cream tiramisu pudding cupcake.
</p> </p>
</div> </div>
@ -89,14 +74,11 @@ const CartSidebarView: FC = () => {
<> <>
<div className="px-4 sm:px-6 flex-1"> <div className="px-4 sm:px-6 flex-1">
<Link href="/cart"> <Link href="/cart">
<h2 <Text variant="sectionHeading" onClick={handleClose}>
className="pt-1 pb-4 text-2xl leading-7 font-bold text-base tracking-wide cursor-pointer inline-block"
onClick={handleClose}
>
My Cart My Cart
</h2> </Text>
</Link> </Link>
<ul className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accents-3 border-t border-accents-3"> <ul className={s.lineItemsList}>
{data!.lineItems.map((item: any) => ( {data!.lineItems.map((item: any) => (
<CartItem <CartItem
key={item.id} key={item.id}
@ -107,34 +89,40 @@ const CartSidebarView: FC = () => {
</ul> </ul>
</div> </div>
<div className="flex-shrink-0 px-4 py-5 sm:px-6"> <div className="flex-shrink-0 px-6 py-6 sm:px-6 sticky z-20 bottom-0 w-full right-0 left-0 bg-accent-0 border-t text-sm">
<div className="border-t border-accents-3"> <ul className="pb-2">
<ul className="py-3"> <li className="flex justify-between py-1">
<li className="flex justify-between py-1"> <span>Subtotal</span>
<span>Subtotal</span> <span>{subTotal}</span>
<span>{subTotal}</span> </li>
</li> <li className="flex justify-between py-1">
<li className="flex justify-between py-1"> <span>Taxes</span>
<span>Taxes</span> <span>Calculated at checkout</span>
<span>Calculated at checkout</span> </li>
</li> <li className="flex justify-between py-1">
<li className="flex justify-between py-1"> <span>Shipping</span>
<span>Estimated Shipping</span> <span className="font-bold tracking-wide">FREE</span>
<span className="font-bold tracking-wide">FREE</span> </li>
</li> </ul>
</ul> <div className="flex justify-between border-t border-accent-2 py-3 font-bold mb-2">
<div className="flex justify-between border-t border-accents-3 py-3 font-bold mb-10"> <span>Total</span>
<span>Total</span> <span>{total}</span>
<span>{total}</span> </div>
</div> <div>
{process.env.COMMERCE_CUSTOMCHECKOUT_ENABLED ? (
<Button Component="a" width="100%" onClick={goToCheckout}>
Proceed to Checkout ({total})
</Button>
) : (
<Button href="/checkout" Component="a" width="100%">
Proceed to Checkout
</Button>
)}
</div> </div>
<Button href="/checkout" Component="a" width="100%">
Proceed to Checkout
</Button>
</div> </div>
</> </>
)} )}
</div> </SidebarLayout>
) )
} }

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 <div
ref={ref} ref={ref}
style={{ backgroundImage: userAvatar }} style={{ backgroundImage: userAvatar }}
className="inline-block h-8 w-8 rounded-full border-2 border-primary hover:border-secondary focus:border-secondary transition linear-out duration-150" className="inline-block h-8 w-8 rounded-full border-2 border-primary hover:border-secondary focus:border-secondary transition-colors ease-linear"
> >
{/* Add an image - We're generating a gradient as placeholder <img></img> */} {/* Add an image - We're generating a gradient as placeholder <img></img> */}
</div> </div>

View File

@ -1,9 +1,7 @@
.root { .root {
@apply text-center p-6 bg-primary text-sm flex-row justify-center items-center font-medium fixed bottom-0 w-full z-30 transition-all duration-300 ease-out; @apply text-center p-6 bg-primary text-sm flex-row justify-center items-center font-medium fixed bottom-0 w-full z-30 transition-all duration-300 ease-out;
}
@screen md { @screen md {
.root {
@apply flex text-left; @apply flex text-left;
} }
} }

View File

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

View File

@ -15,65 +15,50 @@ interface Props {
pages?: Page[] pages?: Page[]
} }
const LEGAL_PAGES = ['terms-of-use', 'shipping-returns', 'privacy-policy'] const links = [
{
name: 'Home',
url: '/',
},
]
const Footer: FC<Props> = ({ className, pages }) => { const Footer: FC<Props> = ({ className, pages }) => {
const { sitePages, legalPages } = usePages(pages) const { sitePages } = usePages(pages)
const rootClassName = cn(className) const rootClassName = cn(s.root, className)
return ( return (
<footer className={rootClassName}> <footer className={rootClassName}>
<Container> <Container>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 border-b border-accents-2 py-12 text-primary bg-primary transition-colors duration-150"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 border-b border-accent-2 py-12 text-primary bg-primary transition-colors duration-150">
<div className="col-span-1 lg:col-span-2"> <div className="col-span-1 lg:col-span-2">
<Link href="/"> <Link href="/">
<a className="flex flex-initial items-center font-bold md:mr-24"> <a className="flex flex-initial items-center font-bold md:mr-24">
<span className="rounded-full border border-gray-700 mr-2"> <span className="rounded-full border border-accent-6 mr-2">
<Logo /> <Logo />
</span> </span>
<span>ACME</span> <span>ACME</span>
</a> </a>
</Link> </Link>
</div> </div>
<div className="col-span-1 lg:col-span-2"> <div className="col-span-1 lg:col-span-8">
<ul className="flex flex-initial flex-col md:flex-1"> <div className="grid md:grid-rows-4 md:grid-cols-3 md:grid-flow-col">
<li className="py-3 md:py-0 md:pb-4"> {[...links, ...sitePages].map((page) => (
<Link href="/"> <span key={page.url} className="py-3 md:py-0 md:pb-4">
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
Home
</a>
</Link>
</li>
{sitePages.map((page) => (
<li key={page.url} className="py-3 md:py-0 md:pb-4">
<Link href={page.url!}> <Link href={page.url!}>
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150"> <a className="text-accent-9 hover:text-accent-6 transition ease-in-out duration-150">
{page.name} {page.name}
</a> </a>
</Link> </Link>
</li> </span>
))} ))}
</ul> </div>
</div> </div>
<div className="col-span-1 lg:col-span-2"> <div className="col-span-1 lg:col-span-2 flex items-start lg:justify-end text-primary">
<ul className="flex flex-initial flex-col md:flex-1">
{legalPages.map((page) => (
<li key={page.url} className="py-3 md:py-0 md:pb-4">
<Link href={page.url!}>
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
{page.name}
</a>
</Link>
</li>
))}
</ul>
</div>
<div className="col-span-1 lg:col-span-6 flex items-start lg:justify-end text-primary">
<div className="flex space-x-6 items-center h-10"> <div className="flex space-x-6 items-center h-10">
<a <a
className={s.link}
aria-label="Github Repository" aria-label="Github Repository"
href="https://github.com/vercel/commerce" href="https://github.com/vercel/commerce"
className={s.link}
> >
<Github /> <Github />
</a> </a>
@ -81,12 +66,12 @@ const Footer: FC<Props> = ({ className, pages }) => {
</div> </div>
</div> </div>
</div> </div>
<div className="py-12 flex flex-col md:flex-row justify-between items-center space-y-4"> <div className="pt-6 pb-10 flex flex-col md:flex-row justify-between items-center space-y-4 text-accent-6 text-sm">
<div> <div>
<span>&copy; 2020 ACME, Inc. All rights reserved.</span> <span>&copy; 2020 ACME, Inc. All rights reserved.</span>
</div> </div>
<div className="flex items-center text-primary"> <div className="flex items-center text-primary text-sm">
<span className="text-primary">Crafted by</span> <span className="text-primary">Created by</span>
<a <a
rel="noopener" rel="noopener"
href="https://vercel.com" href="https://vercel.com"
@ -95,7 +80,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
className="text-primary" className="text-primary"
> >
<Vercel <Vercel
className="inline-block h-6 ml-4 text-primary" className="inline-block h-6 ml-3 text-primary"
alt="Vercel.com Logo" alt="Vercel.com Logo"
/> />
</a> </a>
@ -109,34 +94,21 @@ const Footer: FC<Props> = ({ className, pages }) => {
function usePages(pages?: Page[]) { function usePages(pages?: Page[]) {
const { locale } = useRouter() const { locale } = useRouter()
const sitePages: Page[] = [] const sitePages: Page[] = []
const legalPages: Page[] = []
if (pages) { if (pages) {
pages.forEach((page) => { pages.forEach((page) => {
const slug = page.url && getSlug(page.url) const slug = page.url && getSlug(page.url)
if (!slug) return if (!slug) return
if (locale && !slug.startsWith(`${locale}/`)) return if (locale && !slug.startsWith(`${locale}/`)) return
sitePages.push(page)
if (isLegalPage(slug, locale)) {
legalPages.push(page)
} else {
sitePages.push(page)
}
}) })
} }
return { return {
sitePages: sitePages.sort(bySortOrder), sitePages: sitePages.sort(bySortOrder),
legalPages: legalPages.sort(bySortOrder),
} }
} }
const isLegalPage = (slug: string, locale?: string) =>
locale
? LEGAL_PAGES.some((p) => `${locale}/${p}` === slug)
: LEGAL_PAGES.includes(slug)
// Sort pages by the sort order assigned in the BC dashboard // Sort pages by the sort order assigned in the BC dashboard
function bySortOrder(a: Page, b: Page) { function bySortOrder(a: Page, b: Page) {
return (a.sort_order ?? 0) - (b.sort_order ?? 0) return (a.sort_order ?? 0) - (b.sort_order ?? 0)

View File

@ -28,7 +28,7 @@ const HomeAllProductsGrid: FC<Props> = ({
</Link> </Link>
</li> </li>
{categories.map((cat: any) => ( {categories.map((cat: any) => (
<li key={cat.path} className="py-1 text-accents-8 text-base"> <li key={cat.path} className="py-1 text-accent-8 text-base">
<Link href={getCategoryPath(cat.path)}> <Link href={getCategoryPath(cat.path)}>
<a>{cat.name}</a> <a>{cat.name}</a>
</Link> </Link>
@ -42,7 +42,7 @@ const HomeAllProductsGrid: FC<Props> = ({
</Link> </Link>
</li> </li>
{brands.flatMap(({ node }: any) => ( {brands.flatMap(({ node }: any) => (
<li key={node.path} className="py-1 text-accents-8 text-base"> <li key={node.path} className="py-1 text-accent-8 text-base">
<Link href={getDesignerPath(node.path)}> <Link href={getDesignerPath(node.path)}>
<a>{node.name}</a> <a>{node.name}</a>
</Link> </Link>

View File

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

View File

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

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 cn from 'classnames'
import type { Page } from '@commerce/types/page' import React, { FC } from 'react'
import type { Category } from '@commerce/types/site' import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import { CommerceProvider } from '@framework' import { CommerceProvider } from '@framework'
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { useUI } from '@components/ui/context' import { useUI } from '@components/ui/context'
import type { Page } from '@commerce/types/page'
import { Navbar, Footer } from '@components/common' import { Navbar, Footer } from '@components/common'
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui' import type { Category } from '@commerce/types/site'
import ShippingView from '@components/checkout/ShippingView'
import CartSidebarView from '@components/cart/CartSidebarView' import CartSidebarView from '@components/cart/CartSidebarView'
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
import PaymentMethodView from '@components/checkout/PaymentMethodView'
import CheckoutSidebarView from '@components/checkout/CheckoutSidebarView'
import LoginView from '@components/auth/LoginView' import LoginView from '@components/auth/LoginView'
import s from './Layout.module.css' import s from './Layout.module.css'
@ -45,15 +49,53 @@ interface Props {
} }
} }
const ModalView: FC<{ modalView: string; closeModal(): any }> = ({
modalView,
closeModal,
}) => {
return (
<Modal onClose={closeModal}>
{modalView === 'LOGIN_VIEW' && <LoginView />}
{modalView === 'SIGNUP_VIEW' && <SignUpView />}
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
</Modal>
)
}
const ModalUI: FC = () => {
const { displayModal, closeModal, modalView } = useUI()
return displayModal ? (
<ModalView modalView={modalView} closeModal={closeModal} />
) : null
}
const SidebarView: FC<{ sidebarView: string; closeSidebar(): any }> = ({
sidebarView,
closeSidebar,
}) => {
return (
<Sidebar onClose={closeSidebar}>
{sidebarView === 'CART_VIEW' && <CartSidebarView />}
{sidebarView === 'CHECKOUT_VIEW' && <CheckoutSidebarView />}
{sidebarView === 'PAYMENT_VIEW' && <PaymentMethodView />}
{sidebarView === 'SHIPPING_VIEW' && <ShippingView />}
</Sidebar>
)
}
const SidebarUI: FC = () => {
const { displaySidebar, closeSidebar, sidebarView } = useUI()
return displaySidebar ? (
<SidebarView sidebarView={sidebarView} closeSidebar={closeSidebar} />
) : null
}
const Layout: FC<Props> = ({ const Layout: FC<Props> = ({
children, children,
pageProps: { categories = [], ...pageProps }, pageProps: { categories = [], ...pageProps },
}) => { }) => {
const { displaySidebar, displayModal, closeSidebar, closeModal, modalView } =
useUI()
const { acceptedCookies, onAcceptCookies } = useAcceptCookies() const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
const { locale = 'en-US' } = useRouter() const { locale = 'en-US' } = useRouter()
const navBarlinks = categories.slice(0, 2).map((c) => ({ const navBarlinks = categories.slice(0, 2).map((c) => ({
label: c.name, label: c.name,
href: `/search/${c.slug}`, href: `/search/${c.slug}`,
@ -65,17 +107,8 @@ const Layout: FC<Props> = ({
<Navbar links={navBarlinks} /> <Navbar links={navBarlinks} />
<main className="fit">{children}</main> <main className="fit">{children}</main>
<Footer pages={pageProps.pages} /> <Footer pages={pageProps.pages} />
<ModalUI />
<Modal open={displayModal} onClose={closeModal}> <SidebarUI />
{modalView === 'LOGIN_VIEW' && <LoginView />}
{modalView === 'SIGNUP_VIEW' && <SignUpView />}
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
</Modal>
<Sidebar open={displaySidebar} onClose={closeSidebar}>
<CartSidebarView />
</Sidebar>
<FeatureBar <FeatureBar
title="This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy." title="This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy."
hide={acceptedCookies} hide={acceptedCookies}

View File

@ -2,16 +2,26 @@
@apply sticky top-0 bg-primary z-40 transition-all duration-150; @apply sticky top-0 bg-primary z-40 transition-all duration-150;
} }
.nav {
@apply relative flex flex-row justify-between py-4 md:py-4;
}
.navMenu {
@apply hidden ml-6 space-x-4 lg:block;
}
.link { .link {
@apply inline-flex items-center text-primary leading-6 font-medium transition ease-in-out duration-75 cursor-pointer text-accents-6; @apply inline-flex items-center leading-6
transition ease-in-out duration-75 cursor-pointer
text-accent-5;
} }
.link:hover { .link:hover {
@apply text-accents-9; @apply text-accent-9;
} }
.link:focus { .link:focus {
@apply outline-none text-accents-8; @apply outline-none text-accent-8;
} }
.logo { .logo {

View File

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

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

View File

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

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

View File

@ -109,7 +109,7 @@ const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
</li> </li>
<li> <li>
<a <a
className={cn(s.link, 'border-t border-accents-2 mt-4')} className={cn(s.link, 'border-t border-accent-2 mt-4')}
onClick={() => logout()} onClick={() => logout()}
> >
Logout Logout

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; @apply mr-6 cursor-pointer relative transition ease-in-out duration-100 flex items-center outline-none text-primary;
&:hover { &:hover {
@apply text-accents-6 transition scale-110 duration-100; @apply text-accent-6 transition scale-110 duration-100;
} }
&:last-child { &:last-child {
@ -24,7 +24,7 @@
} }
.bagCount { .bagCount {
@apply border border-accents-1 bg-secondary text-secondary absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs; @apply border border-accent-1 bg-secondary text-secondary absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs;
padding-left: 2.5px; padding-left: 2.5px;
padding-right: 2.5px; padding-right: 2.5px;
min-width: 1.25rem; min-width: 1.25rem;

View File

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

View File

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

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" strokeLinejoin="round"
fill="none" fill="none"
shapeRendering="geometricPrecision" shapeRendering="geometricPrecision"
{...props}
> >
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" /> <rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
<path d="M1 10h22" /> <path d="M1 10h22" />

16
components/icons/Star.tsx Normal file
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 Heart } from './Heart'
export { default as Trash } from './Trash' export { default as Trash } from './Trash'
export { default as Cross } from './Cross' export { default as Cross } from './Cross'
export { default as ArrowLeft } from './ArrowLeft'
export { default as Plus } from './Plus' export { default as Plus } from './Plus'
export { default as Minus } from './Minus' export { default as Minus } from './Minus'
export { default as Check } from './Check' export { default as Check } from './Check'
export { default as Sun } from './Sun' export { default as Sun } from './Sun'
export { default as Moon } from './Moon' export { default as Moon } from './Moon'
export { default as Github } from './Github' export { default as Github } from './Github'
export { default as DoubleChevron } from './DoubleChevron'
export { default as RightArrow } from './RightArrow'
export { default as Info } from './Info' export { default as Info } from './Info'
export { default as ChevronUp } from './ChevronUp'
export { default as Vercel } from './Vercel' export { default as Vercel } from './Vercel'
export { default as MapPin } from './MapPin' export { default as MapPin } from './MapPin'
export { default as Star } from './Star'
export { default as ArrowLeft } from './ArrowLeft'
export { default as ArrowRight } from './ArrowRight'
export { default as CreditCard } from './CreditCard' export { default as CreditCard } from './CreditCard'
export { default as ChevronUp } from './ChevronUp'
export { default as ChevronLeft } from './ChevronLeft'
export { default as ChevronDown } from './ChevronDown'
export { default as ChevronRight } from './ChevronRight'
export { default as DoubleChevron } from './DoubleChevron'

View File

@ -1,136 +1,114 @@
.root { .root {
@apply relative max-h-full w-full box-border overflow-hidden @apply relative max-h-full w-full box-border overflow-hidden
bg-no-repeat bg-center bg-cover transition-transform bg-no-repeat bg-center bg-cover transition-transform
ease-linear cursor-pointer; ease-linear cursor-pointer inline-block bg-accent-1;
height: 100% !important; height: 100% !important;
}
&:hover { .root:hover {
& .squareBg:before { & .productImage {
transform: scale(0.98); transform: scale(1.2625);
}
& .productImage {
transform: scale(1.2625);
}
& .productTitle > span,
& .productPrice,
& .wishlistButton {
@apply bg-secondary text-secondary;
}
&:nth-child(6n + 1) .productTitle > span,
&:nth-child(6n + 1) .productPrice,
&:nth-child(6n + 1) .wishlistButton {
@apply bg-violet text-white;
}
&:nth-child(6n + 5) .productTitle > span,
&:nth-child(6n + 5) .productPrice,
&:nth-child(6n + 5) .wishlistButton {
@apply bg-blue text-white;
}
&:nth-child(6n + 3) .productTitle > span,
&:nth-child(6n + 3) .productPrice,
&:nth-child(6n + 3) .wishlistButton {
@apply bg-pink text-white;
}
&:nth-child(6n + 6) .productTitle > span,
&:nth-child(6n + 6) .productPrice,
&:nth-child(6n + 6) .wishlistButton {
@apply bg-cyan text-white;
}
} }
&:nth-child(6n + 1) .squareBg { & .header .name span,
@apply bg-violet; & .header .price,
& .wishlistButton {
@apply bg-secondary text-secondary;
} }
&:nth-child(6n + 5) .squareBg { &:nth-child(6n + 1) .header .name span,
@apply bg-blue; &:nth-child(6n + 1) .header .price,
&:nth-child(6n + 1) .wishlistButton {
@apply bg-violet text-white;
} }
&:nth-child(6n + 3) .squareBg { &:nth-child(6n + 5) .header .name span,
@apply bg-pink; &:nth-child(6n + 5) .header .price,
&:nth-child(6n + 5) .wishlistButton {
@apply bg-blue text-white;
} }
&:nth-child(6n + 6) .squareBg { &:nth-child(6n + 3) .header .name span,
@apply bg-cyan; &:nth-child(6n + 3) .header .price,
&:nth-child(6n + 3) .wishlistButton {
@apply bg-pink text-white;
}
&:nth-child(6n + 6) .header .name span,
&:nth-child(6n + 6) .header .price,
&:nth-child(6n + 6) .wishlistButton {
@apply bg-cyan text-white;
} }
} }
.squareBg, .header {
.productTitle > span, @apply transition-colors ease-in-out duration-500
.productPrice, absolute top-0 left-0 z-20 pr-16;
.wishlistButton {
@apply transition-colors ease-in-out duration-500;
} }
.squareBg { .header .name {
@apply transition-colors absolute inset-0 z-0; @apply pt-0 max-w-full w-full leading-extra-loose
background-color: #212529; transition-colors ease-in-out duration-500;
}
.squareBg:before {
@apply transition ease-in-out duration-500 bg-repeat-space w-full h-full block;
background-image: url('/bg-products.svg');
content: '';
}
.simple {
& .squareBg {
@apply bg-accents-0 !important;
background-image: url('/bg-products.svg');
}
& .productTitle {
@apply pt-2;
font-size: 1rem;
& span {
@apply leading-extra-loose;
}
}
& .productPrice {
@apply text-sm;
}
}
.productTitle {
@apply pt-0 max-w-full w-full leading-extra-loose;
font-size: 2rem; font-size: 2rem;
letter-spacing: 0.4px; letter-spacing: 0.4px;
& span {
@apply py-4 px-6 bg-primary text-primary font-bold;
font-size: inherit;
letter-spacing: inherit;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
} }
.productPrice { .header .name span {
@apply py-4 px-6 bg-primary text-primary font-semibold inline-block text-sm leading-6; @apply py-4 px-6 bg-primary text-primary font-bold
letter-spacing: 0.4px; transition-colors ease-in-out duration-500;
font-size: inherit;
letter-spacing: inherit;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
} }
.wishlistButton { .header .price {
@apply w-10 h-10 flex ml-auto items-center justify-center bg-primary text-primary font-semibold text-xs leading-6 cursor-pointer; @apply pt-2 px-6 pb-4 text-sm bg-primary text-accent-9
font-semibold inline-block tracking-wide
transition-colors ease-in-out duration-500;
} }
.imageContainer { .imageContainer {
@apply flex items-center justify-center; @apply flex items-center justify-center overflow-hidden;
overflow: hidden;
& > div {
min-width: 100%;
}
} }
.productImage { .imageContainer > div {
@apply transform transition-transform duration-500 object-cover scale-120; min-width: 100%;
}
.imageContainer .productImage {
@apply transform transition-transform duration-500
object-cover scale-120;
}
.root .wishlistButton {
@apply top-0 right-0 z-30 absolute;
}
/* Variant Simple */
.simple .header .name {
@apply pt-2 text-lg leading-10 -mt-1;
}
.simple .header .price {
@apply text-sm;
}
/* Variant Slim */
.slim {
@apply bg-transparent relative overflow-hidden
box-border;
}
.slim .header {
@apply absolute inset-0 flex items-center justify-end mr-8 z-20;
}
.slim span {
@apply bg-accent-9 text-accent-0 inline-block p-3
font-bold text-xl break-words;
}
.root:global(.secondary) .header span {
@apply bg-accent-0 text-accent-9;
} }

View File

@ -5,58 +5,98 @@ import type { Product } from '@commerce/types/product'
import s from './ProductCard.module.css' import s from './ProductCard.module.css'
import Image, { ImageProps } from 'next/image' import Image, { ImageProps } from 'next/image'
import WishlistButton from '@components/wishlist/WishlistButton' import WishlistButton from '@components/wishlist/WishlistButton'
import usePrice from '@framework/product/use-price'
import ProductTag from '../ProductTag'
interface Props { interface Props {
className?: string className?: string
product: Product product: Product
variant?: 'slim' | 'simple' noNameTag?: boolean
imgProps?: Omit<any, 'src'> imgProps?: Omit<ImageProps, 'src'>
variant?: 'default' | 'slim' | 'simple'
} }
const placeholderImg = '/product-img-placeholder.svg' const placeholderImg = '/product-img-placeholder.svg'
const ProductCard: FC<Props> = ({ const ProductCard: FC<Props> = ({
className,
product, product,
variant,
imgProps, imgProps,
className,
noNameTag = false,
variant = 'default',
...props ...props
}) => ( }) => {
<Link href={`/product/${product.slug}`} {...props}> const { price } = usePrice({
<a className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}> amount: product.price.value,
{variant === 'slim' ? ( baseAmount: product.price.retailPrice,
<div className="relative overflow-hidden box-border"> currencyCode: product.price.currencyCode!,
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20"> })
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
{product.name} const rootClassName = cn(
</span> s.root,
</div> { [s.slim]: variant === 'slim', [s.simple]: variant === 'simple' },
{product?.images && ( className
<Image )
quality="85"
src={product.images[0]?.url || placeholderImg} return (
alt={product.name || 'Product Image'} <Link href={`/product/${product.slug}`} {...props}>
height={320} <a className={rootClassName}>
width={320} {variant === 'slim' && (
layout="fixed" <>
{...imgProps} <div className={s.header}>
/> <span>{product.name}</span>
)}
</div>
) : (
<>
<div className={s.squareBg} />
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
<div className="absolute top-0 left-0 pr-16 max-w-full">
<h3 className={s.productTitle}>
<span>{product.name}</span>
</h3>
<span className={s.productPrice}>
{product.price.value}
&nbsp;
{product.price.currencyCode}
</span>
</div> </div>
{product?.images && (
<Image
quality="85"
src={product.images[0]?.url || placeholderImg}
alt={product.name || 'Product Image'}
height={320}
width={320}
layout="fixed"
{...imgProps}
/>
)}
</>
)}
{variant === 'simple' && (
<>
{process.env.COMMERCE_WISHLIST_ENABLED && (
<WishlistButton
className={s.wishlistButton}
productId={product.id}
variant={product.variants[0]}
/>
)}
{!noNameTag && (
<div className={s.header}>
<h3 className={s.name}>
<span>{product.name}</span>
</h3>
<div className={s.price}>
{`${price} ${product.price?.currencyCode}`}
</div>
</div>
)}
<div className={s.imageContainer}>
{product?.images && (
<Image
alt={product.name || 'Product Image'}
className={s.productImage}
src={product.images[0].url || placeholderImg}
height={540}
width={540}
quality="85"
layout="responsive"
{...imgProps}
/>
)}
</div>
</>
)}
{variant === 'default' && (
<>
{process.env.COMMERCE_WISHLIST_ENABLED && ( {process.env.COMMERCE_WISHLIST_ENABLED && (
<WishlistButton <WishlistButton
className={s.wishlistButton} className={s.wishlistButton}
@ -64,25 +104,29 @@ const ProductCard: FC<Props> = ({
variant={product.variants[0] as any} variant={product.variants[0] as any}
/> />
)} )}
</div> <ProductTag
<div className={s.imageContainer}> name={product.name}
{product?.images && ( price={`${price} ${product.price?.currencyCode}`}
<Image />
alt={product.name || 'Product Image'} <div className={s.imageContainer}>
className={s.productImage} {product?.images && (
src={product.images[0]?.url || placeholderImg} <Image
height={540} alt={product.name || 'Product Image'}
width={540} className={s.productImage}
quality="85" src={product.images[0]?.url || placeholderImg}
layout="responsive" height={540}
{...imgProps} width={540}
/> quality="85"
)} layout="responsive"
</div> {...imgProps}
</> />
)} )}
</a> </div>
</Link> </>
) )}
</a>
</Link>
)
}
export default ProductCard export default ProductCard

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 { .root {
@apply relative w-full h-full; @apply relative w-full h-full select-none;
overflow: hidden;
}
.slider {
@apply relative h-full transition-opacity duration-150;
opacity: 0;
}
.slider.show {
opacity: 1;
}
.thumb {
@apply transition-transform transition-colors
ease-linear duration-75 overflow-hidden inline-block
cursor-pointer h-full;
width: 125px;
width: calc(100% / 3);
}
.thumb.selected {
@apply bg-white;
}
.thumb img {
height: 85% !important;
}
.album {
width: 100%;
height: 100%;
@apply bg-violet-dark;
box-sizing: content-box;
overflow-y: hidden; overflow-y: hidden;
overflow-x: auto;
white-space: nowrap;
height: 125px;
scrollbar-width: none;
} }
.leftControl, /* Hide scrollbar for Chrome, Safari and Opera */
.rightControl { .album::-webkit-scrollbar {
@apply absolute top-1/2 -translate-x-1/2 z-20 w-16 h-16 flex items-center justify-center bg-hover-1 rounded-full; display: none;
} }
.leftControl:hover, @screen md {
.rightControl:hover { .thumb:hover {
@apply bg-hover-2; transform: scale(1.02);
} background-color: rgba(255, 255, 255, 0.08);
}
.leftControl:hover, .thumb.selected {
.rightControl:hover { @apply bg-white;
@apply outline-none shadow-outline-normal; }
}
.leftControl { .album {
@apply bg-cover left-10; height: 182px;
background-image: url('public/cursor-left.png'); }
.thumb {
@screen md { width: 235px;
@apply left-6;
} }
} }
.rightControl {
@apply bg-cover right-10;
background-image: url('public/cursor-right.png');
@screen md {
@apply right-6;
}
}
.control {
@apply opacity-0 transition duration-150;
}
.root:hover .control {
@apply opacity-100;
}
.positionIndicatorsContainer {
@apply hidden;
@screen sm {
@apply block absolute bottom-6 left-1/2;
transform: translateX(-50%);
}
}
.positionIndicator {
@apply rounded-full p-2;
}
.dot {
@apply bg-hover-1 transition w-3 h-3 rounded-full;
}
.positionIndicator:hover .dot {
@apply bg-hover-2;
}
.positionIndicator:focus {
@apply outline-none;
}
.positionIndicator:focus .dot {
@apply shadow-outline-normal;
}
.positionIndicatorActive .dot {
@apply bg-white;
}
.positionIndicatorActive:hover .dot {
@apply bg-white;
}

View File

@ -8,20 +8,42 @@ import React, {
useEffect, useEffect,
} from 'react' } from 'react'
import cn from 'classnames' import cn from 'classnames'
import { a } from '@react-spring/web'
import s from './ProductSlider.module.css' import s from './ProductSlider.module.css'
import ProductSliderControl from '../ProductSliderControl'
const ProductSlider: FC = ({ children }) => { interface ProductSliderProps {
children: React.ReactNode[]
className?: string
}
const ProductSlider: React.FC<ProductSliderProps> = ({
children,
className = '',
}) => {
const [currentSlide, setCurrentSlide] = useState(0) const [currentSlide, setCurrentSlide] = useState(0)
const [isMounted, setIsMounted] = useState(false) const [isMounted, setIsMounted] = useState(false)
const sliderContainerRef = useRef<HTMLDivElement>(null) const sliderContainerRef = useRef<HTMLDivElement>(null)
const thumbsContainerRef = useRef<HTMLDivElement>(null)
const [ref, slider] = useKeenSlider<HTMLDivElement>({ const [ref, slider] = useKeenSlider<HTMLDivElement>({
loop: true, loop: true,
slidesPerView: 1, slidesPerView: 1,
mounted: () => setIsMounted(true), mounted: () => setIsMounted(true),
slideChanged(s) { slideChanged(s) {
setCurrentSlide(s.details().relativeSlide) const slideNumber = s.details().relativeSlide
setCurrentSlide(slideNumber)
if (thumbsContainerRef.current) {
const $el = document.getElementById(
`thumb-${s.details().relativeSlide}`
)
if (slideNumber >= 3) {
thumbsContainerRef.current.scrollLeft = $el!.offsetLeft
} else {
thumbsContainerRef.current.scrollLeft = 0
}
}
}, },
}) })
@ -59,23 +81,16 @@ const ProductSlider: FC = ({ children }) => {
} }
}, []) }, [])
const onPrev = React.useCallback(() => slider.prev(), [slider])
const onNext = React.useCallback(() => slider.next(), [slider])
return ( return (
<div className={s.root} ref={sliderContainerRef}> <div className={cn(s.root, className)} ref={sliderContainerRef}>
<button
className={cn(s.leftControl, s.control)}
onClick={slider?.prev}
aria-label="Previous Product Image"
/>
<button
className={cn(s.rightControl, s.control)}
onClick={slider?.next}
aria-label="Next Product Image"
/>
<div <div
ref={ref} ref={ref}
className="keen-slider h-full transition-opacity duration-150" className={cn(s.slider, { [s.show]: isMounted }, 'keen-slider')}
style={{ opacity: isMounted ? 1 : 0 }}
> >
{slider && <ProductSliderControl onPrev={onPrev} onNext={onNext} />}
{Children.map(children, (child) => { {Children.map(children, (child) => {
// Add the keen-slider__slide className to children // Add the keen-slider__slide className to children
if (isValidElement(child)) { if (isValidElement(child)) {
@ -92,26 +107,28 @@ const ProductSlider: FC = ({ children }) => {
return child return child
})} })}
</div> </div>
{slider && (
<div className={cn(s.positionIndicatorsContainer)}> <a.div className={s.album} ref={thumbsContainerRef}>
{[...Array(slider.details().size).keys()].map((idx) => { {slider &&
return ( Children.map(children, (child, idx) => {
<button if (isValidElement(child)) {
aria-label="Position indicator" return {
key={idx} ...child,
className={cn(s.positionIndicator, { props: {
[s.positionIndicatorActive]: currentSlide === idx, ...child.props,
})} className: cn(child.props.className, s.thumb, {
onClick={() => { [s.selected]: currentSlide === idx,
slider.moveToSlideRelative(idx) }),
}} id: `thumb-${idx}`,
> onClick: () => {
<div className={s.dot} /> slider.moveToSlideRelative(idx)
</button> },
) },
}
}
return child
})} })}
</div> </a.div>
)}
</div> </div>
) )
} }

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 { .root {
@apply relative grid items-start gap-8 grid-cols-1 overflow-x-hidden; @apply relative grid items-start gap-1 grid-cols-1 overflow-x-hidden;
min-height: auto;
@screen lg {
@apply grid-cols-12;
}
} }
.productDisplay { .main {
@apply relative flex px-0 pb-0 box-border col-span-1 bg-violet; @apply relative px-0 pb-0 box-border flex flex-col col-span-1;
min-height: 600px; min-height: 500px;
@screen md {
min-height: 700px;
}
@screen lg {
margin-right: -2rem;
margin-left: -2rem;
@apply mx-0 col-span-6;
min-height: 100%;
height: 100%;
}
}
.squareBg {
@apply absolute inset-0 bg-violet z-0 h-full;
}
.nameBox {
@apply absolute top-6 left-0 z-20 pr-16;
@screen lg {
@apply left-6 pr-16;
}
& .name {
@apply px-6 py-2 bg-primary text-primary font-bold;
font-size: 2rem;
letter-spacing: 0.4px;
}
& .price {
@apply px-6 py-2 pb-4 bg-primary text-primary font-bold inline-block tracking-wide;
}
@screen lg {
& .name,
& .price {
@apply bg-violet-light text-white;
}
}
} }
.sidebar { .sidebar {
@apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 w-full h-full; @apply flex flex-col col-span-1 mx-auto max-w-8xl px-6 py-6 w-full h-full;
@screen lg {
@apply col-span-6 py-24 justify-between;
}
} }
.sliderContainer { .sliderContainer {
@apply absolute z-10 inset-0 flex items-center justify-center overflow-x-hidden; @apply flex items-center justify-center overflow-x-hidden bg-violet;
} }
.imageContainer { .imageContainer {
& > div { @apply text-center;
@apply h-full;
& > div {
@apply h-full;
}
}
} }
.img { .imageContainer > div,
.imageContainer > div > div {
@apply h-full;
}
.sliderContainer .img {
@apply w-full h-auto max-h-full object-cover; @apply w-full h-auto max-h-full object-cover;
} }
.button { .button {
text-align: center;
width: 100%; width: 100%;
max-width: 300px;
@screen sm {
min-width: 300px;
}
} }
.wishlistButton { .wishlistButton {
@apply absolute z-30 top-6 right-0 bg-primary text-primary w-10 h-10 flex items-center justify-center font-semibold leading-6 cursor-pointer; @apply absolute z-30 top-0 right-0;
}
@screen lg { .relatedProductsGrid {
@apply right-12 text-white bg-violet; @apply grid grid-cols-2 py-2 gap-2 md:grid-cols-4 md:gap-7;
}
@screen lg {
.root {
@apply grid-cols-12;
}
.main {
@apply mx-0 col-span-8;
}
.sidebar {
@apply col-span-4 py-6;
}
.imageContainer {
max-height: 600px;
} }
} }

View File

@ -1,64 +1,90 @@
import cn from 'classnames' import cn from 'classnames'
import Image from 'next/image' import Image from 'next/image'
import { NextSeo } from 'next-seo' import { NextSeo } from 'next-seo'
import { FC, useEffect, useState } from 'react'
import s from './ProductView.module.css' import s from './ProductView.module.css'
import { Swatch, ProductSlider } from '@components/product' import { FC } from 'react'
import { Button, Container, Text, useUI } from '@components/ui'
import type { Product } from '@commerce/types/product' import type { Product } from '@commerce/types/product'
import usePrice from '@framework/product/use-price' import usePrice from '@framework/product/use-price'
import { useAddItem } from '@framework/cart' import { WishlistButton } from '@components/wishlist'
import { getVariant, SelectedOptions } from '../helpers' import { ProductSlider, ProductCard } from '@components/product'
import WishlistButton from '@components/wishlist/WishlistButton' import { Container, Text } from '@components/ui'
import ProductSidebar from '../ProductSidebar'
interface Props { import ProductTag from '../ProductTag'
children?: any interface ProductViewProps {
product: Product product: Product
className?: string relatedProducts: Product[]
} }
const ProductView: FC<Props> = ({ product }) => { const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
// TODO: fix this missing argument issue
/* @ts-ignore */
const addItem = useAddItem()
const { price } = usePrice({ const { price } = usePrice({
amount: product.price.value, amount: product.price.value,
baseAmount: product.price.retailPrice, baseAmount: product.price.retailPrice,
currencyCode: product.price.currencyCode!, currencyCode: product.price.currencyCode!,
}) })
const { openSidebar } = useUI()
const [loading, setLoading] = useState(false)
const [choices, setChoices] = useState<SelectedOptions>({})
useEffect(() => {
// Selects the default option
const options = product.variants[0].options || []
options.forEach((v) => {
setChoices((choices) => ({
...choices,
[v.displayName.toLowerCase()]: v.values[0]?.label.toLowerCase(),
}))
})
}, [])
const variant = getVariant(product, choices)
const addToCart = async () => {
setLoading(true)
try {
await addItem({
productId: String(product.id),
variantId: String(variant ? variant.id : product.variants[0].id),
})
openSidebar()
setLoading(false)
} catch (err) {
setLoading(false)
}
}
return ( return (
<Container className="max-w-none w-full" clean> <>
<Container className="max-w-none w-full" clean>
<div className={cn(s.root, 'fit')}>
<div className={cn(s.main, 'fit')}>
<ProductTag
name={product.name}
price={`${price} ${product.price?.currencyCode}`}
fontSize={32}
/>
<div className={s.sliderContainer}>
<ProductSlider key={product.id}>
{product.images.map((image, i) => (
<div key={image.url} className={s.imageContainer}>
<Image
className={s.img}
src={image.url!}
alt={image.alt || 'Product Image'}
width={600}
height={600}
priority={i === 0}
quality="85"
/>
</div>
))}
</ProductSlider>
</div>
{process.env.COMMERCE_WISHLIST_ENABLED && (
<WishlistButton
className={s.wishlistButton}
productId={product.id}
variant={product.variants[0]}
/>
)}
</div>
<ProductSidebar product={product} className={s.sidebar} />
</div>
<hr className="mt-7 border-accent-2" />
<section className="py-12 px-6 mb-10">
<Text variant="sectionHeading">Related Products</Text>
<div className={s.relatedProductsGrid}>
{relatedProducts.map((p) => (
<div
key={p.path}
className="animated fadeIn bg-accent-0 border border-accent-2"
>
<ProductCard
noNameTag
product={p}
key={p.path}
variant="simple"
className="animated fadeIn"
imgProps={{
width: 300,
height: 300,
}}
/>
</div>
))}
</div>
</section>
</Container>
<NextSeo <NextSeo
title={product.name} title={product.name}
description={product.description} description={product.description}
@ -76,97 +102,7 @@ const ProductView: FC<Props> = ({ product }) => {
], ],
}} }}
/> />
<div className={cn(s.root, 'fit')}> </>
<div className={cn(s.productDisplay, 'fit')}>
<div className={s.nameBox}>
<h1 className={s.name}>{product.name}</h1>
<div className={s.price}>
{price}
{` `}
{product.price?.currencyCode}
</div>
</div>
<div className={s.sliderContainer}>
<ProductSlider key={product.id}>
{product.images.map((image, i) => (
<div key={image.url} className={s.imageContainer}>
<Image
className={s.img}
src={image.url!}
alt={image.alt || 'Product Image'}
width={1050}
height={1050}
priority={i === 0}
quality="85"
/>
</div>
))}
</ProductSlider>
</div>
</div>
<div className={s.sidebar}>
<section>
{product.options?.map((opt) => (
<div className="pb-4" key={opt.displayName}>
<h2 className="uppercase font-medium">{opt.displayName}</h2>
<div className="flex flex-row py-4">
{opt.values.map((v, i: number) => {
const active = (choices as any)[
opt.displayName.toLowerCase()
]
return (
<Swatch
key={`${opt.id}-${i}`}
active={v.label.toLowerCase() === active}
variant={opt.displayName}
color={v.hexColors ? v.hexColors[0] : ''}
label={v.label}
onClick={() => {
setChoices((choices) => {
return {
...choices,
[opt.displayName.toLowerCase()]:
v.label.toLowerCase(),
}
})
}}
/>
)
})}
</div>
</div>
))}
<div className="pb-14 break-words w-full max-w-xl">
<Text html={product.descriptionHtml || product.description} />
</div>
</section>
<div>
<Button
aria-label="Add to Cart"
type="button"
className={s.button}
onClick={addToCart}
loading={loading}
disabled={variant?.availableForSale === false}
>
{variant?.availableForSale === false
? 'Not Available'
: 'Add To Cart'}
</Button>
</div>
</div>
{process.env.COMMERCE_WISHLIST_ENABLED && (
<WishlistButton
className={s.wishlistButton}
productId={product.id}
variant={product.variants[0]! as any}
/>
)}
</div>
</Container>
) )
} }

View File

@ -1,11 +1,13 @@
.swatch { .swatch {
box-sizing: border-box; box-sizing: border-box;
composes: root from 'components/ui/Button/Button.module.css'; composes: root from '@components/ui/Button/Button.module.css';
@apply h-12 w-12 bg-primary text-primary rounded-full mr-3 inline-flex @apply h-10 w-10 bg-primary text-primary rounded-full mr-3 inline-flex
items-center justify-center cursor-pointer transition duration-150 ease-in-out items-center justify-center cursor-pointer transition duration-150 ease-in-out
p-0 shadow-none border-gray-200 border box-border; p-0 shadow-none border-accent-3 border box-border select-none;
margin-right: calc(0.75rem - 1px); margin-right: calc(0.75rem - 1px);
overflow: hidden; overflow: hidden;
width: 48px;
height: 48px;
} }
.swatch::before, .swatch::before,
@ -35,7 +37,7 @@
} }
.active { .active {
@apply border-accents-9 border-2; @apply border-accent-9 border-2;
padding-right: 1px; padding-right: 1px;
padding-left: 1px; padding-left: 1px;
} }
@ -46,7 +48,7 @@
} }
.active.textLabel { .active.textLabel {
@apply border-accents-9 border-2; @apply border-accent-9 border-2;
padding-right: calc(1rem - 1px); padding-right: calc(1rem - 1px);
padding-left: calc(1rem - 1px); padding-left: calc(1rem - 1px);
} }

View File

@ -1,5 +1,5 @@
import cn from 'classnames' import cn from 'classnames'
import { FC } from 'react' import React from 'react'
import s from './Swatch.module.css' import s from './Swatch.module.css'
import { Check } from '@components/icons' import { Check } from '@components/icons'
import Button, { ButtonProps } from '@components/ui/Button' import Button, { ButtonProps } from '@components/ui/Button'
@ -13,48 +13,50 @@ interface SwatchProps {
label?: string | null label?: string | null
} }
const Swatch: FC<Omit<ButtonProps, 'variant'> & SwatchProps> = ({ const Swatch: React.FC<Omit<ButtonProps, 'variant'> & SwatchProps> = React.memo(
className, ({
color = '', active,
label = null, className,
variant = 'size', color = '',
active, label = null,
...props variant = 'size',
}) => { ...props
variant = variant?.toLowerCase() }) => {
variant = variant?.toLowerCase()
if (label) { if (label) {
label = label?.toLowerCase() label = label?.toLowerCase()
}
const swatchClassName = cn(
s.swatch,
{
[s.color]: color,
[s.active]: active,
[s.size]: variant === 'size',
[s.dark]: color ? isDark(color) : false,
[s.textLabel]: !color && label && label.length > 3,
},
className
)
return (
<Button
aria-label="Variant Swatch"
className={swatchClassName}
{...(label && color && { title: label })}
style={color ? { backgroundColor: color } : {}}
{...props}
>
{color && active && (
<span>
<Check />
</span>
)}
{!color ? label : null}
</Button>
)
} }
)
const swatchClassName = cn(
s.swatch,
{
[s.active]: active,
[s.size]: variant === 'size',
[s.color]: color,
[s.dark]: color ? isDark(color) : false,
[s.textLabel]: !color && label && label.length > 3,
},
className
)
return (
<Button
className={swatchClassName}
style={color ? { backgroundColor: color } : {}}
aria-label="Variant Swatch"
{...(label && color && { title: label })}
{...props}
>
{color && active && (
<span>
<Check />
</span>
)}
{!color ? label : null}
</Button>
)
}
export default Swatch export default Swatch

View File

@ -1,7 +1,8 @@
import type { Product } from '@commerce/types/product' import type { Product } from '@commerce/types/product'
export type SelectedOptions = Record<string, string | null> export type SelectedOptions = Record<string, string | null>
import { Dispatch, SetStateAction } from 'react'
export function getVariant(product: Product, opts: SelectedOptions) { export function getProductVariant(product: Product, opts: SelectedOptions) {
const variant = product.variants.find((variant) => { const variant = product.variants.find((variant) => {
return Object.entries(opts).every(([key, value]) => return Object.entries(opts).every(([key, value]) =>
variant.options.find((option) => { variant.options.find((option) => {
@ -16,3 +17,16 @@ export function getVariant(product: Product, opts: SelectedOptions) {
}) })
return variant return variant
} }
export function selectDefaultOptionFromProduct(
product: Product,
updater: Dispatch<SetStateAction<SelectedOptions>>
) {
// Selects the default option
product.variants[0].options?.forEach((v) => {
updater((choices) => ({
...choices,
[v.displayName.toLowerCase()]: v.values[0].label.toLowerCase(),
}))
})
}

View File

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

439
components/search.tsx Normal file
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 { .root {
@apply bg-secondary text-accents-1 cursor-pointer inline-flex px-10 rounded-sm leading-6 transition ease-in-out duration-150 shadow-sm font-semibold text-center justify-center uppercase py-4 border border-transparent items-center; @apply bg-accent-9 text-accent-0 cursor-pointer inline-flex
px-10 py-5 leading-6 transition ease-in-out duration-150
shadow-sm text-center justify-center uppercase
border border-transparent items-center text-sm font-semibold
tracking-wide;
max-height: 64px;
} }
.root:hover { .root:hover {
@apply bg-accents-0 text-primary border border-secondary; @apply border-accent-9 bg-accent-6;
} }
.root:focus { .root:focus {
@ -11,22 +16,33 @@
} }
.root[data-active] { .root[data-active] {
@apply bg-gray-600; @apply bg-accent-6;
} }
.loading { .loading {
@apply bg-accents-1 text-accents-3 border-accents-2 cursor-not-allowed; @apply bg-accent-1 text-accent-3 border-accent-2 cursor-not-allowed;
} }
.slim { .slim {
@apply py-2 transform-none normal-case; @apply py-2 transform-none normal-case;
} }
.ghost {
@apply border border-accent-2 bg-accent-0 text-accent-9 text-sm;
}
.ghost:hover {
@apply border-accent-9 bg-accent-9 text-accent-0;
}
.disabled, .disabled,
.disabled:hover { .disabled:hover {
@apply text-accents-4 border-accents-2 bg-accents-1 cursor-not-allowed; @apply text-accent-4 border-accent-2 bg-accent-1 cursor-not-allowed;
filter: grayscale(1); filter: grayscale(1);
-webkit-transform: translateZ(0); -webkit-transform: translateZ(0);
-webkit-perspective: 1000; -webkit-perspective: 1000;
-webkit-backface-visibility: hidden; -webkit-backface-visibility: hidden;
} }
.progress {
}

View File

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

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

View File

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

View File

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

View File

@ -1,9 +1,30 @@
.root { .root {
@apply mx-auto grid grid-cols-1 py-32 gap-4; @apply flex flex-col py-16 mx-auto;
} }
@screen md { .title {
@apply text-accent-0 font-extrabold text-4xl leading-none tracking-tight;
}
.description {
@apply mt-4 text-xl leading-8 text-accent-2 mb-1 lg:max-w-4xl;
}
@screen lg {
.root { .root {
@apply grid-cols-2; @apply flex-row items-start justify-center py-32;
}
.title {
@apply text-5xl max-w-xl text-right leading-10 -mt-3;
line-height: 3.5rem;
}
.description {
@apply mt-0 ml-6;
}
}
@screen xl {
.title {
@apply text-6xl;
} }
} }

View File

@ -1,30 +1,26 @@
import React, { FC } from 'react' import React, { FC } from 'react'
import { Container } from '@components/ui' import { Container } from '@components/ui'
import { RightArrow } from '@components/icons' import { ArrowRight } from '@components/icons'
import s from './Hero.module.css' import s from './Hero.module.css'
import Link from 'next/link' import Link from 'next/link'
interface Props { interface HeroProps {
className?: string className?: string
headline: string headline: string
description: string description: string
} }
const Hero: FC<Props> = ({ headline, description }) => { const Hero: FC<HeroProps> = ({ headline, description }) => {
return ( return (
<div className="bg-black"> <div className="bg-accent-9 border-b border-t border-accent-2">
<Container> <Container>
<div className={s.root}> <div className={s.root}>
<h2 className="text-4xl leading-10 font-extrabold text-white sm:text-5xl sm:leading-none sm:tracking-tight lg:text-6xl"> <h2 className={s.title}>{headline}</h2>
{headline} <div className={s.description}>
</h2> <p>{description}</p>
<div className="flex flex-col justify-between">
<p className="mt-5 text-xl leading-7 text-accent-2 text-white">
{description}
</p>
<Link href="/"> <Link href="/">
<a className="text-white pt-3 font-bold hover:underline flex flex-row cursor-pointer w-max-content"> <a className="flex items-center text-accent-0 pt-3 font-bold hover:underline cursor-pointer w-max-content">
Read it here Read it here
<RightArrow width="20" heigh="20" className="ml-1" /> <ArrowRight width="20" heigh="20" className="ml-1" />
</a> </a>
</Link> </Link>
</div> </div>

View File

@ -1,5 +1,5 @@
.root { .root {
@apply bg-primary py-2 px-6 w-full appearance-none transition duration-150 ease-in-out pr-10 border border-accents-3 text-accents-6; @apply bg-primary py-2 px-6 w-full appearance-none transition duration-150 ease-in-out pr-10 border border-accent-3 text-accent-6;
} }
.root:focus { .root:focus {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,17 @@
.root { .root {
@apply fixed bg-primary text-primary flex items-center inset-0 z-50 justify-center; @apply fixed bg-black bg-opacity-40 flex items-center inset-0 z-50 justify-center;
background-color: rgba(0, 0, 0, 0.35); backdrop-filter: blur(0.8px);
-webkit-backdrop-filter: blur(0.8px);
} }
.modal { .modal {
@apply bg-primary p-12 border border-accents-2 relative; @apply bg-primary p-12 border border-accent-2 relative;
} }
.modal:focus { .modal:focus {
@apply outline-none; @apply outline-none;
} }
.close {
@apply hover:text-accent-5 transition ease-in-out duration-150 focus:outline-none absolute right-0 top-0 m-6;
}

View File

@ -1,22 +1,20 @@
import { FC, useRef, useEffect, useCallback } from 'react' import { FC, useRef, useEffect, useCallback } from 'react'
import Portal from '@reach/portal'
import s from './Modal.module.css' import s from './Modal.module.css'
import FocusTrap from '@lib/focus-trap'
import { Cross } from '@components/icons' import { Cross } from '@components/icons'
import { import {
disableBodyScroll, disableBodyScroll,
enableBodyScroll,
clearAllBodyScrollLocks, clearAllBodyScrollLocks,
enableBodyScroll,
} from 'body-scroll-lock' } from 'body-scroll-lock'
import FocusTrap from '@lib/focus-trap' interface ModalProps {
interface Props {
className?: string className?: string
children?: any children?: any
open?: boolean
onClose: () => void onClose: () => void
onEnter?: () => void | null onEnter?: () => void | null
} }
const Modal: FC<Props> = ({ children, open, onClose, onEnter = null }) => { const Modal: FC<ModalProps> = ({ children, onClose }) => {
const ref = useRef() as React.MutableRefObject<HTMLDivElement> const ref = useRef() as React.MutableRefObject<HTMLDivElement>
const handleKey = useCallback( const handleKey = useCallback(
@ -30,36 +28,31 @@ const Modal: FC<Props> = ({ children, open, onClose, onEnter = null }) => {
useEffect(() => { useEffect(() => {
if (ref.current) { if (ref.current) {
if (open) { disableBodyScroll(ref.current, { reserveScrollBarGap: true })
disableBodyScroll(ref.current) window.addEventListener('keydown', handleKey)
window.addEventListener('keydown', handleKey)
} else {
enableBodyScroll(ref.current)
}
} }
return () => { return () => {
window.removeEventListener('keydown', handleKey) if (ref && ref.current) {
enableBodyScroll(ref.current)
}
clearAllBodyScrollLocks() clearAllBodyScrollLocks()
window.removeEventListener('keydown', handleKey)
} }
}, [open, handleKey]) }, [handleKey])
return ( return (
<Portal> <div className={s.root}>
{open ? ( <div className={s.modal} role="dialog" ref={ref}>
<div className={s.root}> <button
<div className={s.modal} role="dialog" ref={ref}> onClick={() => onClose()}
<button aria-label="Close panel"
onClick={() => onClose()} className={s.close}
aria-label="Close panel" >
className="hover:text-gray-500 transition ease-in-out duration-150 focus:outline-none absolute right-0 top-0 m-6" <Cross className="h-6 w-6" />
> </button>
<Cross className="h-6 w-6" /> <FocusTrap focusFirst>{children}</FocusTrap>
</button> </div>
<FocusTrap focusFirst>{children}</FocusTrap> </div>
</div>
</div>
) : null}
</Portal>
) )
} }

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

View File

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

View File

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

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