From c9366090153e8aba3a673b2b77cbc8acc24e59a5 Mon Sep 17 00:00:00 2001 From: Rafi Zadanly Date: Fri, 15 Dec 2023 17:15:32 +0700 Subject: Update promotion program feature --- src-migrate/modules/cart/components/CartDetail.tsx | 76 +++++++++++++++ .../modules/cart/components/CartItemAction.tsx | 104 +++++++++++++++++++++ .../modules/cart/components/CartItemSelect.tsx | 45 +++++++++ src-migrate/modules/cart/stores/useCartStore.ts | 63 +++++++++++++ .../modules/cart/styles/CartDetail.module.css | 3 + .../modules/cart/styles/CartItem.module.css | 47 ++++++++++ .../modules/cart/styles/CartItemAction.module.css | 32 +++++++ .../modules/cart/styles/CartSummary.module.css | 21 +++++ .../modules/cart/styles/ProductPromo.module.css | 24 +++++ src-migrate/modules/cart/ui/CartItem.tsx | 80 ++++++++++++++++ src-migrate/modules/cart/ui/CartSummary.tsx | 74 +++++++++++++++ src-migrate/modules/cart/ui/ProductPromo.tsx | 33 +++++++ 12 files changed, 602 insertions(+) create mode 100644 src-migrate/modules/cart/components/CartDetail.tsx create mode 100644 src-migrate/modules/cart/components/CartItemAction.tsx create mode 100644 src-migrate/modules/cart/components/CartItemSelect.tsx create mode 100644 src-migrate/modules/cart/stores/useCartStore.ts create mode 100644 src-migrate/modules/cart/styles/CartDetail.module.css create mode 100644 src-migrate/modules/cart/styles/CartItem.module.css create mode 100644 src-migrate/modules/cart/styles/CartItemAction.module.css create mode 100644 src-migrate/modules/cart/styles/CartSummary.module.css create mode 100644 src-migrate/modules/cart/styles/ProductPromo.module.css create mode 100644 src-migrate/modules/cart/ui/CartItem.tsx create mode 100644 src-migrate/modules/cart/ui/CartSummary.tsx create mode 100644 src-migrate/modules/cart/ui/ProductPromo.tsx (limited to 'src-migrate/modules/cart') diff --git a/src-migrate/modules/cart/components/CartDetail.tsx b/src-migrate/modules/cart/components/CartDetail.tsx new file mode 100644 index 00000000..734c61d3 --- /dev/null +++ b/src-migrate/modules/cart/components/CartDetail.tsx @@ -0,0 +1,76 @@ +import React, { useEffect, useMemo } from 'react' +import { getAuth } from '~/common/libs/auth' +import { useCartStore } from '../stores/useCartStore' +import CartItem from '../ui/CartItem' +import style from '../styles/CartDetail.module.css' +import CartSummary from '../ui/CartSummary' +import { Button, Tooltip } from '@chakra-ui/react' + +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 ( +
+
+ {/*
*/} +
+
Keranjang Belanja
+
+ {!cart && } +
+
+ {cart?.products.map((item) => )} +
+
+
+ +
+
+ +
+ + + + + + +
+
+
+
+ ) +} + +export default CartDetail \ No newline at end of file diff --git a/src-migrate/modules/cart/components/CartItemAction.tsx b/src-migrate/modules/cart/components/CartItemAction.tsx new file mode 100644 index 00000000..742d1a39 --- /dev/null +++ b/src-migrate/modules/cart/components/CartItemAction.tsx @@ -0,0 +1,104 @@ +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' + +import style from '../styles/CartItemAction.module.css' + +type Props = { + item: CartItem +} + +const CartItemAction = ({ item }: Props) => { + const auth = getAuth() + + const [isLoadDelete, setIsLoadDelete] = useState(false) + const [isLoadQuantity, setIsLoadQuantity] = useState(false) + + const [quantity, setQuantity] = useState(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 ( +
+ + +
+ {isLoadQuantity && ( +
+ +
+ )} + + + + setQuantity(parseInt(e.target.value))} + value={quantity} + /> + + 0 ? `Max. ${limitQty}` : ''}> + + +
+
+ ) +} + +export default CartItemAction \ No newline at end of file diff --git a/src-migrate/modules/cart/components/CartItemSelect.tsx b/src-migrate/modules/cart/components/CartItemSelect.tsx new file mode 100644 index 00000000..f44b0d7e --- /dev/null +++ b/src-migrate/modules/cart/components/CartItemSelect.tsx @@ -0,0 +1,45 @@ +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(false) + + const handleChange = async (e: React.ChangeEvent) => { + 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 ( +
+ {isLoad && ( + + )} + {!isLoad && ( + + )} +
+ ) +} + +export default CartItemSelect \ No newline at end of file diff --git a/src-migrate/modules/cart/stores/useCartStore.ts b/src-migrate/modules/cart/stores/useCartStore.ts new file mode 100644 index 00000000..1963df53 --- /dev/null +++ b/src-migrate/modules/cart/stores/useCartStore.ts @@ -0,0 +1,63 @@ +import { create } from 'zustand'; +import { CartProps } from '~/common/types/cart'; +import { deleteUserCart, getUserCart, upsertUserCart } from '~/services/cart'; + +type State = { + cart: CartProps | null; + isLoadCart: boolean; + summary: { + subtotal: number; + discount: number; + total: number; + tax: number; + grandTotal: number; + }; +}; + +type Action = { + loadCart: (userId: number) => Promise; +}; + +export const useCartStore = create((set, get) => ({ + cart: null, + isLoadCart: false, + summary: { + subtotal: 0, + discount: 0, + total: 0, + tax: 0, + grandTotal: 0, + }, + loadCart: async (userId) => { + if (get().isLoadCart === true) return; + + set({ isLoadCart: true }); + const cart: CartProps = (await getUserCart(userId)) as CartProps; + set({ cart }); + set({ isLoadCart: false }); + + const summary = computeSummary(cart); + set({ summary }); + }, +})); + +const computeSummary = (cart: CartProps) => { + let subtotal = 0; + let discount = 0; + for (const item of cart.products) { + if (!item.selected) continue; + + let price = 0; + if (item.cart_type === 'promotion') price = item?.package_price || 0; + else if (item.cart_type === 'product') + price = item.price.price * item.quantity; + + subtotal += price; + discount += price - item.price.price_discount * item.quantity; + } + let total = subtotal - discount; + let tax = total * 0.11; + let grandTotal = total + tax; + + return { subtotal, discount, total, tax, grandTotal }; +}; diff --git a/src-migrate/modules/cart/styles/CartDetail.module.css b/src-migrate/modules/cart/styles/CartDetail.module.css new file mode 100644 index 00000000..42d492bb --- /dev/null +++ b/src-migrate/modules/cart/styles/CartDetail.module.css @@ -0,0 +1,3 @@ +.wrapper { + @apply flex flex-wrap; +} diff --git a/src-migrate/modules/cart/styles/CartItem.module.css b/src-migrate/modules/cart/styles/CartItem.module.css new file mode 100644 index 00000000..8ee3d3e9 --- /dev/null +++ b/src-migrate/modules/cart/styles/CartItem.module.css @@ -0,0 +1,47 @@ +.wrapper { + @apply border-b border-gray-300 pb-8; +} + +.mainProdWrapper { + @apply flex; +} + +.image { + @apply h-32 w-32 rounded flex p-2 border border-gray-300; +} + +.noImage { + @apply m-auto font-semibold text-gray-400; +} + +.details { + @apply ml-4 flex flex-col gap-y-1; +} + +.name { + @apply font-medium; +} + +.spacing2 { + @apply h-2; +} + +.discPriceSection { + @apply flex gap-x-2.5; +} + +.priceBefore { + @apply line-through text-gray-500; +} + +.price { + @apply text-red-600 font-medium; +} + +.savingAmt { + @apply text-success-600; +} + +.weightLabel { + @apply text-gray-500; +} diff --git a/src-migrate/modules/cart/styles/CartItemAction.module.css b/src-migrate/modules/cart/styles/CartItemAction.module.css new file mode 100644 index 00000000..e4db7fa5 --- /dev/null +++ b/src-migrate/modules/cart/styles/CartItemAction.module.css @@ -0,0 +1,32 @@ +.actionSection { + @apply flex ml-auto h-fit my-auto; +} + +.deleteButton { + @apply bg-red-100 disabled:bg-gray-100 + text-red-700 disabled:text-gray-500 + hover:bg-red-200 + disabled:cursor-not-allowed + transition-all + p-2.5 rounded; +} + +.quantitySection { + @apply relative flex border border-gray-300 rounded ml-4 items-center text-red-700; +} + +.quantityLoading { + @apply absolute flex items-center justify-center text-white rounded w-full h-full bg-gray-900/50 z-10; +} + +.quantityControl { + @apply h-full w-8 flex items-center justify-center hover:bg-gray-100 + disabled:text-gray-500 + disabled:bg-transparent + disabled:cursor-not-allowed + transition; +} + +.quantity { + @apply text-gray-900 font-medium max-w-[28px] outline-none text-center; +} diff --git a/src-migrate/modules/cart/styles/CartSummary.module.css b/src-migrate/modules/cart/styles/CartSummary.module.css new file mode 100644 index 00000000..48ccec28 --- /dev/null +++ b/src-migrate/modules/cart/styles/CartSummary.module.css @@ -0,0 +1,21 @@ +.line { + @apply flex justify-between; +} + +.label, +.value { + @apply text-gray-700; +} + +.value, +.grandTotal { + @apply font-medium; +} + +.discount { + @apply text-red-700; +} + +.divider { + @apply my-0.5 h-0.5 bg-gray-200; +} diff --git a/src-migrate/modules/cart/styles/ProductPromo.module.css b/src-migrate/modules/cart/styles/ProductPromo.module.css new file mode 100644 index 00000000..3f6e7a05 --- /dev/null +++ b/src-migrate/modules/cart/styles/ProductPromo.module.css @@ -0,0 +1,24 @@ +.wrapper { + @apply ml-16 mt-4 flex; +} + +.imageWrapper { + @apply h-24 w-24 border border-gray-300 rounded p-2.5; +} + +.details { + @apply ml-4 flex flex-col gap-y-1; +} + +.name { + @apply font-medium; +} + +.code, +.weightLabel { + @apply text-gray-600; +} + +.quantity { + @apply py-2.5 bg-gray-100 border border-gray-300 h-fit my-auto rounded-md ml-auto font-medium w-12 text-center; +} diff --git a/src-migrate/modules/cart/ui/CartItem.tsx b/src-migrate/modules/cart/ui/CartItem.tsx new file mode 100644 index 00000000..70d50bff --- /dev/null +++ b/src-migrate/modules/cart/ui/CartItem.tsx @@ -0,0 +1,80 @@ +import Image from 'next/image' +import React from 'react' +import formatCurrency from '~/common/libs/formatCurrency' +import { CartItem as CartItemProps } from '~/common/types/cart' +import ProductPromo from './ProductPromo' +import { Skeleton, SkeletonProps } from '@chakra-ui/react' +import style from '../styles/CartItem.module.css' +import CartItemAction from '../components/CartItemAction' +import CartItemSelect from '../components/CartItemSelect' + +type Props = { + item: CartItemProps +} + +const CartItem = ({ item }: Props) => { + const image = item?.image || item?.parent?.image + + return ( +
+
+ +
+
+ {image && {item.name}} + {!image &&
No Image
} +
+ +
+
{item.name}
+
+ {item.cart_type === 'promotion' && ( +
+ + Rp {formatCurrency((item.package_price || 0))} + + + Hemat Rp {formatCurrency((item.package_price || 0) - item.subtotal)} + + + Rp {formatCurrency(item.subtotal)} + +
+ )} + {item.cart_type === 'product' && ( + <> +
+ Rp {formatCurrency(item.price.price)} +
+
{item.code}
+ + )} +
+ Berat barang: + {item.weight} Kg +
+
+ + +
+ +
+ {item.products?.map((product) => )} + {item.free_products?.map((product) => )} +
+
+ ) +} + +CartItem.Skeleton = function CartItemSkeleton(props: SkeletonProps & { count: number }) { + return Array.from({ length: props.count }).map((_, index) => ( + + )) +} + +export default CartItem \ No newline at end of file diff --git a/src-migrate/modules/cart/ui/CartSummary.tsx b/src-migrate/modules/cart/ui/CartSummary.tsx new file mode 100644 index 00000000..390c1c77 --- /dev/null +++ b/src-migrate/modules/cart/ui/CartSummary.tsx @@ -0,0 +1,74 @@ +import React from 'react' +import style from '../styles/CartSummary.module.css' +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 ( + <> +
Ringkasan Pesanan
+ +
+ +
+ + Total Belanja + Rp {formatCurrency(subtotal || 0)} + + + + Total Diskon + - Rp {formatCurrency(discount || 0)} + + +
+ + + Subtotal + Rp {formatCurrency(total || 0)} + + + + Tax 11% + Rp {formatCurrency(tax || 0)} + + + + Biaya Kirim + Rp {formatCurrency(shipping || 0)} + + +
+ + + + Grand Total + + Rp {formatCurrency(grandTotal || 0)} + +
+ + ) +} + +export default CartSummary \ No newline at end of file diff --git a/src-migrate/modules/cart/ui/ProductPromo.tsx b/src-migrate/modules/cart/ui/ProductPromo.tsx new file mode 100644 index 00000000..a41afc97 --- /dev/null +++ b/src-migrate/modules/cart/ui/ProductPromo.tsx @@ -0,0 +1,33 @@ +import Image from 'next/image' +import React from 'react' +import { CartProduct } from '~/common/types/cart' +import style from '../styles/ProductPromo.module.css' + +type Props = { + product: CartProduct +} + +const ProductPromo = ({ product }: Props) => { + return ( +
+
+ {product?.image && {product.name}} +
+ +
+
{product.display_name}
+
{product.code}
+
+ Berat Barang: + {product.package_weight} Kg +
+
+ +
+ {product.qty} +
+
+ ) +} + +export default ProductPromo \ No newline at end of file -- cgit v1.2.3