mirror of
https://github.com/vercel/commerce.git
synced 2025-05-18 07:26:59 +00:00
Merge branch 'custom-checkout' into improvements
This commit is contained in:
commit
46bb71cca1
@ -6,23 +6,6 @@
|
|||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions:hover {
|
|
||||||
@apply bg-accent-1 border-accent-3 text-accent-9;
|
|
||||||
transition: border-color;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions:focus {
|
|
||||||
@apply bg-accent-2 outline-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quantity {
|
.quantity {
|
||||||
appearance: textfield;
|
appearance: textfield;
|
||||||
@apply w-8 border-accent-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;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
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'
|
||||||
@ -9,6 +9,7 @@ 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
|
||||||
@ -28,6 +29,10 @@ const CartItem = ({
|
|||||||
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,
|
||||||
@ -35,43 +40,22 @@ 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) => {
|
|
||||||
await updateItem({ quantity: val })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleQuantity = (e: ChangeEvent<HTMLInputElement>) => {
|
const increaseQuantity = async (n = 1) => {
|
||||||
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
|
const val = Number(quantity) + n
|
||||||
if (Number.isInteger(val) && val >= 0) {
|
setQuantity(val)
|
||||||
setQuantity(val)
|
await updateItem({ quantity: 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)
|
||||||
@ -152,38 +136,13 @@ const CartItem = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{variant === 'default' && (
|
{variant === 'default' && (
|
||||||
<div className="flex flex-row h-9">
|
<Quantity
|
||||||
<button className={s.actions} onClick={handleRemove}>
|
value={quantity}
|
||||||
<Cross width={20} height={20} />
|
handleRemove={handleRemove}
|
||||||
</button>
|
handleChange={handleChange}
|
||||||
<label className="w-full border-accent-2 border ml-2">
|
increase={() => increaseQuantity(1)}
|
||||||
<input
|
decrease={() => increaseQuantity(-1)}
|
||||||
type="number"
|
/>
|
||||||
max={99}
|
|
||||||
min={0}
|
|
||||||
className="bg-transparent px-4 w-full h-full focus:outline-none"
|
|
||||||
value={quantity}
|
|
||||||
onChange={handleQuantity}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => increaseQuantity(-1)}
|
|
||||||
className={s.actions}
|
|
||||||
style={{ marginLeft: '-1px' }}
|
|
||||||
>
|
|
||||||
<Minus width={18} height={18} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => increaseQuantity(1)}
|
|
||||||
className={cn(s.actions)}
|
|
||||||
style={{ marginLeft: '-1px' }}
|
|
||||||
>
|
|
||||||
<Plus width={18} height={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
@ -89,7 +89,7 @@ const CartSidebarView: FC = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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 shadow-outline-normal text-sm">
|
<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">
|
<ul className="pb-2">
|
||||||
<li className="flex justify-between py-1">
|
<li className="flex justify-between py-1">
|
||||||
<span>Subtotal</span>
|
<span>Subtotal</span>
|
||||||
|
@ -50,7 +50,7 @@ const CheckoutSidebarView: FC = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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 shadow-outline-normal text-sm">
|
<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">
|
<ul className="pb-2">
|
||||||
<li className="flex justify-between py-1">
|
<li className="flex justify-between py-1">
|
||||||
<span>Subtotal</span>
|
<span>Subtotal</span>
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@apply h-10 px-2 rounded-md border border-accent-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-accent-4 shadow-sm;
|
@apply border-accent-3 shadow-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:focus {
|
.button:focus {
|
||||||
@ -38,5 +38,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon.active {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ const I18nWidget: FC = () => {
|
|||||||
/>
|
/>
|
||||||
{options && (
|
{options && (
|
||||||
<span className="cursor-pointer">
|
<span className="cursor-pointer">
|
||||||
<ChevronUp className={cn({ [s.icon]: display })} />
|
<ChevronUp className={cn(s.icon, { [s.active]: display })} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
@ -25,7 +25,7 @@ const SidebarLayout: FC<ComponentProps> = ({
|
|||||||
className="hover:text-gray-500 transition ease-in-out duration-150 flex items-center focus:outline-none"
|
className="hover:text-gray-500 transition ease-in-out duration-150 flex items-center focus:outline-none"
|
||||||
>
|
>
|
||||||
<Cross className="h-6 w-6" />
|
<Cross className="h-6 w-6" />
|
||||||
<span className="ml-2 text-accent-7 text-xs hover:text-gray-500">
|
<span className="ml-2 text-accent-7 text-sm hover:text-gray-500">
|
||||||
Close
|
Close
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply relative w-full h-full;
|
@apply relative w-full h-full select-none;
|
||||||
overflow-y: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
.control {
|
.control {
|
||||||
@apply bg-violet absolute bottom-10 right-10 flex flex-row
|
@apply bg-violet absolute bottom-10 right-10 flex flex-row
|
||||||
border-accent-0 border text-accent-0 z-30 shadow-xl;
|
border-accent-0 border text-accent-0 z-30 shadow-xl select-none;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,11 +59,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.album {
|
.album {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
@apply bg-violet-dark;
|
@apply bg-violet-dark;
|
||||||
|
box-sizing: content-box;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
height: 125px;
|
height: 125px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: -10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@screen md {
|
@screen md {
|
||||||
|
@ -9,11 +9,11 @@ import { getVariant, SelectedOptions } from '../helpers'
|
|||||||
import { Swatch, ProductSlider } from '@components/product'
|
import { Swatch, ProductSlider } from '@components/product'
|
||||||
import { Button, Container, Text, useUI } from '@components/ui'
|
import { Button, Container, Text, useUI } from '@components/ui'
|
||||||
import { useAddItem } from '@framework/cart'
|
import { useAddItem } from '@framework/cart'
|
||||||
|
import Rating from '@components/ui/Rating'
|
||||||
import Collapse from '@components/ui/Collapse'
|
import Collapse from '@components/ui/Collapse'
|
||||||
import ProductCard from '@components/product/ProductCard'
|
import ProductCard from '@components/product/ProductCard'
|
||||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||||
import rangeMap from '@lib/range-map'
|
|
||||||
import { Star } from '@components/icons'
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: any
|
children?: any
|
||||||
product: Product
|
product: Product
|
||||||
@ -154,20 +154,10 @@ const ProductView: FC<Props> = ({ product, relatedProducts }) => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div className="flex flex-row justify-between items-center">
|
<div className="flex flex-row justify-between items-center">
|
||||||
{/**
|
<Rating value={2} />
|
||||||
* TODO make component Rate stars={}
|
<div className="text-accent-6 pr-1 font-medium select-none">
|
||||||
*/}
|
36 reviews
|
||||||
<div className="flex flex-row py-6">
|
|
||||||
{rangeMap(4, (i) => (
|
|
||||||
<span className="inline-block ml-1" key={i}>
|
|
||||||
<Star />
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
<span className="inline-block ml-1 text-accent-5">
|
|
||||||
<Star />
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-accent-6 pr-1">36 reviews</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
composes: root from '@components/ui/Button/Button.module.css';
|
composes: root from '@components/ui/Button/Button.module.css';
|
||||||
@apply h-10 w-10 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-gray-200 border box-border select-none;
|
||||||
margin-right: calc(0.75rem - 1px);
|
margin-right: calc(0.75rem - 1px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
@ -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,
|
||||||
|
@ -3,13 +3,13 @@ import { Container } from '@components/ui'
|
|||||||
import { RightArrow } from '@components/icons'
|
import { RightArrow } 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-black">
|
||||||
<Container>
|
<Container>
|
||||||
|
@ -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)
|
||||||
|
@ -3,13 +3,13 @@ import s from './Marquee.module.css'
|
|||||||
import { FC, ReactNode, Component } from 'react'
|
import { FC, ReactNode, Component } from 'react'
|
||||||
import Ticker from 'react-ticker'
|
import Ticker from 'react-ticker'
|
||||||
|
|
||||||
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',
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
clearAllBodyScrollLocks,
|
clearAllBodyScrollLocks,
|
||||||
} from 'body-scroll-lock'
|
} from 'body-scroll-lock'
|
||||||
import FocusTrap from '@lib/focus-trap'
|
import FocusTrap from '@lib/focus-trap'
|
||||||
interface Props {
|
interface ModalProps {
|
||||||
className?: string
|
className?: string
|
||||||
children?: any
|
children?: any
|
||||||
open?: boolean
|
open?: boolean
|
||||||
@ -16,7 +16,7 @@ interface Props {
|
|||||||
onEnter?: () => void | null
|
onEnter?: () => void | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal: FC<Props> = ({ children, open, onClose, onEnter = null }) => {
|
const Modal: FC<ModalProps> = ({ children, open, onClose, onEnter = null }) => {
|
||||||
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
||||||
|
|
||||||
const handleKey = useCallback(
|
const handleKey = useCallback(
|
||||||
|
22
components/ui/Quantity/Quantity.module.css
Normal file
22
components/ui/Quantity/Quantity.module.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
@apply bg-transparent px-4 w-full h-full focus:outline-none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
62
components/ui/Quantity/Quantity.tsx
Normal file
62
components/ui/Quantity/Quantity.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import s from './Quantity.module.css'
|
||||||
|
import { Cross, Plus, Minus } from '@components/icons'
|
||||||
|
import cn from 'classnames'
|
||||||
|
export interface QuantityProps {
|
||||||
|
value: number
|
||||||
|
increase: () => any
|
||||||
|
decrease: () => any
|
||||||
|
handleRemove: React.MouseEventHandler<HTMLButtonElement>
|
||||||
|
handleChange: React.ChangeEventHandler<HTMLInputElement>
|
||||||
|
max?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const Quantity: FC<QuantityProps> = ({
|
||||||
|
value,
|
||||||
|
increase,
|
||||||
|
decrease,
|
||||||
|
handleChange,
|
||||||
|
handleRemove,
|
||||||
|
max = 6,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row h-9">
|
||||||
|
<button className={s.actions} onClick={handleRemove}>
|
||||||
|
<Cross width={20} height={20} />
|
||||||
|
</button>
|
||||||
|
<label className="w-full border-accent-2 border ml-2">
|
||||||
|
<input
|
||||||
|
className={s.input}
|
||||||
|
onChange={(e) =>
|
||||||
|
Number(e.target.value) < max + 1 ? handleChange(e) : () => {}
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
type="number"
|
||||||
|
max={max}
|
||||||
|
min="0"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={decrease}
|
||||||
|
className={s.actions}
|
||||||
|
style={{ marginLeft: '-1px' }}
|
||||||
|
disabled={value <= 1}
|
||||||
|
>
|
||||||
|
<Minus width={18} height={18} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={increase}
|
||||||
|
className={cn(s.actions)}
|
||||||
|
style={{ marginLeft: '-1px' }}
|
||||||
|
disabled={value < 1 || value >= max}
|
||||||
|
>
|
||||||
|
<Plus width={18} height={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Quantity
|
2
components/ui/Quantity/index.ts
Normal file
2
components/ui/Quantity/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './Quantity'
|
||||||
|
export * from './Quantity'
|
0
components/ui/Rating/Rating.module.css
Normal file
0
components/ui/Rating/Rating.module.css
Normal file
27
components/ui/Rating/Rating.tsx
Normal file
27
components/ui/Rating/Rating.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import rangeMap from '@lib/range-map'
|
||||||
|
import { Star } from '@components/icons'
|
||||||
|
import cn from 'classnames'
|
||||||
|
|
||||||
|
export interface RatingProps {
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const Quantity: FC<RatingProps> = ({ value = 5 }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row py-6 text-accent-9">
|
||||||
|
{rangeMap(5, (i) => (
|
||||||
|
<span
|
||||||
|
key={`star_${i}`}
|
||||||
|
className={cn('inline-block ml-1 ', {
|
||||||
|
'text-accent-5': i >= Math.floor(value),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Star />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Quantity
|
2
components/ui/Rating/index.ts
Normal file
2
components/ui/Rating/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './Rating'
|
||||||
|
export * from './Rating'
|
@ -7,13 +7,13 @@ import {
|
|||||||
clearAllBodyScrollLocks,
|
clearAllBodyScrollLocks,
|
||||||
} from 'body-scroll-lock'
|
} from 'body-scroll-lock'
|
||||||
|
|
||||||
interface Props {
|
interface SidebarProps {
|
||||||
children: any
|
children: any
|
||||||
open: boolean
|
open: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar: FC<Props> = ({ children, open = false, onClose }) => {
|
const Sidebar: FC<SidebarProps> = ({ children, open = false, onClose }) => {
|
||||||
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -3,7 +3,7 @@ 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
|
width?: string | number
|
||||||
height?: string | number
|
height?: string | number
|
||||||
boxHeight?: string | number
|
boxHeight?: string | number
|
||||||
@ -13,7 +13,7 @@ interface Props {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Skeleton: React.FC<Props> = ({
|
const Skeleton: React.FC<SkeletonProps> = ({
|
||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
@ -6,7 +6,7 @@ import React, {
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import s from './Text.module.css'
|
import s from './Text.module.css'
|
||||||
|
|
||||||
interface Props {
|
interface TextProps {
|
||||||
variant?: Variant
|
variant?: Variant
|
||||||
className?: string
|
className?: string
|
||||||
style?: CSSProperties
|
style?: CSSProperties
|
||||||
@ -17,7 +17,7 @@ interface Props {
|
|||||||
|
|
||||||
type Variant = 'heading' | 'body' | 'pageHeading' | 'sectionHeading'
|
type Variant = 'heading' | 'body' | 'pageHeading' | 'sectionHeading'
|
||||||
|
|
||||||
const Text: FunctionComponent<Props> = ({
|
const Text: FunctionComponent<TextProps> = ({
|
||||||
style,
|
style,
|
||||||
className = '',
|
className = '',
|
||||||
variant = 'body',
|
variant = 'body',
|
||||||
|
@ -11,4 +11,6 @@ export { default as Modal } from './Modal'
|
|||||||
export { default as Text } from './Text'
|
export { default as Text } from './Text'
|
||||||
export { default as Input } from './Input'
|
export { default as Input } from './Input'
|
||||||
export { default as Collapse } from './Collapse'
|
export { default as Collapse } from './Collapse'
|
||||||
|
export { default as Quantity } from './Quantity'
|
||||||
|
export { default as Rating } from './Rating'
|
||||||
export { useUI } from './context'
|
export { useUI } from './context'
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
import { Product } from '@commerce/types'
|
|
||||||
import { getConfig, ShopifyConfig } from '../api'
|
|
||||||
import fetchAllProducts from '../api/utils/fetch-all-products'
|
|
||||||
import { ProductEdge } from '../schema'
|
|
||||||
import getAllProductsPathsQuery from '../utils/queries/get-all-products-paths-query'
|
|
||||||
|
|
||||||
type ProductPath = {
|
|
||||||
path: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProductPathNode = {
|
|
||||||
node: ProductPath
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReturnType = {
|
|
||||||
products: ProductPathNode[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllProductPaths = async (options?: {
|
|
||||||
variables?: any
|
|
||||||
config?: ShopifyConfig
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<ReturnType> => {
|
|
||||||
let { config, variables = { first: 250 } } = options ?? {}
|
|
||||||
config = getConfig(config)
|
|
||||||
|
|
||||||
const products = await fetchAllProducts({
|
|
||||||
config,
|
|
||||||
query: getAllProductsPathsQuery,
|
|
||||||
variables,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
products: products?.map(({ node: { handle } }: ProductEdge) => ({
|
|
||||||
node: {
|
|
||||||
path: `/${handle}`,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getAllProductPaths
|
|
@ -1,40 +0,0 @@
|
|||||||
import { GraphQLFetcherResult } from '@commerce/api'
|
|
||||||
import { getConfig, ShopifyConfig } from '../api'
|
|
||||||
import { ProductEdge } from '../schema'
|
|
||||||
import { getAllProductsQuery } from '../utils/queries'
|
|
||||||
import { normalizeProduct } from '../utils/normalize'
|
|
||||||
import { Product } from '@commerce/types'
|
|
||||||
|
|
||||||
type Variables = {
|
|
||||||
first?: number
|
|
||||||
field?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReturnType = {
|
|
||||||
products: Product[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllProducts = async (options: {
|
|
||||||
variables?: Variables
|
|
||||||
config?: ShopifyConfig
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<ReturnType> => {
|
|
||||||
let { config, variables = { first: 250 } } = options ?? {}
|
|
||||||
config = getConfig(config)
|
|
||||||
|
|
||||||
const { data }: GraphQLFetcherResult = await config.fetch(
|
|
||||||
getAllProductsQuery,
|
|
||||||
{ variables }
|
|
||||||
)
|
|
||||||
|
|
||||||
const products =
|
|
||||||
data.products?.edges?.map(({ node: p }: ProductEdge) =>
|
|
||||||
normalizeProduct(p)
|
|
||||||
) ?? []
|
|
||||||
|
|
||||||
return {
|
|
||||||
products,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getAllProducts
|
|
@ -1,32 +0,0 @@
|
|||||||
import { GraphQLFetcherResult } from '@commerce/api'
|
|
||||||
import { getConfig, ShopifyConfig } from '../api'
|
|
||||||
import { normalizeProduct, getProductQuery } from '../utils'
|
|
||||||
|
|
||||||
type Variables = {
|
|
||||||
slug: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReturnType = {
|
|
||||||
product: any
|
|
||||||
}
|
|
||||||
|
|
||||||
const getProduct = async (options: {
|
|
||||||
variables: Variables
|
|
||||||
config: ShopifyConfig
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<ReturnType> => {
|
|
||||||
let { config, variables } = options ?? {}
|
|
||||||
config = getConfig(config)
|
|
||||||
|
|
||||||
const { data }: GraphQLFetcherResult = await config.fetch(getProductQuery, {
|
|
||||||
variables,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { productByHandle } = data
|
|
||||||
|
|
||||||
return {
|
|
||||||
product: productByHandle ? normalizeProduct(productByHandle) : null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getProduct
|
|
@ -55,14 +55,8 @@ export default function Home({
|
|||||||
))}
|
))}
|
||||||
</Marquee>
|
</Marquee>
|
||||||
<Hero
|
<Hero
|
||||||
headline="Release Details: The Yeezy BOOST 350 V2 ‘Natural'"
|
headline=" Dessert dragée halvah croissant."
|
||||||
description="
|
description="Cupcake ipsum dolor sit amet lemon drops pastry cotton candy. Sweet carrot cake macaroon bonbon croissant fruitcake jujubes macaroon oat cake. Soufflé bonbon caramels jelly beans. Tiramisu sweet roll cheesecake pie carrot cake. "
|
||||||
The Yeezy BOOST 350 V2 lineup continues to grow. We recently had the
|
|
||||||
‘Carbon’ iteration, and now release details have been locked in for
|
|
||||||
this ‘Natural’ joint. Revealed by Yeezy Mafia earlier this year, the
|
|
||||||
shoe was originally called ‘Abez’, which translated to ‘Tin’ in
|
|
||||||
Hebrew. It’s now undergone a name change, and will be referred to as
|
|
||||||
‘Natural’."
|
|
||||||
/>
|
/>
|
||||||
<Grid layout="B" variant="filled">
|
<Grid layout="B" variant="filled">
|
||||||
{products.slice(0, 3).map((product, i) => (
|
{products.slice(0, 3).map((product, i) => (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user