mirror of
https://github.com/vercel/commerce.git
synced 2025-06-20 06:01:21 +00:00
Restricting user to add more product items than the stock has
This commit is contained in:
parent
bf0b9807e6
commit
6b49f58eaa
@ -8,7 +8,7 @@ import type { Page } from '@commerce/types/page'
|
||||
import { Navbar, Footer } from '@components/common'
|
||||
import type { Category } from '@commerce/types/site'
|
||||
import ShippingView from '@theme/checkout/ShippingView'
|
||||
import CartSidebarView from '@components/cart/CartSidebarView'
|
||||
import CartSidebarView from '@theme/cart/CartSidebarView'
|
||||
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
|
||||
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
|
||||
import PaymentMethodView from '@components/checkout/PaymentMethodView'
|
||||
|
@ -54,6 +54,7 @@ export type ProductVariant = {
|
||||
// The variant's depth. If a depth was not explicitly specified on the
|
||||
// variant, this will be the product's depth.
|
||||
depth?: Measurement
|
||||
stockLevel?: string
|
||||
}
|
||||
|
||||
// Shopping cart, a.k.a Checkout
|
||||
|
@ -46,6 +46,7 @@ export function normalizeCart(order: CartFragment): Cart {
|
||||
sku: l.productVariant.sku,
|
||||
price: l.discountedUnitPriceWithTax / 100,
|
||||
listPrice: l.unitPriceWithTax / 100,
|
||||
stockLevel: l.productVariant.stockLevel,
|
||||
image: {
|
||||
url: l.featuredAsset?.preview + '?preset=thumb' || '',
|
||||
},
|
||||
|
@ -5,7 +5,7 @@ import commerce from '@lib/api/commerce'
|
||||
import { Layout } from '@components/common'
|
||||
import { Button, Text } from '@components/ui'
|
||||
import { Bag, Cross, Check, MapPin, CreditCard } from '@components/icons'
|
||||
import { CartItem } from '@components/cart'
|
||||
import { CartItem } from '@theme/cart'
|
||||
|
||||
export async function getStaticProps({
|
||||
preview,
|
||||
|
32
theme/dap/cart/CartItem/CartItem.module.css
Normal file
32
theme/dap/cart/CartItem/CartItem.module.css
Normal file
@ -0,0 +1,32 @@
|
||||
.root {
|
||||
@apply flex flex-col py-4;
|
||||
}
|
||||
|
||||
.root:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.quantity {
|
||||
appearance: textfield;
|
||||
@apply w-8 border-accent-2 border mx-3 rounded text-center text-sm text-black;
|
||||
}
|
||||
|
||||
.quantity::-webkit-outer-spin-button,
|
||||
.quantity::-webkit-inner-spin-button {
|
||||
@apply appearance-none m-0;
|
||||
}
|
||||
|
||||
.productImage {
|
||||
position: absolute;
|
||||
transform: scale(1.9);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 30% !important;
|
||||
top: 30% !important;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.productName {
|
||||
@apply font-medium cursor-pointer pb-1;
|
||||
margin-top: -4px;
|
||||
}
|
152
theme/dap/cart/CartItem/CartItem.tsx
Normal file
152
theme/dap/cart/CartItem/CartItem.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
import { ChangeEvent, FocusEventHandler, useEffect, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import s from './CartItem.module.css'
|
||||
import { Trash, Plus, Minus, Cross } from '@components/icons'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import type { LineItem } from '@commerce/types/cart'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
import useUpdateItem from '@framework/cart/use-update-item'
|
||||
import useRemoveItem from '@framework/cart/use-remove-item'
|
||||
import Quantity from '@components/ui/Quantity'
|
||||
|
||||
type ItemOption = {
|
||||
name: string
|
||||
nameId: number
|
||||
value: string
|
||||
valueId: number
|
||||
}
|
||||
|
||||
const CartItem = ({
|
||||
item,
|
||||
variant = 'default',
|
||||
currencyCode,
|
||||
...rest
|
||||
}: {
|
||||
variant?: 'default' | 'display'
|
||||
item: LineItem
|
||||
currencyCode: string
|
||||
}) => {
|
||||
const { closeSidebarIfPresent } = useUI()
|
||||
const [removing, setRemoving] = useState(false)
|
||||
const [quantity, setQuantity] = useState<number>(item.quantity)
|
||||
const removeItem = useRemoveItem()
|
||||
const updateItem = useUpdateItem({ item })
|
||||
|
||||
const { price } = usePrice({
|
||||
amount: item.variant.price * item.quantity,
|
||||
baseAmount: item.variant.listPrice * item.quantity,
|
||||
currencyCode,
|
||||
})
|
||||
|
||||
const handleChange = async ({
|
||||
target: { value },
|
||||
}: ChangeEvent<HTMLInputElement>) => {
|
||||
setQuantity(Number(value))
|
||||
await updateItem({ quantity: Number(value) })
|
||||
}
|
||||
|
||||
const increaseQuantity = async (n = 1) => {
|
||||
const val = Number(quantity) + n
|
||||
setQuantity(val)
|
||||
await updateItem({ quantity: val })
|
||||
}
|
||||
|
||||
const handleRemove = async () => {
|
||||
setRemoving(true)
|
||||
try {
|
||||
await removeItem(item)
|
||||
} catch (error) {
|
||||
setRemoving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add a type for this
|
||||
const options = (item as any).options
|
||||
|
||||
useEffect(() => {
|
||||
// Reset the quantity state if the item quantity changes
|
||||
if (item.quantity !== Number(quantity)) {
|
||||
setQuantity(item.quantity)
|
||||
}
|
||||
}, [item.quantity])
|
||||
|
||||
return (
|
||||
<li
|
||||
className={cn(s.root, {
|
||||
'opacity-50 pointer-events-none': removing,
|
||||
})}
|
||||
{...rest}
|
||||
>
|
||||
<div className="flex flex-row space-x-4 py-4">
|
||||
<div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer z-0">
|
||||
<Link href={`/product/${item.path}`}>
|
||||
<Image
|
||||
onClick={() => closeSidebarIfPresent()}
|
||||
className={s.productImage}
|
||||
width={150}
|
||||
height={150}
|
||||
src={item.variant.image!.url}
|
||||
alt={item.variant.image!.altText}
|
||||
unoptimized
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col text-base">
|
||||
<Link href={`/product/${item.path}`}>
|
||||
<span
|
||||
className={s.productName}
|
||||
onClick={() => closeSidebarIfPresent()}
|
||||
>
|
||||
{item.name}
|
||||
</span>
|
||||
</Link>
|
||||
{options && options.length > 0 && (
|
||||
<div className="flex items-center pb-1">
|
||||
{options.map((option: ItemOption, i: number) => (
|
||||
<div
|
||||
key={`${item.id}-${option.name}`}
|
||||
className="text-sm font-semibold text-accent-7 inline-flex items-center justify-center"
|
||||
>
|
||||
{option.name}
|
||||
{option.name === 'Color' ? (
|
||||
<span
|
||||
className="mx-2 rounded-full bg-transparent border w-5 h-5 p-1 text-accent-9 inline-flex items-center justify-center overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: `${option.value}`,
|
||||
}}
|
||||
></span>
|
||||
) : (
|
||||
<span className="mx-2 rounded-full bg-transparent border h-5 p-1 text-accent-9 inline-flex items-center justify-center overflow-hidden">
|
||||
{option.value}
|
||||
</span>
|
||||
)}
|
||||
{i === options.length - 1 ? '' : <span className="mr-3" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{variant === 'display' && (
|
||||
<div className="text-sm tracking-wider">{quantity}x</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col justify-between space-y-2 text-sm">
|
||||
<span>{price}</span>
|
||||
</div>
|
||||
</div>
|
||||
{variant === 'default' && (
|
||||
<Quantity
|
||||
value={quantity}
|
||||
handleRemove={handleRemove}
|
||||
handleChange={handleChange}
|
||||
increase={() => increaseQuantity(1)}
|
||||
decrease={() => increaseQuantity(-1)}
|
||||
max={Number(item.variant.stockLevel)}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default CartItem
|
1
theme/dap/cart/CartItem/index.ts
Normal file
1
theme/dap/cart/CartItem/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './CartItem'
|
11
theme/dap/cart/CartSidebarView/CartSidebarView.module.css
Normal file
11
theme/dap/cart/CartSidebarView/CartSidebarView.module.css
Normal file
@ -0,0 +1,11 @@
|
||||
.root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.root.empty {
|
||||
@apply bg-secondary text-secondary;
|
||||
}
|
||||
|
||||
.lineItemsList {
|
||||
@apply py-4 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accent-2 border-accent-2;
|
||||
}
|
129
theme/dap/cart/CartSidebarView/CartSidebarView.tsx
Normal file
129
theme/dap/cart/CartSidebarView/CartSidebarView.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import cn from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import { FC } from 'react'
|
||||
import s from './CartSidebarView.module.css'
|
||||
import CartItem from '../CartItem'
|
||||
import { Button, Text } from '@components/ui'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { Bag, Cross, Check } from '@components/icons'
|
||||
import useCart from '@framework/cart/use-cart'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
import SidebarLayout from '@components/common/SidebarLayout'
|
||||
|
||||
const CartSidebarView: FC = () => {
|
||||
const { closeSidebar, setSidebarView } = useUI()
|
||||
const { data, isLoading, isEmpty } = 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,
|
||||
}
|
||||
)
|
||||
const handleClose = () => closeSidebar()
|
||||
const goToCheckout = () => setSidebarView('CHECKOUT_VIEW')
|
||||
|
||||
const error = null
|
||||
const success = null
|
||||
|
||||
return (
|
||||
<SidebarLayout
|
||||
className={cn({
|
||||
[s.empty]: error || success || isLoading || isEmpty,
|
||||
})}
|
||||
handleClose={handleClose}
|
||||
>
|
||||
{isLoading || isEmpty ? (
|
||||
<div className="flex-1 px-4 flex flex-col justify-center items-center">
|
||||
<span className="border border-dashed border-primary rounded-full flex items-center justify-center w-16 h-16 p-12 bg-secondary text-secondary">
|
||||
<Bag className="absolute" />
|
||||
</span>
|
||||
<h2 className="pt-6 text-2xl font-bold tracking-wide text-center">
|
||||
Your cart is empty
|
||||
</h2>
|
||||
<p className="text-accent-3 px-10 text-center pt-2">
|
||||
Biscuit oat cake wafer icing ice cream tiramisu pudding cupcake.
|
||||
</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex-1 px-4 flex flex-col justify-center items-center">
|
||||
<span className="border border-white rounded-full flex items-center justify-center w-16 h-16">
|
||||
<Cross width={24} height={24} />
|
||||
</span>
|
||||
<h2 className="pt-6 text-xl font-light text-center">
|
||||
We couldn’t process the purchase. Please check your card information
|
||||
and try again.
|
||||
</h2>
|
||||
</div>
|
||||
) : success ? (
|
||||
<div className="flex-1 px-4 flex flex-col justify-center items-center">
|
||||
<span className="border border-white rounded-full flex items-center justify-center w-16 h-16">
|
||||
<Check />
|
||||
</span>
|
||||
<h2 className="pt-6 text-xl font-light text-center">
|
||||
Thank you for your order.
|
||||
</h2>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="px-4 sm:px-6 flex-1">
|
||||
<Link href="/cart">
|
||||
<Text variant="sectionHeading" onClick={handleClose}>
|
||||
My Cart
|
||||
</Text>
|
||||
</Link>
|
||||
<ul className={s.lineItemsList}>
|
||||
{data!.lineItems.map((item: any) => (
|
||||
<CartItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
currencyCode={data!.currency.code}
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
</SidebarLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default CartSidebarView
|
1
theme/dap/cart/CartSidebarView/index.ts
Normal file
1
theme/dap/cart/CartSidebarView/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './CartSidebarView'
|
2
theme/dap/cart/index.ts
Normal file
2
theme/dap/cart/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as CartSidebarView } from './CartSidebarView'
|
||||
export { default as CartItem } from './CartItem'
|
@ -17,7 +17,7 @@ import request from '@commerce/utils/request'
|
||||
const CheckoutSidebarView: FC = () => {
|
||||
const { setSidebarView, closeSidebarIfPresent } = useUI()
|
||||
const { data } = useCart()
|
||||
|
||||
console.log(data, 555)
|
||||
const { price: subTotal } = usePrice(
|
||||
data && {
|
||||
amount: Number(data.subtotalPrice),
|
||||
@ -109,12 +109,17 @@ const CheckoutSidebarView: FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
{/* Once data is correcly filled */}
|
||||
{data?.customerId ? (
|
||||
<Button Component="a" width="100%" onClick={handleConfirmPurchase}>
|
||||
Confirm Purchase
|
||||
</Button>
|
||||
{/* <Button Component="a" width="100%" variant="ghost" disabled>
|
||||
) : (
|
||||
<Button Component="a" width="100%" variant="ghost" disabled>
|
||||
Continue
|
||||
</Button> */}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { FC, useState, useCallback, useEffect } from 'react'
|
||||
import cn from 'classnames'
|
||||
import s from './ShippingView.module.css'
|
||||
import { Button, Input } from '@components/ui'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import SidebarLayout from '@components/common/SidebarLayout'
|
||||
|
||||
import { validate } from 'email-validator'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { setCustomerForOrderMutation } from '@framework/utils/mutations/set-customer-for-order-mutation'
|
||||
import { setOrderShippingAddressMutation } from '@framework/utils/mutations/set-order-shipping-address-mutation'
|
||||
@ -19,7 +20,7 @@ const PaymentMethodView: FC = () => {
|
||||
const [streetDetails, setStreetDetails] = useState('')
|
||||
const [apartmentDetails, setApartmentDetails] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [disabled, setDisabled] = useState(true)
|
||||
const { setSidebarView } = useUI()
|
||||
|
||||
const setCustomerForOrder = async () => {
|
||||
@ -54,10 +55,30 @@ const PaymentMethodView: FC = () => {
|
||||
})
|
||||
console.log(data, 222)
|
||||
}
|
||||
const handleValidation = useCallback(
|
||||
() => {
|
||||
|
||||
setDisabled(
|
||||
!validate(email)
|
||||
|| firstName.length < 3
|
||||
|| lastName.length < 3
|
||||
|| phoneNumber.length < 6
|
||||
|| streetDetails.length < 5
|
||||
)
|
||||
},
|
||||
[
|
||||
firstName,
|
||||
lastName,
|
||||
phoneNumber,
|
||||
email,
|
||||
streetDetails,
|
||||
apartmentDetails
|
||||
],
|
||||
)
|
||||
|
||||
const handleAddShippingAddress = async (e: React.SyntheticEvent<EventTarget>) => {
|
||||
e.preventDefault()
|
||||
|
||||
handleValidation()
|
||||
console.log('Handle Add Shipping Address')
|
||||
|
||||
try {
|
||||
@ -71,6 +92,10 @@ const PaymentMethodView: FC = () => {
|
||||
}
|
||||
|
||||
}
|
||||
useEffect(() => {
|
||||
handleValidation()
|
||||
}, [handleValidation])
|
||||
|
||||
return (
|
||||
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
|
||||
<form
|
||||
@ -143,6 +168,7 @@ const PaymentMethodView: FC = () => {
|
||||
type="submit"
|
||||
loading={loading}
|
||||
width="100%"
|
||||
disabled={disabled}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
|
Loading…
x
Reference in New Issue
Block a user