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;
}
.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 {
appearance: textfield;
@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 Image from 'next/image'
import Link from 'next/link'
@ -9,6 +9,7 @@ import type { LineItem } from '@commerce/types/cart'
import usePrice from '@framework/product/use-price'
import useUpdateItem from '@framework/cart/use-update-item'
import useRemoveItem from '@framework/cart/use-remove-item'
import Quantity from '@components/ui/Quantity'
type ItemOption = {
name: string
@ -28,6 +29,10 @@ const CartItem = ({
currencyCode: string
}) => {
const { closeSidebarIfPresent } = useUI()
const [removing, setRemoving] = useState(false)
const [quantity, setQuantity] = useState<number>(item.quantity)
const removeItem = useRemoveItem()
const updateItem = useUpdateItem({ item })
const { price } = usePrice({
amount: item.variant.price * item.quantity,
@ -35,43 +40,22 @@ const CartItem = ({
currencyCode,
})
const updateItem = useUpdateItem({ item })
const removeItem = useRemoveItem()
const [quantity, setQuantity] = useState<number | ''>(item.quantity)
const [removing, setRemoving] = useState(false)
const updateQuantity = async (val: number) => {
await updateItem({ quantity: val })
const handleChange = async ({
target: { value },
}: ChangeEvent<HTMLInputElement>) => {
setQuantity(Number(value))
await updateItem({ quantity: Number(value) })
}
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 increaseQuantity = async (n = 1) => {
const val = Number(quantity) + n
if (Number.isInteger(val) && val >= 0) {
setQuantity(val)
updateQuantity(val)
}
await updateItem({ quantity: val })
}
const handleRemove = async () => {
setRemoving(true)
try {
// If this action succeeds then there's no need to do `setRemoving(true)`
// because the component will be removed from the view
await removeItem(item)
} catch (error) {
setRemoving(false)
@ -152,38 +136,13 @@ const CartItem = ({
</div>
</div>
{variant === 'default' && (
<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
type="number"
max={99}
min={0}
className="bg-transparent px-4 w-full h-full focus:outline-none"
<Quantity
value={quantity}
onChange={handleQuantity}
onBlur={handleBlur}
handleRemove={handleRemove}
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>
)

View File

@ -89,7 +89,7 @@ const CartSidebarView: FC = () => {
</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 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">
<li className="flex justify-between py-1">
<span>Subtotal</span>

View File

@ -50,7 +50,7 @@ const CheckoutSidebarView: FC = () => {
</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 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">
<li className="flex justify-between py-1">
<span>Subtotal</span>

View File

@ -3,11 +3,11 @@
}
.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 {
@apply border-accent-4 shadow-sm;
@apply border-accent-3 shadow-sm;
}
.button:focus {
@ -38,5 +38,9 @@
}
.icon {
transition: transform 0.2s ease;
}
.icon.active {
transform: rotate(180deg);
}

View File

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

View File

@ -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"
>
<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
</span>
</button>

View File

@ -1,6 +1,6 @@
.root {
@apply relative w-full h-full;
overflow-y: hidden;
@apply relative w-full h-full select-none;
overflow: hidden;
}
.slider {
@ -14,7 +14,7 @@
.control {
@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;
}
@ -59,11 +59,16 @@
}
.album {
width: 100%;
height: 100%;
@apply bg-violet-dark;
box-sizing: content-box;
overflow-y: hidden;
overflow-x: auto;
white-space: nowrap;
height: 125px;
padding-bottom: 10px;
margin-bottom: -10px;
}
@screen md {

View File

@ -9,11 +9,11 @@ import { getVariant, SelectedOptions } from '../helpers'
import { Swatch, ProductSlider } from '@components/product'
import { Button, Container, Text, useUI } from '@components/ui'
import { useAddItem } from '@framework/cart'
import Rating from '@components/ui/Rating'
import Collapse from '@components/ui/Collapse'
import ProductCard from '@components/product/ProductCard'
import WishlistButton from '@components/wishlist/WishlistButton'
import rangeMap from '@lib/range-map'
import { Star } from '@components/icons'
interface Props {
children?: any
product: Product
@ -154,20 +154,10 @@ const ProductView: FC<Props> = ({ product, relatedProducts }) => {
</div>
</section>
<div className="flex flex-row justify-between items-center">
{/**
* TODO make component Rate stars={}
*/}
<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>
<Rating value={2} />
<div className="text-accent-6 pr-1 font-medium select-none">
36 reviews
</div>
<div className="text-accent-6 pr-1">36 reviews</div>
</div>
<div>
<Button

View File

@ -3,7 +3,7 @@
composes: root from '@components/ui/Button/Button.module.css';
@apply h-10 w-10 bg-primary text-primary rounded-full mr-3 inline-flex
items-center justify-center cursor-pointer transition duration-150 ease-in-out
p-0 shadow-none border-gray-200 border box-border;
p-0 shadow-none border-gray-200 border box-border select-none;
margin-right: calc(0.75rem - 1px);
overflow: hidden;
width: 48px;

View File

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

View File

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

View File

@ -3,13 +3,13 @@ import { Container } from '@components/ui'
import { RightArrow } from '@components/icons'
import s from './Hero.module.css'
import Link from 'next/link'
interface Props {
interface HeroProps {
className?: string
headline: string
description: string
}
const Hero: FC<Props> = ({ headline, description }) => {
const Hero: FC<HeroProps> = ({ headline, description }) => {
return (
<div className="bg-black">
<Container>

View File

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

View File

@ -3,13 +3,13 @@ import s from './Marquee.module.css'
import { FC, ReactNode, Component } from 'react'
import Ticker from 'react-ticker'
interface Props {
interface MarqueeProps {
className?: string
children?: ReactNode[] | Component[] | any[]
variant?: 'primary' | 'secondary'
}
const Marquee: FC<Props> = ({
const Marquee: FC<MarqueeProps> = ({
className = '',
children,
variant = 'primary',

View File

@ -8,7 +8,7 @@ import {
clearAllBodyScrollLocks,
} from 'body-scroll-lock'
import FocusTrap from '@lib/focus-trap'
interface Props {
interface ModalProps {
className?: string
children?: any
open?: boolean
@ -16,7 +16,7 @@ interface Props {
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 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,
} from 'body-scroll-lock'
interface Props {
interface SidebarProps {
children: any
open: boolean
onClose: () => void
}
const Sidebar: FC<Props> = ({ children, open = false, onClose }) => {
const Sidebar: FC<SidebarProps> = ({ children, open = false, onClose }) => {
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
useEffect(() => {

View File

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

View File

@ -6,7 +6,7 @@ import React, {
import cn from 'classnames'
import s from './Text.module.css'
interface Props {
interface TextProps {
variant?: Variant
className?: string
style?: CSSProperties
@ -17,7 +17,7 @@ interface Props {
type Variant = 'heading' | 'body' | 'pageHeading' | 'sectionHeading'
const Text: FunctionComponent<Props> = ({
const Text: FunctionComponent<TextProps> = ({
style,
className = '',
variant = 'body',

View File

@ -11,4 +11,6 @@ export { default as Modal } from './Modal'
export { default as Text } from './Text'
export { default as Input } from './Input'
export { default as Collapse } from './Collapse'
export { default as Quantity } from './Quantity'
export { default as Rating } from './Rating'
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>
<Hero
headline="Release Details: The Yeezy BOOST 350 V2 Natural'"
description="
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."
headline=" Dessert dragée halvah croissant."
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. "
/>
<Grid layout="B" variant="filled">
{products.slice(0, 3).map((product, i) => (