diff options
| author | IT Fixcomart <it@fixcomart.co.id> | 2024-01-04 03:07:56 +0000 |
|---|---|---|
| committer | IT Fixcomart <it@fixcomart.co.id> | 2024-01-04 03:07:56 +0000 |
| commit | 0d33de3744f612262c12d648cd7147a2ef238a36 (patch) | |
| tree | 285af0ed69169621228e252affdac958f016dab2 /src-migrate/modules/cart/components | |
| parent | bb8ee26d89842b4f9b99b48f2a7cc464c6ecc4ee (diff) | |
| parent | 67398e6f10d6f7729d8f1ace7005ef13d32c5ddd (diff) | |
Merged in Feature/promotion-program (pull request #124)
Feature/promotion program
Diffstat (limited to 'src-migrate/modules/cart/components')
| -rw-r--r-- | src-migrate/modules/cart/components/Detail.tsx | 83 | ||||
| -rw-r--r-- | src-migrate/modules/cart/components/Item.tsx | 110 | ||||
| -rw-r--r-- | src-migrate/modules/cart/components/ItemAction.tsx | 105 | ||||
| -rw-r--r-- | src-migrate/modules/cart/components/ItemPromo.tsx | 40 | ||||
| -rw-r--r-- | src-migrate/modules/cart/components/ItemSelect.tsx | 47 | ||||
| -rw-r--r-- | src-migrate/modules/cart/components/Summary.tsx | 75 |
6 files changed, 460 insertions, 0 deletions
diff --git a/src-migrate/modules/cart/components/Detail.tsx b/src-migrate/modules/cart/components/Detail.tsx new file mode 100644 index 00000000..ccb0bb4d --- /dev/null +++ b/src-migrate/modules/cart/components/Detail.tsx @@ -0,0 +1,83 @@ +import style from '../styles/detail.module.css' + +import React, { useEffect, useMemo } from 'react' +import Link from 'next/link' +import { Button, Tooltip } from '@chakra-ui/react' + +import { getAuth } from '~/common/libs/auth' +import { useCartStore } from '../stores/useCartStore' + +import CartItem from './Item' +import CartSummary from './Summary' + +const CartDetail = () => { + const auth = getAuth() + + const { loadCart, cart, summary } = useCartStore() + + useEffect(() => { + if (typeof auth === 'object' && !cart) loadCart(auth.id) + }, [auth, loadCart, cart]) + + const hasSelectedPromo = useMemo(() => { + if (!cart) return false + for (const item of cart.products) { + if (item.cart_type === 'promotion' && item.selected) return true + } + return false + }, [cart]) + + const hasSelected = useMemo(() => { + if (!cart) return false + for (const item of cart.products) { + if (item.selected) return true + } + return false + }, [cart]) + + return ( + <div className={style.wrapper}> + <div className='w-full md:w-3/4'> + <div className=''> + <div className='text-h-lg font-semibold mb-6'>Keranjang Belanja</div> + <div className='grid grid-cols-1 gap-y-4'> + {!cart && <CartItem.Skeleton count={5} height='120px' />} + </div> + <div className='flex flex-col gap-y-8 border-t border-gray-300 pt-8'> + {cart?.products.map((item) => <CartItem key={item.id} item={item} />)} + </div> + </div> + </div> + + <div className='w-full md:w-1/4 md:pl-6 mt-6 md:mt-0'> + <div className='border border-gray-300 p-4 rounded-md sticky top-[180px]'> + <CartSummary {...summary} isLoaded={!!cart} /> + <div className='grid grid-cols-2 gap-x-3 mt-6'> + <Tooltip label={hasSelectedPromo ? 'Barang promo tidak dapat dibuat quotation' : ''}> + <Button + colorScheme='yellow' + w='full' + isDisabled={hasSelectedPromo || !hasSelected} + > + Quotation + </Button> + </Tooltip> + <Tooltip label={hasSelected ? '' : 'Tidak ada item yang dipilih'}> + <Button + colorScheme='red' + w='full' + isDisabled={!hasSelected} + as={Link} + href='/shop/checkout' + > + Checkout + </Button> + </Tooltip> + </div> + </div> + </div> + </div> + ) +} + +export default CartDetail
\ No newline at end of file diff --git a/src-migrate/modules/cart/components/Item.tsx b/src-migrate/modules/cart/components/Item.tsx new file mode 100644 index 00000000..baf48bb6 --- /dev/null +++ b/src-migrate/modules/cart/components/Item.tsx @@ -0,0 +1,110 @@ +import style from '../styles/item.module.css' + +import Image from 'next/image' +import React from 'react' +import { Skeleton, SkeletonProps, Tooltip } from '@chakra-ui/react' +import { InfoIcon } from 'lucide-react' + +import { PROMO_CATEGORY } from '~/constants/promotion' + +import formatCurrency from '~/common/libs/formatCurrency' +import { CartItem as CartItemProps } from '~/common/types/cart' + +import CartItemPromo from './ItemPromo' +import CartItemAction from './ItemAction' +import CartItemSelect from './ItemSelect' + +type Props = { + item: CartItemProps + editable?: boolean +} + +const CartItem = ({ item, editable = true }: Props) => { + const image = item?.image || item?.parent?.image + + return ( + <div className={style.wrapper}> + {item.cart_type === 'promotion' && ( + <div className={style.header}> + {item.promotion_type?.value && ( + <Tooltip label={PROMO_CATEGORY[item.promotion_type?.value].description} placement="top" bgColor='red.600' p={2} rounded={6}> + <div className={style.badgeType}> + Paket {PROMO_CATEGORY[item.promotion_type?.value].alias} + <InfoIcon size={14} /> + </div> + </Tooltip> + )} + <div className='w-2' /> + <div> + Selamat! Pembelian anda lebih hemat {' '} + <span className={style.savingAmt}> + Rp{formatCurrency((item.package_price || 0) * item.quantity - item.subtotal)} + </span> + </div> + </div> + )} + + <div className={style.mainProdWrapper}> + {editable && <CartItemSelect item={item} />} + <div className='w-4' /> + <div className={style.image}> + {image && <Image src={image} alt={item.name} width={128} height={128} />} + {!image && <div className={style.noImage}>No Image</div>} + </div> + + <div className={style.details}> + <div className={style.name}>{item.name}</div> + <div className='mt-2 flex justify-between w-full'> + <div className='flex flex-col gap-y-1'> + {item.cart_type === 'promotion' && ( + <div className={style.discPriceSection}> + <span className={style.priceBefore}> + Rp {formatCurrency((item.package_price || 0))} + </span> + <span className={style.price}> + Rp {formatCurrency(item.price.price)} + </span> + </div> + )} + + {item.cart_type === 'product' && ( + <> + <div className={style.price}> + Rp {formatCurrency(item.price.price)} + </div> + <div>{item.code}</div> + </> + )} + + <div> + {item.weight} Kg + </div> + </div> + + {editable && <CartItemAction item={item} />} + {!editable && <div className={style.quantity}>{item.quantity}</div>} + </div> + </div> + + </div> + + <div className="flex flex-col"> + {item.products?.map((product) => <CartItemPromo key={product.id} product={product} />)} + {item.free_products?.map((product) => <CartItemPromo key={product.id} product={product} />)} + </div> + </div> + ) +} + +CartItem.Skeleton = function CartItemSkeleton(props: SkeletonProps & { count: number }) { + return Array.from({ length: props.count }).map((_, index) => ( + <Skeleton key={index} + height='100px' + width='100%' + rounded='md' + {...props} + /> + )) +} + +export default CartItem
\ No newline at end of file diff --git a/src-migrate/modules/cart/components/ItemAction.tsx b/src-migrate/modules/cart/components/ItemAction.tsx new file mode 100644 index 00000000..3e264aef --- /dev/null +++ b/src-migrate/modules/cart/components/ItemAction.tsx @@ -0,0 +1,105 @@ +import style from '../styles/item-action.module.css' + +import React, { useEffect, useState } from 'react' + +import { Spinner, Tooltip } from '@chakra-ui/react' +import { MinusIcon, PlusIcon, Trash2Icon } from 'lucide-react' + +import { CartItem } from '~/common/types/cart' +import { getAuth } from '~/common/libs/auth' +import { deleteUserCart, upsertUserCart } from '~/services/cart' + +import { useDebounce } from 'usehooks-ts' +import { useCartStore } from '../stores/useCartStore' + + +type Props = { + item: CartItem +} + +const CartItemAction = ({ item }: Props) => { + const auth = getAuth() + + const [isLoadDelete, setIsLoadDelete] = useState<boolean>(false) + const [isLoadQuantity, setIsLoadQuantity] = useState<boolean>(false) + + const [quantity, setQuantity] = useState<number>(item.quantity) + + const { loadCart } = useCartStore() + + const limitQty = item.limit_qty?.transaction || 0 + + const handleDelete = async () => { + if (typeof auth !== 'object') return + + setIsLoadDelete(true) + await deleteUserCart(auth.id, [item.cart_id]) + await loadCart(auth.id) + setIsLoadDelete(false) + } + + const decreaseQty = () => { setQuantity((quantity) => quantity -= 1) } + const increaseQty = () => { setQuantity((quantity) => quantity += 1) } + const debounceQty = useDebounce(quantity, 1000) + useEffect(() => { + if (isNaN(debounceQty)) setQuantity(1) + if (limitQty > 0 && debounceQty > limitQty) setQuantity(limitQty) + }, [debounceQty, limitQty]) + + useEffect(() => { + const updateCart = async () => { + if (typeof auth !== 'object' || isNaN(debounceQty)) return + + setIsLoadQuantity(true) + await upsertUserCart(auth.id, item.cart_type, item.id, debounceQty, item.selected) + await loadCart(auth.id) + setIsLoadQuantity(false) + } + updateCart() + //eslint-disable-next-line react-hooks/exhaustive-deps + }, [debounceQty]) + + return ( + <div className={style.actionSection}> + <button className={style.deleteButton} onClick={handleDelete} disabled={isLoadDelete}> + {isLoadDelete && <Spinner size='xs' />} + {!isLoadDelete && <Trash2Icon size={16} />} + </button> + + <div className={style.quantitySection}> + {isLoadQuantity && ( + <div className={style.quantityLoading}> + <Spinner size='sm' /> + </div> + )} + + <button + className={style.quantityControl} + onClick={decreaseQty} + disabled={quantity <= 1} + > + <MinusIcon size={16} /> + </button> + + <input + type='number' + className={style.quantity.toString()} + onChange={(e) => setQuantity(parseInt(e.target.value))} + value={quantity} + /> + + <Tooltip label={limitQty > 0 ? `Max. ${limitQty}` : ''}> + <button + className={style.quantityControl} + onClick={increaseQty} + disabled={limitQty > 0 && quantity >= limitQty} + > + <PlusIcon size={16} /> + </button> + </Tooltip> + </div> + </div> + ) +} + +export default CartItemAction
\ No newline at end of file diff --git a/src-migrate/modules/cart/components/ItemPromo.tsx b/src-migrate/modules/cart/components/ItemPromo.tsx new file mode 100644 index 00000000..bb286e8b --- /dev/null +++ b/src-migrate/modules/cart/components/ItemPromo.tsx @@ -0,0 +1,40 @@ +import style from '../styles/item-promo.module.css' + +import Image from 'next/image' +import React from 'react' + +import { CartProduct } from '~/common/types/cart' + +type Props = { + product: CartProduct +} + +const CartItemPromo = ({ product }: Props) => { + return ( + <div key={product.id} className={style.wrapper}> + <div className={style.imageWrapper}> + {product?.image && <Image src={product.image} alt={product.name} width={128} height={128} className={style.image} />} + </div> + + <div className={style.details}> + <div className={style.name}>{product.display_name}</div> + <div className='flex w-full'> + <div className="flex flex-col"> + <div className={style.code}>{product.code}</div> + <div> + <span className={style.weightLabel}>Berat Barang: </span> + <span>{product.package_weight} Kg</span> + </div> + </div> + + <div className={style.quantity}> + {product.qty} + </div> + </div> + </div> + + </div> + ) +} + +export default CartItemPromo
\ No newline at end of file diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx new file mode 100644 index 00000000..96e7c713 --- /dev/null +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -0,0 +1,47 @@ +import { Checkbox, Spinner } from '@chakra-ui/react' +import React, { useState } from 'react' + +import { getAuth } from '~/common/libs/auth' +import { CartItem } from '~/common/types/cart' +import { upsertUserCart } from '~/services/cart' + +import { useCartStore } from '../stores/useCartStore' + +type Props = { + item: CartItem +} + +const CartItemSelect = ({ item }: Props) => { + const auth = getAuth() + const { loadCart } = useCartStore() + + const [isLoad, setIsLoad] = useState<boolean>(false) + + const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => { + if (typeof auth !== 'object') return + + setIsLoad(true) + await upsertUserCart(auth.id, item.cart_type, item.id, item.quantity, e.target.checked) + await loadCart(auth.id) + setIsLoad(false) + } + + return ( + <div className='w-5 my-auto'> + {isLoad && ( + <Spinner className='my-auto' size='sm' /> + )} + {!isLoad && ( + <Checkbox + borderColor='gray.600' + colorScheme='red' + size='lg' + isChecked={item.selected} + onChange={handleChange} + /> + )} + </div> + ) +} + +export default CartItemSelect
\ No newline at end of file diff --git a/src-migrate/modules/cart/components/Summary.tsx b/src-migrate/modules/cart/components/Summary.tsx new file mode 100644 index 00000000..a835bca9 --- /dev/null +++ b/src-migrate/modules/cart/components/Summary.tsx @@ -0,0 +1,75 @@ +import style from '../styles/summary.module.css' + +import React from 'react' +import formatCurrency from '~/common/libs/formatCurrency' +import clsxm from '~/common/libs/clsxm' +import { Skeleton } from '@chakra-ui/react' +import _ from 'lodash' + +type Props = { + total?: number + discount?: number + subtotal?: number + tax?: number + shipping?: number + grandTotal?: number + isLoaded: boolean +} + +const CartSummary = ({ + total, + discount, + subtotal, + tax, + shipping, + grandTotal, + isLoaded = false, +}: Props) => { + return ( + <> + <div className='text-h-sm font-medium'>Ringkasan Pesanan</div> + + <div className="h-6" /> + + <div className='flex flex-col gap-y-3'> + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Total Belanja</span> + <span className={style.value}>Rp {formatCurrency(subtotal || 0)}</span> + </Skeleton> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Total Diskon</span> + <span className={clsxm(style.value, style.discount)}>- Rp {formatCurrency(discount || 0)}</span> + </Skeleton> + + <div className={style.divider} /> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Subtotal</span> + <span className={style.value}>Rp {formatCurrency(total || 0)}</span> + </Skeleton> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Tax 11%</span> + <span className={style.value}>Rp {formatCurrency(tax || 0)}</span> + </Skeleton> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={style.label}>Biaya Kirim</span> + <span className={style.value}>Rp {formatCurrency(shipping || 0)}</span> + </Skeleton> + + <div className={style.divider} /> + + <Skeleton isLoaded={isLoaded} className={style.line}> + <span className={clsxm(style.label, style.grandTotal)}> + Grand Total + </span> + <span className={style.value}>Rp {formatCurrency(grandTotal || 0)}</span> + </Skeleton> + </div> + </> + ) +} + +export default CartSummary
\ No newline at end of file |
