Merge branch 'custom-checkout' into improvements

This commit is contained in:
Shu Ding 2021-06-05 17:37:50 +08:00 committed by GitHub
commit 46bb71cca1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 187 additions and 244 deletions

View File

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

View File

@ -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)
updateQuantity(val) await updateItem({ quantity: 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}>
<Cross width={20} height={20} />
</button>
<label className="w-full border-accent-2 border ml-2">
<input
type="number"
max={99}
min={0}
className="bg-transparent px-4 w-full h-full focus:outline-none"
value={quantity} value={quantity}
onChange={handleQuantity} handleRemove={handleRemove}
onBlur={handleBlur} handleChange={handleChange}
increase={() => increaseQuantity(1)}
decrease={() => increaseQuantity(-1)}
/> />
</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>
) )

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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

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

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

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

View File

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

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

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

View File

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

View File

@ -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(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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. Its 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) => (