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/common/libs/formatCurrency.ts | 5 + src-migrate/common/types/cart.ts | 70 ++++++++++++ src-migrate/common/types/productVariant.ts | 13 +++ src-migrate/common/types/promotion.ts | 28 +++++ src-migrate/common/types/solr.ts | 7 ++ src-migrate/modules/account-activation/index.tsx | 1 - 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 ++++++ src-migrate/modules/product/PromoCard.module.css | 62 +++++++++++ src-migrate/modules/product/PromoCard.tsx | 117 +++++++++++++++++++++ .../modules/product/PromoProduct.module.css | 15 +++ src-migrate/modules/product/PromoProduct.tsx | 22 ++++ .../modules/product/PromoSection.module.css | 11 ++ src-migrate/modules/product/PromoSection.tsx | 42 ++++++++ src-migrate/pages/api/product-variant/[id].tsx | 45 ++++++++ .../product-variant/[id]/promotion/highlight.tsx | 56 ++++++++++ src-migrate/services/auth.ts | 24 +---- src-migrate/services/banner.ts | 0 src-migrate/services/cart.ts | 29 +++++ src-migrate/services/pageContent.ts | 11 +- 30 files changed, 1130 insertions(+), 30 deletions(-) create mode 100644 src-migrate/common/libs/formatCurrency.ts create mode 100644 src-migrate/common/types/cart.ts create mode 100644 src-migrate/common/types/productVariant.ts create mode 100644 src-migrate/common/types/promotion.ts create mode 100644 src-migrate/common/types/solr.ts 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 create mode 100644 src-migrate/modules/product/PromoCard.module.css create mode 100644 src-migrate/modules/product/PromoCard.tsx create mode 100644 src-migrate/modules/product/PromoProduct.module.css create mode 100644 src-migrate/modules/product/PromoProduct.tsx create mode 100644 src-migrate/modules/product/PromoSection.module.css create mode 100644 src-migrate/modules/product/PromoSection.tsx create mode 100644 src-migrate/pages/api/product-variant/[id].tsx create mode 100644 src-migrate/pages/api/product-variant/[id]/promotion/highlight.tsx delete mode 100644 src-migrate/services/banner.ts create mode 100644 src-migrate/services/cart.ts (limited to 'src-migrate') diff --git a/src-migrate/common/libs/formatCurrency.ts b/src-migrate/common/libs/formatCurrency.ts new file mode 100644 index 00000000..41db4a6f --- /dev/null +++ b/src-migrate/common/libs/formatCurrency.ts @@ -0,0 +1,5 @@ +const formatCurrency = (value: number) => { + return value.toLocaleString('id-ID'); +}; + +export default formatCurrency; diff --git a/src-migrate/common/types/cart.ts b/src-migrate/common/types/cart.ts new file mode 100644 index 00000000..15e08093 --- /dev/null +++ b/src-migrate/common/types/cart.ts @@ -0,0 +1,70 @@ +type Price = { + price: number; + discount_percentage: number; + price_discount: number; +}; + +export type CartProduct = { + id: number; + image: string; + display_name: string; + name: string; + code: string; + price: Price; + qty: number; + weight: number; + package_weight: number; +}; + +export type CartItem = { + cart_id: number; + quantity: number; + selected: boolean; + can_buy: boolean; + cart_type: 'product' | 'promotion'; + id: number; + name: string; + stock: number; + weight: number; + attributes: string[]; + parent: { + id: number; + name: string; + image: string; + }; + price: Price; + manufacture: { + id: number; + name: string; + }; + has_flashsale: boolean; + subtotal: number; + + code?: string; + + image?: string; + remaining_time?: number; + promotion_type?: { + value?: string; + label?: string; + }; + limit_qty?: { + all?: number; + user?: number; + transaction?: number; + }; + remaining_qty?: { + all?: number; + user?: number; + transaction?: number; + }; + used_percentage?: number; + products?: CartProduct[]; + free_products?: CartProduct[]; + package_price?: number; +}; + +export type CartProps = { + product_total: number; + products: CartItem[]; +}; diff --git a/src-migrate/common/types/productVariant.ts b/src-migrate/common/types/productVariant.ts new file mode 100644 index 00000000..c4aa9534 --- /dev/null +++ b/src-migrate/common/types/productVariant.ts @@ -0,0 +1,13 @@ +export interface IProductVariant { + id: number; + parent_id: number; + display_name: string; + image: string; + name: string; + default_code: string; + price: { + price: number; + discount_percentage: number; + price_discount: number; + }; +} diff --git a/src-migrate/common/types/promotion.ts b/src-migrate/common/types/promotion.ts new file mode 100644 index 00000000..9e62cc3f --- /dev/null +++ b/src-migrate/common/types/promotion.ts @@ -0,0 +1,28 @@ +import { IProductVariant } from './productVariant'; + +export interface IPromotion { + id: number; + program_id: number; + name: string; + type: { + value: string; + label: string; + }; + limit: number; + limit_user: number; + limit_trx: number; + price: number; + total_qty: number; + products: { + product_id: number; + qty: number; + }[]; + free_products: { + product_id: number; + qty: number; + }[]; +} + +export interface IProductVariantPromo extends IProductVariant { + qty: number; +} diff --git a/src-migrate/common/types/solr.ts b/src-migrate/common/types/solr.ts new file mode 100644 index 00000000..d231c305 --- /dev/null +++ b/src-migrate/common/types/solr.ts @@ -0,0 +1,7 @@ +export type SolrResponse = { + response: { + numFound: number; + start: number; + docs: T; + }; +}; diff --git a/src-migrate/modules/account-activation/index.tsx b/src-migrate/modules/account-activation/index.tsx index 97c96953..c6e2c683 100644 --- a/src-migrate/modules/account-activation/index.tsx +++ b/src-migrate/modules/account-activation/index.tsx @@ -1,4 +1,3 @@ -import { useRouter } from "next/router" import FormToken from "./components/FormToken" import FormEmail from "./components/FormEmail" import FormOTP from "./components/FormOTP" 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 diff --git a/src-migrate/modules/product/PromoCard.module.css b/src-migrate/modules/product/PromoCard.module.css new file mode 100644 index 00000000..4d98671f --- /dev/null +++ b/src-migrate/modules/product/PromoCard.module.css @@ -0,0 +1,62 @@ +.card { + @apply border border-gray_r-7 + rounded-lg + min-w-[360px] + max-w-[360px] + py-3; +} + +.countdownSection { + @apply w-fit p-2.5 pr-6 + rounded-r-full + font-medium + flex items-center gap-x-2.5; +} + +.countdown { + @apply flex gap-x-1; +} + +.countdown span { + @apply py-0.5 w-8 bg-red-600 text-gray_r-4 rounded-md text-center; +} + +.title { + @apply font-semibold text-h-md; +} + +.productSection { + @apply flex gap-x-3 mt-4 min-h-[180px]; +} + +.priceSection { + @apply flex items-center justify-between mt-4; +} + +.priceCol { + @apply flex flex-col gap-y-1; +} + +.priceRow { + @apply flex gap-x-2 items-center; +} + +.basePrice { + @apply line-through; +} + +.savingAmt { + @apply text-success-600 font-medium; +} + +.price { + @apply text-body-1 text-danger-600 font-medium; +} + +.totalItems { + @apply text-gray_r-9; +} + +.addToCartBtn { + @apply btn-yellow flex items-center gap-x-1 px-2 rounded-lg; +} diff --git a/src-migrate/modules/product/PromoCard.tsx b/src-migrate/modules/product/PromoCard.tsx new file mode 100644 index 00000000..8bb48155 --- /dev/null +++ b/src-migrate/modules/product/PromoCard.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useMemo, useState } from 'react' +import style from "./PromoCard.module.css" +import { ClockIcon, PlusIcon } from "lucide-react" +import { IProductVariantPromo, IPromotion } from '~/common/types/promotion' +import formatCurrency from '~/common/libs/formatCurrency' +import PromoProduct from './PromoProduct' +import { Skeleton, Spinner } from '@chakra-ui/react' +import clsxm from '~/common/libs/clsxm' +import { useCountdown } from 'usehooks-ts' + +type Props = { + promotion: IPromotion +} + +const PromoCard = ({ promotion }: Props) => { + // TODO: useCountdown() + const [products, setProducts] = useState([]) + + useEffect(() => { + const getProducts = async () => { + const datas = [] + for (const product of promotion.products) { + const response = await fetch(`/api/product-variant/${product.product_id}`) + const res = await response.json() + res.data.qty = product.qty + datas.push(res.data) + } + setProducts(datas) + } + + getProducts() + }, [promotion.products]) + + const [freeProducts, setFreeProducts] = useState([]) + + useEffect(() => { + const getFreeProducts = async () => { + const datas = [] + for (const product of promotion.free_products) { + const response = await fetch(`/api/product-variant/${product.product_id}`) + const res = await response.json() + res.data.qty = product.qty + datas.push(res.data) + } + setFreeProducts(datas) + } + + getFreeProducts() + }, [promotion.free_products]) + + const priceTotal = useMemo(() => { + let total = 0; + [...products, ...freeProducts].forEach((product) => { + total += product.price.price_discount * product.qty + console.log({ product }); + + }) + return total + }, [products, freeProducts]) + + const countdownClass = { + 'text-white': true, + 'bg-[#312782]': promotion.type.value === 'bundling', + 'bg-[#329E44]': promotion.type.value === 'discount_loading', + 'bg-[#FAD147]': promotion.type.value === 'merchandise', + 'text-gray-700': promotion.type.value === 'merchandise', + } + + return ( +
+
+ + + + Berakhir dalam +
+ 00 + 01 + 35 +
+
+ +
+
{promotion.name}
+ + 0}> + {products.map((product) => )} + {freeProducts.map((product) => )} + + +
+
+ 0}> + Rp{formatCurrency(priceTotal)} + Hemat Rp {formatCurrency(priceTotal - promotion.price)} + + +
+ Rp{formatCurrency(promotion.price)} + (Total {promotion.total_qty} barang) +
+
+
+ +
+ +
+
+
+ ) +} + +export default PromoCard \ No newline at end of file diff --git a/src-migrate/modules/product/PromoProduct.module.css b/src-migrate/modules/product/PromoProduct.module.css new file mode 100644 index 00000000..c13bccb8 --- /dev/null +++ b/src-migrate/modules/product/PromoProduct.module.css @@ -0,0 +1,15 @@ +.product { + @apply w-4/12; +} + +.image { + @apply border border-gray_r-6 p-2.5 rounded-lg; +} + +.fillDesc { + @apply mt-2 text-danger-600; +} + +.name { + @apply mt-1 line-clamp-3 font-medium; +} diff --git a/src-migrate/modules/product/PromoProduct.tsx b/src-migrate/modules/product/PromoProduct.tsx new file mode 100644 index 00000000..83b05e88 --- /dev/null +++ b/src-migrate/modules/product/PromoProduct.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import style from './PromoProduct.module.css' +import { IProductVariantPromo } from '~/common/types/promotion' +import Image from 'next/image' + +type Props = { + variant: IProductVariantPromo +} + +const PromoProduct = ({ variant }: Props) => { + return ( +
+
+ {variant.display_name} +
+
Isi {variant.qty} barang
+
{variant.name}
+
+ ) +} + +export default PromoProduct \ No newline at end of file diff --git a/src-migrate/modules/product/PromoSection.module.css b/src-migrate/modules/product/PromoSection.module.css new file mode 100644 index 00000000..a9c9b704 --- /dev/null +++ b/src-migrate/modules/product/PromoSection.module.css @@ -0,0 +1,11 @@ +.titleWrapper { + @apply w-full mb-4 h-20 bg-[#C70817] rounded-lg flex items-center justify-between px-4 py-1; +} + +.seeMore { + @apply py-2 px-3 btn-yellow rounded-lg text-body-2; +} + +.title { + @apply font-semibold text-xl text-white; +} diff --git a/src-migrate/modules/product/PromoSection.tsx b/src-migrate/modules/product/PromoSection.tsx new file mode 100644 index 00000000..299cbb78 --- /dev/null +++ b/src-migrate/modules/product/PromoSection.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import style from "./PromoSection.module.css" +import PromoCard from './PromoCard' +import { useQuery } from 'react-query' +import { Skeleton } from '@chakra-ui/react' +import { IPromotion } from '~/common/types/promotion' + +type Props = { + productId: number +} + +const PromoSection = ({ productId }: Props) => { + const promotionsQuery = useQuery( + `promotions-highlight:${productId}`, + async () => await fetch(`/api/product-variant/${productId}/promotion/highlight`).then((res) => res.json()) as { data: IPromotion[] }, + ) + + const promotions = promotionsQuery.data + + const handleSeeMore = () => { } + + return ( +
+ {promotions?.data && promotions?.data.length > 0 && ( +
+ Promo Tersedia + +
+ )} + + + {promotions?.data.map((promotion) => ( + + ))} + +
+ ) +} + +export default PromoSection \ No newline at end of file diff --git a/src-migrate/pages/api/product-variant/[id].tsx b/src-migrate/pages/api/product-variant/[id].tsx new file mode 100644 index 00000000..ec95714d --- /dev/null +++ b/src-migrate/pages/api/product-variant/[id].tsx @@ -0,0 +1,45 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { SolrResponse } from "~/common/types/solr"; + +const SOLR_HOST = process.env.SOLR_HOST as string + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const variantId = req.query.id as string + let price_tier = 'tier1' + + let auth = req.cookies.auth ? JSON.parse(req.cookies.auth) : null + if (auth?.pricelist) price_tier = auth.pricelist + + if (req.method === 'GET') { + const queryParams = new URLSearchParams({ q: `id:${variantId}` }) + const response = await fetch(`${SOLR_HOST}/solr/variants/select?${queryParams.toString()}`) + const data: SolrResponse = await response.json() + + if (data.response.numFound === 0) { + res.status(404).json({ error: 'Variant not found' }) + return + } + + const variant = await extractVariant(data.response.docs[0], price_tier) + + res.status(200).json({ data: variant }) + } +} + +const extractVariant = async (variant: any, price_tier: string) => { + const data: any = {} + + data.id = parseInt(variant.id) + data.parent_id = variant.template_id_i + data.display_name = variant.display_name_s + data.image = variant.image_s + data.name = variant.name_s + data.default_code = variant.default_code_s + data.price = { + price: variant.price_v2_f, + discount_percentage: variant[`discount_${price_tier}_v2_f`] || 0, + price_discount: variant[`price_${price_tier}_v2_f`] || 0, + } + + return data +} \ No newline at end of file diff --git a/src-migrate/pages/api/product-variant/[id]/promotion/highlight.tsx b/src-migrate/pages/api/product-variant/[id]/promotion/highlight.tsx new file mode 100644 index 00000000..0fe8fd1b --- /dev/null +++ b/src-migrate/pages/api/product-variant/[id]/promotion/highlight.tsx @@ -0,0 +1,56 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { SolrResponse } from "~/common/types/solr"; + +const SOLR_HOST = process.env.SOLR_HOST as string + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const productId = req.query.id as string + + if (req.method === 'GET') { + const types = ['bundling', 'discount_loading', 'merchandise'] + const queryParams = new URLSearchParams({ + q: `product_ids:${productId}`, + rows: '1' + }) + + let programs: any[] = [] + + for (const type of types) { + queryParams.set('fq', `type_value_s:${type}`) + const response = await fetch(`${SOLR_HOST}/solr/promotion_program_lines/select?${queryParams.toString()}`) + const data: SolrResponse = await response.json() + programs.push(...data.response.docs) + } + + programs = await extractPrograms(programs) + res.status(200).json({ data: programs }) + } +} + +const extractPrograms = async (programs: any[]) => { + const result = [] + + for (const program of programs) { + const data: any = {} + + data.id = program.id + data.program_id = program.program_id_i + data.name = program.name_s + data.type = { + value: program.type_value_s, + label: program.type_label_s, + } + data.limit = program.package_limit_i + data.limit_user = program.package_limit_user_i + data.limit_trx = program.package_limit_trx_i + data.price = program.price_f + data.total_qty = program.total_qty_i + + data.products = JSON.parse(program.products_s) + data.free_products = JSON.parse(program.free_products_s) + + result.push(data) + } + + return result +} \ No newline at end of file diff --git a/src-migrate/services/auth.ts b/src-migrate/services/auth.ts index a5d02754..1cc09c10 100644 --- a/src-migrate/services/auth.ts +++ b/src-migrate/services/auth.ts @@ -15,39 +15,23 @@ const BASE_PATH = '/api/v1/user'; export const registerUser = async ( data: RegisterProps ): Promise => { - const response = await odooApi('POST', `${BASE_PATH}/register`, data); - - return response; + return await odooApi('POST', `${BASE_PATH}/register`, data); }; export const activationUserToken = async ( params: ActivationTokenProps ): Promise => { - const response = await odooApi( - 'POST', - `${BASE_PATH}/activation-token`, - params - ); - - return response; + return await odooApi('POST', `${BASE_PATH}/activation-token`, params); }; export const activationUserOTP = async ( params: ActivationOtpProps ): Promise => { - const response = await odooApi('POST', `${BASE_PATH}/activation-otp`, params); - - return response; + return await odooApi('POST', `${BASE_PATH}/activation-otp`, params); }; export const activationReq = async ( params: ActivationReqProps ): Promise => { - const response = await odooApi( - 'POST', - `${BASE_PATH}/activation-request`, - params - ); - - return response; + return await odooApi('POST', `${BASE_PATH}/activation-request`, params); }; diff --git a/src-migrate/services/banner.ts b/src-migrate/services/banner.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src-migrate/services/cart.ts b/src-migrate/services/cart.ts new file mode 100644 index 00000000..b238be3d --- /dev/null +++ b/src-migrate/services/cart.ts @@ -0,0 +1,29 @@ +import odooApi from '~/common/libs/odooApi'; + +export const getUserCart = async (userId: number) => { + return await odooApi('GET', `/api/v1/user/${userId}/cart`); +}; + +export const upsertUserCart = async ( + userId: number, + type: 'product' | 'promotion', + id: number, + qty: number, + selected: boolean, + source: 'buy' | 'add_to_cart' = 'add_to_cart' +) => { + return await odooApi('POST', `/api/v1/user/${userId}/cart/create-or-update`, { + product_id: type === 'product' ? id : null, + qty, + selected, + program_line_id: type === 'promotion' ? id : null, + source, + }); +}; + +export const deleteUserCart = async (userId: number, ids: number[]) => { + return await odooApi( + 'DELETE', + `/api/v1/user/${userId}/cart?ids=${ids.join(',')}` + ); +}; diff --git a/src-migrate/services/pageContent.ts b/src-migrate/services/pageContent.ts index 24f2c2f0..16146059 100644 --- a/src-migrate/services/pageContent.ts +++ b/src-migrate/services/pageContent.ts @@ -1,14 +1,7 @@ import odooApi from '~/common/libs/odooApi'; export const getPageContent = async ({ path }: { path: string }) => { - const params = new URLSearchParams({ - url_path: path, - }); + const params = new URLSearchParams({ url_path: path }); - const pageContent = await odooApi( - 'GET', - `/api/v1/page-content?${params.toString()}` - ); - - return pageContent; + return await odooApi('GET', `/api/v1/page-content?${params.toString()}`); }; -- cgit v1.2.3 From 89f32128f37d99b490de7590e2116a9cfd853f89 Mon Sep 17 00:00:00 2001 From: Rafi Zadanly Date: Fri, 22 Dec 2023 17:33:46 +0700 Subject: Update promotion program feature --- src-migrate/common/libs/parse | 0 src-migrate/common/types/cart.ts | 4 +- src-migrate/common/types/checkout.ts | 16 +++ src-migrate/common/types/promotion.ts | 9 +- src-migrate/common/types/promotionProgram.ts | 8 ++ src-migrate/constants/promotion.ts | 17 +++ src-migrate/modules/cart/components/CartDetail.tsx | 76 ------------- .../modules/cart/components/CartItemAction.tsx | 104 ------------------ .../modules/cart/components/CartItemSelect.tsx | 45 -------- src-migrate/modules/cart/components/Detail.tsx | 84 +++++++++++++++ src-migrate/modules/cart/components/Item.tsx | 109 +++++++++++++++++++ src-migrate/modules/cart/components/ItemAction.tsx | 105 ++++++++++++++++++ src-migrate/modules/cart/components/ItemPromo.tsx | 41 +++++++ src-migrate/modules/cart/components/ItemSelect.tsx | 47 ++++++++ src-migrate/modules/cart/components/Summary.tsx | 75 +++++++++++++ src-migrate/modules/cart/stores/useCartStore.ts | 4 +- .../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/styles/detail.module.css | 3 + .../modules/cart/styles/item-action.module.css | 32 ++++++ .../modules/cart/styles/item-promo.module.css | 31 ++++++ src-migrate/modules/cart/styles/item.module.css | 60 +++++++++++ src-migrate/modules/cart/styles/summary.module.css | 21 ++++ src-migrate/modules/cart/ui/CartItem.tsx | 80 -------------- src-migrate/modules/cart/ui/CartSummary.tsx | 74 ------------- src-migrate/modules/cart/ui/ProductPromo.tsx | 33 ------ .../modules/product-promo/components/AddToCart.tsx | 61 +++++++++++ .../modules/product-promo/components/Card.tsx | 120 +++++++++++++++++++++ .../product-promo/components/CardCountdown.tsx | 67 ++++++++++++ .../product-promo/components/CategoryTab.tsx | 34 ++++++ .../modules/product-promo/components/Item.tsx | 24 +++++ .../modules/product-promo/components/Modal.tsx | 25 +++++ .../product-promo/components/ModalContent.tsx | 33 ++++++ .../modules/product-promo/components/Section.tsx | 50 +++++++++ .../modules/product-promo/stores/useModalStore.ts | 28 +++++ .../product-promo/styles/card-countdown.module.css | 14 +++ .../modules/product-promo/styles/card.module.css | 46 ++++++++ .../product-promo/styles/category-tab.module.css | 12 +++ .../modules/product-promo/styles/item.module.css | 19 ++++ .../product-promo/styles/section.module.css | 7 ++ src-migrate/modules/product/PromoCard.module.css | 62 ----------- src-migrate/modules/product/PromoCard.tsx | 117 -------------------- .../modules/product/PromoProduct.module.css | 15 --- src-migrate/modules/product/PromoProduct.tsx | 22 ---- .../modules/product/PromoSection.module.css | 11 -- src-migrate/modules/product/PromoSection.tsx | 42 -------- src-migrate/pages/api/product-variant/[id].tsx | 4 +- .../product-variant/[id]/promotion/[category].tsx | 51 +++++++++ src-migrate/pages/api/promotion-program/[id].tsx | 43 ++++++++ src-migrate/services/checkout.ts | 7 ++ src-migrate/services/promotionProgram.ts | 8 ++ src-migrate/services/variant.ts | 14 +++ 55 files changed, 1327 insertions(+), 814 deletions(-) create mode 100644 src-migrate/common/libs/parse create mode 100644 src-migrate/common/types/checkout.ts create mode 100644 src-migrate/common/types/promotionProgram.ts create mode 100644 src-migrate/constants/promotion.ts delete mode 100644 src-migrate/modules/cart/components/CartDetail.tsx delete mode 100644 src-migrate/modules/cart/components/CartItemAction.tsx delete mode 100644 src-migrate/modules/cart/components/CartItemSelect.tsx create mode 100644 src-migrate/modules/cart/components/Detail.tsx create mode 100644 src-migrate/modules/cart/components/Item.tsx create mode 100644 src-migrate/modules/cart/components/ItemAction.tsx create mode 100644 src-migrate/modules/cart/components/ItemPromo.tsx create mode 100644 src-migrate/modules/cart/components/ItemSelect.tsx create mode 100644 src-migrate/modules/cart/components/Summary.tsx delete mode 100644 src-migrate/modules/cart/styles/CartDetail.module.css delete mode 100644 src-migrate/modules/cart/styles/CartItem.module.css delete mode 100644 src-migrate/modules/cart/styles/CartItemAction.module.css delete mode 100644 src-migrate/modules/cart/styles/CartSummary.module.css delete mode 100644 src-migrate/modules/cart/styles/ProductPromo.module.css create mode 100644 src-migrate/modules/cart/styles/detail.module.css create mode 100644 src-migrate/modules/cart/styles/item-action.module.css create mode 100644 src-migrate/modules/cart/styles/item-promo.module.css create mode 100644 src-migrate/modules/cart/styles/item.module.css create mode 100644 src-migrate/modules/cart/styles/summary.module.css delete mode 100644 src-migrate/modules/cart/ui/CartItem.tsx delete mode 100644 src-migrate/modules/cart/ui/CartSummary.tsx delete mode 100644 src-migrate/modules/cart/ui/ProductPromo.tsx create mode 100644 src-migrate/modules/product-promo/components/AddToCart.tsx create mode 100644 src-migrate/modules/product-promo/components/Card.tsx create mode 100644 src-migrate/modules/product-promo/components/CardCountdown.tsx create mode 100644 src-migrate/modules/product-promo/components/CategoryTab.tsx create mode 100644 src-migrate/modules/product-promo/components/Item.tsx create mode 100644 src-migrate/modules/product-promo/components/Modal.tsx create mode 100644 src-migrate/modules/product-promo/components/ModalContent.tsx create mode 100644 src-migrate/modules/product-promo/components/Section.tsx create mode 100644 src-migrate/modules/product-promo/stores/useModalStore.ts create mode 100644 src-migrate/modules/product-promo/styles/card-countdown.module.css create mode 100644 src-migrate/modules/product-promo/styles/card.module.css create mode 100644 src-migrate/modules/product-promo/styles/category-tab.module.css create mode 100644 src-migrate/modules/product-promo/styles/item.module.css create mode 100644 src-migrate/modules/product-promo/styles/section.module.css delete mode 100644 src-migrate/modules/product/PromoCard.module.css delete mode 100644 src-migrate/modules/product/PromoCard.tsx delete mode 100644 src-migrate/modules/product/PromoProduct.module.css delete mode 100644 src-migrate/modules/product/PromoProduct.tsx delete mode 100644 src-migrate/modules/product/PromoSection.module.css delete mode 100644 src-migrate/modules/product/PromoSection.tsx create mode 100644 src-migrate/pages/api/product-variant/[id]/promotion/[category].tsx create mode 100644 src-migrate/pages/api/promotion-program/[id].tsx create mode 100644 src-migrate/services/checkout.ts create mode 100644 src-migrate/services/promotionProgram.ts create mode 100644 src-migrate/services/variant.ts (limited to 'src-migrate') diff --git a/src-migrate/common/libs/parse b/src-migrate/common/libs/parse new file mode 100644 index 00000000..e69de29b diff --git a/src-migrate/common/types/cart.ts b/src-migrate/common/types/cart.ts index 15e08093..3aceeac4 100644 --- a/src-migrate/common/types/cart.ts +++ b/src-migrate/common/types/cart.ts @@ -1,3 +1,5 @@ +import { CategoryPromo } from "./promotion"; + type Price = { price: number; discount_percentage: number; @@ -45,7 +47,7 @@ export type CartItem = { image?: string; remaining_time?: number; promotion_type?: { - value?: string; + value?: CategoryPromo; label?: string; }; limit_qty?: { diff --git a/src-migrate/common/types/checkout.ts b/src-migrate/common/types/checkout.ts new file mode 100644 index 00000000..dc1365d8 --- /dev/null +++ b/src-migrate/common/types/checkout.ts @@ -0,0 +1,16 @@ +import { CartItem } from './cart'; + +export interface ICheckout { + total_purchase: number; + total_discount: number; + discount_voucher: number; + subtotal: number; + tax: number; + grand_total: number; + total_weight: { + kg: number; + g: number; + }; + has_product_without_weight: boolean; + products: CartItem[]; +} diff --git a/src-migrate/common/types/promotion.ts b/src-migrate/common/types/promotion.ts index 9e62cc3f..1f8316cf 100644 --- a/src-migrate/common/types/promotion.ts +++ b/src-migrate/common/types/promotion.ts @@ -5,7 +5,7 @@ export interface IPromotion { program_id: number; name: string; type: { - value: string; + value: CategoryPromo; label: string; }; limit: number; @@ -26,3 +26,10 @@ export interface IPromotion { export interface IProductVariantPromo extends IProductVariant { qty: number; } + +export type CategoryPromo = 'bundling' | 'discount_loading' | 'merchandise'; + +export interface ICategoryPromo { + value: CategoryPromo; + label: string; +} diff --git a/src-migrate/common/types/promotionProgram.ts b/src-migrate/common/types/promotionProgram.ts new file mode 100644 index 00000000..205884b6 --- /dev/null +++ b/src-migrate/common/types/promotionProgram.ts @@ -0,0 +1,8 @@ +export type IPromotionProgram = { + id: number; + name: string; + start_time: string; + end_time: string; + applies_to: string; + time_left: number; +}; diff --git a/src-migrate/constants/promotion.ts b/src-migrate/constants/promotion.ts new file mode 100644 index 00000000..e6dfcc9b --- /dev/null +++ b/src-migrate/constants/promotion.ts @@ -0,0 +1,17 @@ +export const PROMO_CATEGORY = { + bundling: { + name: 'Bundling', + alias: 'Silat', + description: 'Kombinasi Kilat (SiLat)', + }, + discount_loading: { + name: 'Discount Loading', + alias: 'Barong', + description: 'Barang Borong (BaRong)', + }, + merchandise: { + name: 'Merchandise', + alias: 'Angklung', + description: 'Menang Langsung (Angklung)', + }, +}; diff --git a/src-migrate/modules/cart/components/CartDetail.tsx b/src-migrate/modules/cart/components/CartDetail.tsx deleted file mode 100644 index 734c61d3..00000000 --- a/src-migrate/modules/cart/components/CartDetail.tsx +++ /dev/null @@ -1,76 +0,0 @@ -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 deleted file mode 100644 index 742d1a39..00000000 --- a/src-migrate/modules/cart/components/CartItemAction.tsx +++ /dev/null @@ -1,104 +0,0 @@ -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 deleted file mode 100644 index f44b0d7e..00000000 --- a/src-migrate/modules/cart/components/CartItemSelect.tsx +++ /dev/null @@ -1,45 +0,0 @@ -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/components/Detail.tsx b/src-migrate/modules/cart/components/Detail.tsx new file mode 100644 index 00000000..c9de086b --- /dev/null +++ b/src-migrate/modules/cart/components/Detail.tsx @@ -0,0 +1,84 @@ +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 ( +
+
+ {/*
*/} +
+
Keranjang Belanja
+
+ {!cart && } +
+
+ {cart?.products.map((item) => )} +
+
+
+ +
+
+ +
+ + + + + + +
+
+
+
+ ) +} + +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..92beda86 --- /dev/null +++ b/src-migrate/modules/cart/components/Item.tsx @@ -0,0 +1,109 @@ +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 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' +import { PROMO_CATEGORY } from '~/constants/promotion' +import { InfoIcon } from 'lucide-react' + +type Props = { + item: CartItemProps + editable?: boolean +} + +const CartItem = ({ item, editable = true }: Props) => { + const image = item?.image || item?.parent?.image + + return ( +
+ {item.cart_type === 'promotion' && ( +
+ {item.promotion_type?.value && ( + +
+ Paket {PROMO_CATEGORY[item.promotion_type?.value].alias} + +
+
+ )} +
+
+ Selamat! Pembelian anda lebih hemat {' '} + + Rp {formatCurrency((item.package_price || 0) - item.subtotal)} + +
+
+ )} + +
+ {editable && } +
+
+ {image && {item.name}} + {!image &&
No Image
} +
+ +
+
{item.name}
+
+
+ {item.cart_type === 'promotion' && ( +
+ + Rp {formatCurrency((item.package_price || 0))} + + + Rp {formatCurrency(item.subtotal)} + +
+ )} + + {item.cart_type === 'product' && ( + <> +
+ Rp {formatCurrency(item.price.price)} +
+
{item.code}
+ + )} + +
+ {item.weight} Kg +
+
+ + {editable && } + {!editable &&
{item.quantity}
} +
+
+ +
+ +
+ {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/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(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/ItemPromo.tsx b/src-migrate/modules/cart/components/ItemPromo.tsx new file mode 100644 index 00000000..951d4d6a --- /dev/null +++ b/src-migrate/modules/cart/components/ItemPromo.tsx @@ -0,0 +1,41 @@ +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 ( +
+
+ {product?.image && {product.name}} +
+ +
+
{product.display_name}
+
+
+
{product.code}
+
+ Berat Barang: + {product.package_weight} Kg +
+
+ +
+ {product.qty} +
+
+
+ +
+ ) +} + +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(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/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 ( + <> +
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/stores/useCartStore.ts b/src-migrate/modules/cart/stores/useCartStore.ts index 1963df53..d3eaadb7 100644 --- a/src-migrate/modules/cart/stores/useCartStore.ts +++ b/src-migrate/modules/cart/stores/useCartStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; import { CartProps } from '~/common/types/cart'; -import { deleteUserCart, getUserCart, upsertUserCart } from '~/services/cart'; +import { getUserCart } from '~/services/cart'; type State = { cart: CartProps | null; @@ -56,7 +56,7 @@ const computeSummary = (cart: CartProps) => { discount += price - item.price.price_discount * item.quantity; } let total = subtotal - discount; - let tax = total * 0.11; + let tax = Math.round(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 deleted file mode 100644 index 42d492bb..00000000 --- a/src-migrate/modules/cart/styles/CartDetail.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.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 deleted file mode 100644 index 8ee3d3e9..00000000 --- a/src-migrate/modules/cart/styles/CartItem.module.css +++ /dev/null @@ -1,47 +0,0 @@ -.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 deleted file mode 100644 index e4db7fa5..00000000 --- a/src-migrate/modules/cart/styles/CartItemAction.module.css +++ /dev/null @@ -1,32 +0,0 @@ -.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 deleted file mode 100644 index 48ccec28..00000000 --- a/src-migrate/modules/cart/styles/CartSummary.module.css +++ /dev/null @@ -1,21 +0,0 @@ -.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 deleted file mode 100644 index 3f6e7a05..00000000 --- a/src-migrate/modules/cart/styles/ProductPromo.module.css +++ /dev/null @@ -1,24 +0,0 @@ -.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/styles/detail.module.css b/src-migrate/modules/cart/styles/detail.module.css new file mode 100644 index 00000000..42d492bb --- /dev/null +++ b/src-migrate/modules/cart/styles/detail.module.css @@ -0,0 +1,3 @@ +.wrapper { + @apply flex flex-wrap; +} diff --git a/src-migrate/modules/cart/styles/item-action.module.css b/src-migrate/modules/cart/styles/item-action.module.css new file mode 100644 index 00000000..e4db7fa5 --- /dev/null +++ b/src-migrate/modules/cart/styles/item-action.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/item-promo.module.css b/src-migrate/modules/cart/styles/item-promo.module.css new file mode 100644 index 00000000..17dbf1c7 --- /dev/null +++ b/src-migrate/modules/cart/styles/item-promo.module.css @@ -0,0 +1,31 @@ +.wrapper { + @apply md:ml-16 ml-12 mt-4 flex; +} + +.imageWrapper { + @apply md:h-24 md:w-24 md:min-w-[96px] + h-20 w-20 min-w-[80px] + border border-gray-300 rounded + p-2.5; +} + +.image { + @apply w-full h-full object-contain; +} + +.details { + @apply ml-4 flex flex-col gap-y-1; +} + +.name { + @apply font-medium; +} + +.code, +.weightLabel { + @apply text-gray-600; +} + +.quantity { + @apply w-12 min-w-[48px] py-2.5 bg-gray-100 border border-gray-300 h-fit my-auto rounded-md ml-auto font-medium text-center; +} diff --git a/src-migrate/modules/cart/styles/item.module.css b/src-migrate/modules/cart/styles/item.module.css new file mode 100644 index 00000000..6380cdad --- /dev/null +++ b/src-migrate/modules/cart/styles/item.module.css @@ -0,0 +1,60 @@ +.wrapper { + @apply border-b border-gray-300 pb-8; +} + +.header { + @apply mb-4 flex items-center text-caption-1 leading-6; +} + +.badgeType { + @apply min-w-fit p-2 flex gap-x-1.5 rounded-md border border-danger-500 text-danger-500; +} + +.mainProdWrapper { + @apply flex; +} + +.image { + @apply md:h-32 md:w-32 md:min-w-[128px] + w-24 h-24 min-w-[96px] 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 w-full; +} + +.name { + @apply font-medium; +} + +.spacing2 { + @apply h-2; +} + +.discPriceSection { + @apply flex flex-col md:flex-row 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; +} + +.quantity { + @apply py-2.5 bg-red-100 border border-red-300 text-red-800 h-fit my-auto rounded-md ml-auto font-medium w-12 text-center; +} diff --git a/src-migrate/modules/cart/styles/summary.module.css b/src-migrate/modules/cart/styles/summary.module.css new file mode 100644 index 00000000..48ccec28 --- /dev/null +++ b/src-migrate/modules/cart/styles/summary.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/ui/CartItem.tsx b/src-migrate/modules/cart/ui/CartItem.tsx deleted file mode 100644 index 70d50bff..00000000 --- a/src-migrate/modules/cart/ui/CartItem.tsx +++ /dev/null @@ -1,80 +0,0 @@ -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 deleted file mode 100644 index 390c1c77..00000000 --- a/src-migrate/modules/cart/ui/CartSummary.tsx +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index a41afc97..00000000 --- a/src-migrate/modules/cart/ui/ProductPromo.tsx +++ /dev/null @@ -1,33 +0,0 @@ -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 diff --git a/src-migrate/modules/product-promo/components/AddToCart.tsx b/src-migrate/modules/product-promo/components/AddToCart.tsx new file mode 100644 index 00000000..9d856ccf --- /dev/null +++ b/src-migrate/modules/product-promo/components/AddToCart.tsx @@ -0,0 +1,61 @@ +import React, { useEffect, useState } from 'react' +import { CheckIcon, PlusIcon } from 'lucide-react' +import { IPromotion } from '~/common/types/promotion' +import { upsertUserCart } from '~/services/cart' +import { getAuth } from '~/common/libs/auth' +import { Button, Spinner, useToast } from '@chakra-ui/react' + +type Props = { + promotion: IPromotion +} + +type Status = 'idle' | 'loading' | 'success' + +const ProductPromoAddToCart = ({ promotion }: Props) => { + const auth = getAuth() + const toast = useToast() + + const [status, setStatus] = useState('idle') + + const handleButton = async () => { + if (typeof auth !== 'object') return + if (status === 'success') return + + setStatus('loading') + await upsertUserCart(auth.id, 'promotion', promotion.id, 1, true) + setStatus('idle') + + toast({ + title: 'Tambah ke keranjang', + description: 'Berhasil menambahkan barang ke keranjang belanja', + status: 'success', + duration: 3000, + isClosable: true, + position: 'top', + }) + } + + useEffect(() => { + if (status === 'success') setTimeout(() => { setStatus('idle') }, 3000) + }, [status]) + + return ( + + ) +} + +export default ProductPromoAddToCart \ No newline at end of file diff --git a/src-migrate/modules/product-promo/components/Card.tsx b/src-migrate/modules/product-promo/components/Card.tsx new file mode 100644 index 00000000..2874c2cc --- /dev/null +++ b/src-migrate/modules/product-promo/components/Card.tsx @@ -0,0 +1,120 @@ +import style from "../styles/card.module.css" + +import React, { useEffect, useMemo, useState } from 'react' +import { InfoIcon, PlusIcon } from "lucide-react" +import { Skeleton, Tooltip } from '@chakra-ui/react' +import { motion } from "framer-motion" + +import { IProductVariantPromo, IPromotion } from '~/common/types/promotion' +import formatCurrency from '~/common/libs/formatCurrency' +import clsxm from '~/common/libs/clsxm' +import { PROMO_CATEGORY } from "~/constants/promotion" +import { getVariantById } from "~/services/variant" + +import ProductPromoItem from './Item' +import ProductPromoAddToCart from "./AddToCart" +import ProductPromoCardCountdown from "./CardCountdown" + +type Props = { + promotion: IPromotion +} + +const ProductPromoCard = ({ promotion }: Props) => { + const [products, setProducts] = useState([]) + + useEffect(() => { + const getProducts = async () => { + const datas = [] + for (const product of promotion.products) { + const res = await getVariantById(product.product_id) + res.data.qty = product.qty + datas.push(res.data) + } + setProducts(datas) + } + + getProducts() + }, [promotion.products]) + + const [freeProducts, setFreeProducts] = useState([]) + + useEffect(() => { + const getFreeProducts = async () => { + const datas = [] + for (const product of promotion.free_products) { + const res = await getVariantById(product.product_id) + res.data.qty = product.qty + datas.push(res.data) + } + setFreeProducts(datas) + } + + getFreeProducts() + }, [promotion.free_products]) + + const priceTotal = useMemo(() => { + let total = 0; + [...products, ...freeProducts].forEach((product) => { + total += product.price.price_discount * product.qty + }) + return total + }, [products, freeProducts]) + + const allProducts = [...products, ...freeProducts] + + return ( +
+ + +
+
+
{promotion.name}
+ + +
+ Paket {PROMO_CATEGORY[promotion.type.value].alias} + +
+
+
+ + 0}> + {allProducts.map((product, index) => ( + <> + + + + + {index + 1 < allProducts.length && ( +
+ +
+ )} +
+ + ))} +
+ +
+
+ 0}> + Rp{formatCurrency(priceTotal)} + Hemat Rp {formatCurrency(priceTotal - promotion.price)} + + +
+ Rp{formatCurrency(promotion.price)} + (Total {promotion.total_qty} barang) +
+
+
+ +
+ +
+
+
+ ) +} + +export default ProductPromoCard \ No newline at end of file diff --git a/src-migrate/modules/product-promo/components/CardCountdown.tsx b/src-migrate/modules/product-promo/components/CardCountdown.tsx new file mode 100644 index 00000000..e398a390 --- /dev/null +++ b/src-migrate/modules/product-promo/components/CardCountdown.tsx @@ -0,0 +1,67 @@ +import style from '../styles/card-countdown.module.css' + +import React, { useEffect, useState } from 'react' +import { useQuery } from 'react-query' +import { ClockIcon } from 'lucide-react' +import { Skeleton } from '@chakra-ui/react' +import moment from 'moment' + +import clsxm from '~/common/libs/clsxm' +import { IPromotion } from '~/common/types/promotion' +import { getPromotionProgram } from '~/services/promotionProgram' + +type Props = { + promotion: IPromotion +} + +const ProductPromoCardCountdown = ({ promotion }: Props) => { + const query = useQuery(['promotion-program', promotion.program_id], async () => { + return await getPromotionProgram(promotion.program_id) + }) + + const program = query.data?.data || null + + const [count, setCount] = useState(program?.time_left || 0); + + useEffect(() => { + let interval: NodeJS.Timeout; + + if (program?.time_left && program?.time_left > 0) { + setCount(program?.time_left); + + interval = setInterval(() => { + setCount((prevCount) => prevCount - 1); + }, 1000); + } + + return () => { + clearInterval(interval); + }; + }, [program?.time_left]); + + const duration = moment.duration(count, 'seconds') + + const countdownClass = { + 'text-white': true, + 'bg-[#312782]': promotion.type.value === 'bundling', + 'bg-[#329E44]': promotion.type.value === 'discount_loading', + 'bg-[#FAD147]': promotion.type.value === 'merchandise', + 'text-gray-700': promotion.type.value === 'merchandise', + } + + return ( + + + + + Berakhir dalam +
+ {duration.hours().toString().padStart(2, '0')} + {duration.minutes().toString().padStart(2, '0')} + {duration.seconds().toString().padStart(2, '0')} +
+
+ ) +} + +export default ProductPromoCardCountdown \ No newline at end of file diff --git a/src-migrate/modules/product-promo/components/CategoryTab.tsx b/src-migrate/modules/product-promo/components/CategoryTab.tsx new file mode 100644 index 00000000..edc4aa92 --- /dev/null +++ b/src-migrate/modules/product-promo/components/CategoryTab.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import style from '../styles/category-tab.module.css' +import { useModalStore } from '../stores/useModalStore' +import clsxm from '~/common/libs/clsxm' +import { ICategoryPromo } from '~/common/types/promotion' + +const TABS: ICategoryPromo[] = [ + { value: 'bundling', label: 'Bundling' }, + { value: 'discount_loading', label: 'Discount Loading' }, + { value: 'merchandise', label: 'Free Merchant' }, +] + +const ProductPromoCategoryTab = () => { + const { activeTab, changeTab } = useModalStore() + return ( +
+ {TABS.map((tab) => ( + + ))} +
+ ) +} + +export default ProductPromoCategoryTab \ No newline at end of file diff --git a/src-migrate/modules/product-promo/components/Item.tsx b/src-migrate/modules/product-promo/components/Item.tsx new file mode 100644 index 00000000..058b2f6c --- /dev/null +++ b/src-migrate/modules/product-promo/components/Item.tsx @@ -0,0 +1,24 @@ +import style from '../styles/item.module.css' + +import React from 'react' +import Image from 'next/image' + +import { IProductVariantPromo } from '~/common/types/promotion' + +type Props = { + variant: IProductVariantPromo +} + +const ProductPromoItem = ({ variant }: Props) => { + return ( +
+
+ {variant.display_name} +
{variant.qty} pcs
+
+
{variant.name}
+
+ ) +} + +export default ProductPromoItem \ No newline at end of file diff --git a/src-migrate/modules/product-promo/components/Modal.tsx b/src-migrate/modules/product-promo/components/Modal.tsx new file mode 100644 index 00000000..598b7bbe --- /dev/null +++ b/src-migrate/modules/product-promo/components/Modal.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import Modal from '~/common/components/elements/Modal' +import { useModalStore } from '../stores/useModalStore' +import ProductPromoCategoryTab from './CategoryTab' +import ProductPromoModalContent from './ModalContent' + +const ProductPromoModal = () => { + const { active, closeModal } = useModalStore() + + return ( + + + +
+ + + + ) +} + +export default ProductPromoModal \ No newline at end of file diff --git a/src-migrate/modules/product-promo/components/ModalContent.tsx b/src-migrate/modules/product-promo/components/ModalContent.tsx new file mode 100644 index 00000000..45995af6 --- /dev/null +++ b/src-migrate/modules/product-promo/components/ModalContent.tsx @@ -0,0 +1,33 @@ +import { useQuery } from "react-query" +import { Skeleton } from "@chakra-ui/react" +import { motion } from "framer-motion" + +import { getVariantPromoByCategory } from "~/services/variant" + +import { useModalStore } from "../stores/useModalStore" +import ProductPromoCard from "./Card" + +const ProductPromoModalContent = () => { + const { activeTab, variantId } = useModalStore() + + const promotionsQuery = useQuery( + `variant-promo:${variantId}:${activeTab}`, + async () => { + if (!variantId) return + + return getVariantPromoByCategory(variantId, activeTab) + }, + ) + + const promotions = promotionsQuery.data + + return ( + + {promotions?.data.map((promo) => ( + + ))} + + ) +} + +export default ProductPromoModalContent \ No newline at end of file diff --git a/src-migrate/modules/product-promo/components/Section.tsx b/src-migrate/modules/product-promo/components/Section.tsx new file mode 100644 index 00000000..47e1de29 --- /dev/null +++ b/src-migrate/modules/product-promo/components/Section.tsx @@ -0,0 +1,50 @@ +import style from "../styles/section.module.css" + +import React from 'react' +import { useQuery } from 'react-query' +import { Button, Skeleton } from '@chakra-ui/react' + +import ProductPromoCard from './Card' +import { IPromotion } from '~/common/types/promotion' +import ProductPromoModal from "./Modal" +import { useModalStore } from "../stores/useModalStore" + +type Props = { + productId: number +} + +const ProductPromoSection = ({ productId }: Props) => { + const promotionsQuery = useQuery( + `promotions-highlight:${productId}`, + async () => await fetch(`/api/product-variant/${productId}/promotion/highlight`).then((res) => res.json()) as { data: IPromotion[] }, + ) + + const promotions = promotionsQuery.data + + const { openModal } = useModalStore() + + return ( +
+ + + {promotions?.data && promotions?.data.length > 0 && ( +
+ Promo Tersedia + +
+ )} + + + {promotions?.data.map((promotion) => ( +
+ +
+ ))} +
+
+ ) +} + +export default ProductPromoSection \ No newline at end of file diff --git a/src-migrate/modules/product-promo/stores/useModalStore.ts b/src-migrate/modules/product-promo/stores/useModalStore.ts new file mode 100644 index 00000000..bbb2b1fb --- /dev/null +++ b/src-migrate/modules/product-promo/stores/useModalStore.ts @@ -0,0 +1,28 @@ +import { create } from 'zustand'; +import { CategoryPromo } from '~/common/types/promotion'; + +type State = { + active: boolean; + variantId?: number; + activeTab: CategoryPromo; +}; + +type Action = { + openModal: (variantId: number) => void; + closeModal: () => void; + changeTab: (tab: State['activeTab']) => void; +}; + +const defaultState: Omit = { + active: false, + variantId: undefined, +}; + +export const useModalStore = create((set) => ({ + ...defaultState, + activeTab: 'bundling', + openModal: (variantId: number) => set({ active: true, variantId }), + closeModal: () => set(defaultState), + // TABS + changeTab: (tab) => set({ activeTab: tab }), +})); diff --git a/src-migrate/modules/product-promo/styles/card-countdown.module.css b/src-migrate/modules/product-promo/styles/card-countdown.module.css new file mode 100644 index 00000000..dae8945f --- /dev/null +++ b/src-migrate/modules/product-promo/styles/card-countdown.module.css @@ -0,0 +1,14 @@ +.countdownSection { + @apply w-fit p-2.5 pr-6 + rounded-r-full + font-medium + flex items-center gap-x-2.5; +} + +.countdown { + @apply flex gap-x-1; +} + +.countdown span { + @apply py-0.5 w-8 bg-red-600 text-gray_r-4 rounded-md text-center; +} \ No newline at end of file diff --git a/src-migrate/modules/product-promo/styles/card.module.css b/src-migrate/modules/product-promo/styles/card.module.css new file mode 100644 index 00000000..a2ad9af6 --- /dev/null +++ b/src-migrate/modules/product-promo/styles/card.module.css @@ -0,0 +1,46 @@ +.card { + @apply border border-gray_r-7 + rounded-lg + h-fit + py-3; +} + +.title { + @apply font-semibold text-h-md; +} + +.badgeType { + @apply p-2 flex gap-x-1.5 rounded-md border border-danger-500 text-danger-500; +} + +.productSection { + @apply flex gap-x-2 overflow-x-auto overflow-y-hidden mt-4 min-h-[160px]; +} + +.priceSection { + @apply flex items-center justify-between mt-4; +} + +.priceCol { + @apply flex flex-col gap-y-1; +} + +.priceRow { + @apply flex gap-x-2 items-center; +} + +.basePrice { + @apply line-through; +} + +.savingAmt { + @apply text-success-600 font-medium; +} + +.price { + @apply text-body-1 text-danger-600 font-medium; +} + +.totalItems { + @apply text-gray_r-9; +} diff --git a/src-migrate/modules/product-promo/styles/category-tab.module.css b/src-migrate/modules/product-promo/styles/category-tab.module.css new file mode 100644 index 00000000..cab2cb1b --- /dev/null +++ b/src-migrate/modules/product-promo/styles/category-tab.module.css @@ -0,0 +1,12 @@ +.tabs { + @apply flex gap-x-4; +} + +.tab { + @apply py-1.5 duration-300; + transition-property: background-color; +} + +.tabActive { + @apply cursor-default border-b-2 border-danger-500 font-medium; +} diff --git a/src-migrate/modules/product-promo/styles/item.module.css b/src-migrate/modules/product-promo/styles/item.module.css new file mode 100644 index 00000000..86127836 --- /dev/null +++ b/src-migrate/modules/product-promo/styles/item.module.css @@ -0,0 +1,19 @@ +.item { + @apply min-w-[110px] max-w-[110px]; +} + +.image { + @apply relative border border-gray_r-6 p-2.5 rounded-lg mb-3; +} + +.fillDesc { + @apply mt-2 text-danger-600; +} + +.quantity { + @apply backdrop-blur-lg border border-danger-300 text-danger-600 font-semibold px-2 py-1 text-caption-2 flex items-center justify-center rounded absolute bottom-2.5; +} + +.name { + @apply mt-1 line-clamp-2 leading-5 font-medium; +} diff --git a/src-migrate/modules/product-promo/styles/section.module.css b/src-migrate/modules/product-promo/styles/section.module.css new file mode 100644 index 00000000..d830f5d4 --- /dev/null +++ b/src-migrate/modules/product-promo/styles/section.module.css @@ -0,0 +1,7 @@ +.titleWrapper { + @apply w-full mb-4 h-20 bg-[#C70817] rounded-none md:rounded-lg flex items-center justify-between px-4 py-1; +} + +.title { + @apply font-semibold text-xl text-white; +} diff --git a/src-migrate/modules/product/PromoCard.module.css b/src-migrate/modules/product/PromoCard.module.css deleted file mode 100644 index 4d98671f..00000000 --- a/src-migrate/modules/product/PromoCard.module.css +++ /dev/null @@ -1,62 +0,0 @@ -.card { - @apply border border-gray_r-7 - rounded-lg - min-w-[360px] - max-w-[360px] - py-3; -} - -.countdownSection { - @apply w-fit p-2.5 pr-6 - rounded-r-full - font-medium - flex items-center gap-x-2.5; -} - -.countdown { - @apply flex gap-x-1; -} - -.countdown span { - @apply py-0.5 w-8 bg-red-600 text-gray_r-4 rounded-md text-center; -} - -.title { - @apply font-semibold text-h-md; -} - -.productSection { - @apply flex gap-x-3 mt-4 min-h-[180px]; -} - -.priceSection { - @apply flex items-center justify-between mt-4; -} - -.priceCol { - @apply flex flex-col gap-y-1; -} - -.priceRow { - @apply flex gap-x-2 items-center; -} - -.basePrice { - @apply line-through; -} - -.savingAmt { - @apply text-success-600 font-medium; -} - -.price { - @apply text-body-1 text-danger-600 font-medium; -} - -.totalItems { - @apply text-gray_r-9; -} - -.addToCartBtn { - @apply btn-yellow flex items-center gap-x-1 px-2 rounded-lg; -} diff --git a/src-migrate/modules/product/PromoCard.tsx b/src-migrate/modules/product/PromoCard.tsx deleted file mode 100644 index 8bb48155..00000000 --- a/src-migrate/modules/product/PromoCard.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react' -import style from "./PromoCard.module.css" -import { ClockIcon, PlusIcon } from "lucide-react" -import { IProductVariantPromo, IPromotion } from '~/common/types/promotion' -import formatCurrency from '~/common/libs/formatCurrency' -import PromoProduct from './PromoProduct' -import { Skeleton, Spinner } from '@chakra-ui/react' -import clsxm from '~/common/libs/clsxm' -import { useCountdown } from 'usehooks-ts' - -type Props = { - promotion: IPromotion -} - -const PromoCard = ({ promotion }: Props) => { - // TODO: useCountdown() - const [products, setProducts] = useState([]) - - useEffect(() => { - const getProducts = async () => { - const datas = [] - for (const product of promotion.products) { - const response = await fetch(`/api/product-variant/${product.product_id}`) - const res = await response.json() - res.data.qty = product.qty - datas.push(res.data) - } - setProducts(datas) - } - - getProducts() - }, [promotion.products]) - - const [freeProducts, setFreeProducts] = useState([]) - - useEffect(() => { - const getFreeProducts = async () => { - const datas = [] - for (const product of promotion.free_products) { - const response = await fetch(`/api/product-variant/${product.product_id}`) - const res = await response.json() - res.data.qty = product.qty - datas.push(res.data) - } - setFreeProducts(datas) - } - - getFreeProducts() - }, [promotion.free_products]) - - const priceTotal = useMemo(() => { - let total = 0; - [...products, ...freeProducts].forEach((product) => { - total += product.price.price_discount * product.qty - console.log({ product }); - - }) - return total - }, [products, freeProducts]) - - const countdownClass = { - 'text-white': true, - 'bg-[#312782]': promotion.type.value === 'bundling', - 'bg-[#329E44]': promotion.type.value === 'discount_loading', - 'bg-[#FAD147]': promotion.type.value === 'merchandise', - 'text-gray-700': promotion.type.value === 'merchandise', - } - - return ( -
-
- - - - Berakhir dalam -
- 00 - 01 - 35 -
-
- -
-
{promotion.name}
- - 0}> - {products.map((product) => )} - {freeProducts.map((product) => )} - - -
-
- 0}> - Rp{formatCurrency(priceTotal)} - Hemat Rp {formatCurrency(priceTotal - promotion.price)} - - -
- Rp{formatCurrency(promotion.price)} - (Total {promotion.total_qty} barang) -
-
-
- -
- -
-
-
- ) -} - -export default PromoCard \ No newline at end of file diff --git a/src-migrate/modules/product/PromoProduct.module.css b/src-migrate/modules/product/PromoProduct.module.css deleted file mode 100644 index c13bccb8..00000000 --- a/src-migrate/modules/product/PromoProduct.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.product { - @apply w-4/12; -} - -.image { - @apply border border-gray_r-6 p-2.5 rounded-lg; -} - -.fillDesc { - @apply mt-2 text-danger-600; -} - -.name { - @apply mt-1 line-clamp-3 font-medium; -} diff --git a/src-migrate/modules/product/PromoProduct.tsx b/src-migrate/modules/product/PromoProduct.tsx deleted file mode 100644 index 83b05e88..00000000 --- a/src-migrate/modules/product/PromoProduct.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react' -import style from './PromoProduct.module.css' -import { IProductVariantPromo } from '~/common/types/promotion' -import Image from 'next/image' - -type Props = { - variant: IProductVariantPromo -} - -const PromoProduct = ({ variant }: Props) => { - return ( -
-
- {variant.display_name} -
-
Isi {variant.qty} barang
-
{variant.name}
-
- ) -} - -export default PromoProduct \ No newline at end of file diff --git a/src-migrate/modules/product/PromoSection.module.css b/src-migrate/modules/product/PromoSection.module.css deleted file mode 100644 index a9c9b704..00000000 --- a/src-migrate/modules/product/PromoSection.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.titleWrapper { - @apply w-full mb-4 h-20 bg-[#C70817] rounded-lg flex items-center justify-between px-4 py-1; -} - -.seeMore { - @apply py-2 px-3 btn-yellow rounded-lg text-body-2; -} - -.title { - @apply font-semibold text-xl text-white; -} diff --git a/src-migrate/modules/product/PromoSection.tsx b/src-migrate/modules/product/PromoSection.tsx deleted file mode 100644 index 299cbb78..00000000 --- a/src-migrate/modules/product/PromoSection.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react' -import style from "./PromoSection.module.css" -import PromoCard from './PromoCard' -import { useQuery } from 'react-query' -import { Skeleton } from '@chakra-ui/react' -import { IPromotion } from '~/common/types/promotion' - -type Props = { - productId: number -} - -const PromoSection = ({ productId }: Props) => { - const promotionsQuery = useQuery( - `promotions-highlight:${productId}`, - async () => await fetch(`/api/product-variant/${productId}/promotion/highlight`).then((res) => res.json()) as { data: IPromotion[] }, - ) - - const promotions = promotionsQuery.data - - const handleSeeMore = () => { } - - return ( -
- {promotions?.data && promotions?.data.length > 0 && ( -
- Promo Tersedia - -
- )} - - - {promotions?.data.map((promotion) => ( - - ))} - -
- ) -} - -export default PromoSection \ No newline at end of file diff --git a/src-migrate/pages/api/product-variant/[id].tsx b/src-migrate/pages/api/product-variant/[id].tsx index ec95714d..b3bd4096 100644 --- a/src-migrate/pages/api/product-variant/[id].tsx +++ b/src-migrate/pages/api/product-variant/[id].tsx @@ -20,13 +20,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return } - const variant = await extractVariant(data.response.docs[0], price_tier) + const variant = await map(data.response.docs[0], price_tier) res.status(200).json({ data: variant }) } } -const extractVariant = async (variant: any, price_tier: string) => { +const map = async (variant: any, price_tier: string) => { const data: any = {} data.id = parseInt(variant.id) diff --git a/src-migrate/pages/api/product-variant/[id]/promotion/[category].tsx b/src-migrate/pages/api/product-variant/[id]/promotion/[category].tsx new file mode 100644 index 00000000..b1207c5e --- /dev/null +++ b/src-migrate/pages/api/product-variant/[id]/promotion/[category].tsx @@ -0,0 +1,51 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { SolrResponse } from "~/common/types/solr"; + +const SOLR_HOST = process.env.SOLR_HOST as string + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const productId = req.query.id as string + const category = req.query.category as string + + if (req.method === 'GET') { + const queryParams = new URLSearchParams({ + q: `product_ids:${productId}`, + fq: `type_value_s:${category}`, + rows: '1' + }) + + const response = await fetch(`${SOLR_HOST}/solr/promotion_program_lines/select?${queryParams.toString()}`) + const data: SolrResponse = await response.json() + + const promotions = await map(data.response.docs) + res.status(200).json({ data: promotions }) + } +} + +const map = async (promotions: any[]) => { + const result = [] + + for (const promotion of promotions) { + const data: any = {} + + data.id = promotion.id + data.program_id = promotion.program_id_i + data.name = promotion.name_s + data.type = { + value: promotion.type_value_s, + label: promotion.type_label_s, + } + data.limit = promotion.package_limit_i + data.limit_user = promotion.package_limit_user_i + data.limit_trx = promotion.package_limit_trx_i + data.price = promotion.price_f + data.total_qty = promotion.total_qty_i + + data.products = JSON.parse(promotion.products_s) + data.free_products = JSON.parse(promotion.free_products_s) + + result.push(data) + } + + return result +} \ No newline at end of file diff --git a/src-migrate/pages/api/promotion-program/[id].tsx b/src-migrate/pages/api/promotion-program/[id].tsx new file mode 100644 index 00000000..ba716e85 --- /dev/null +++ b/src-migrate/pages/api/promotion-program/[id].tsx @@ -0,0 +1,43 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { SolrResponse } from "~/common/types/solr"; +import moment from 'moment' + +const SOLR_HOST = process.env.SOLR_HOST as string + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const id = req.query.id as string + + if (req.method === 'GET') { + const queryParams = new URLSearchParams({ q: `id:${id}` }) + const response = await fetch(`${SOLR_HOST}/solr/promotion_programs/select?${queryParams.toString()}`) + const data: SolrResponse = await response.json() + + if (data.response.numFound === 0) { + res.status(404).json({ error: 'Program not found' }) + return + } + + const program = await map(data.response.docs[0]) + + res.status(200).json({ data: program }) + } +} + +const map = async (program: any) => { + const data: any = {} + + data.id = program.id + data.name = program.name_s + data.start_time = program.start_time_s + data.end_time = program.end_time_s + data.applies_to = program.applies_to_s + data.time_left = (new Date(data.end_time).getTime() - new Date().getTime()) / 1000 + + // const duration = moment.duration(data.time_left, 'seconds') + // const days = duration.days() + // const hours = duration.hours() + // const minutes = duration.minutes() + // const seconds = duration.seconds() + + return data +} \ No newline at end of file diff --git a/src-migrate/services/checkout.ts b/src-migrate/services/checkout.ts new file mode 100644 index 00000000..3dd1c8e8 --- /dev/null +++ b/src-migrate/services/checkout.ts @@ -0,0 +1,7 @@ +import odooApi from '~/common/libs/odooApi'; + +export const getUserCheckout = async (userId: number) => { + return await odooApi('GET', `/api/v1/user/${userId}/sale_order/checkout`); +}; + +// /api/v1/user/${id}/sale_order/checkout?voucher=${voucher} \ No newline at end of file diff --git a/src-migrate/services/promotionProgram.ts b/src-migrate/services/promotionProgram.ts new file mode 100644 index 00000000..a5026c71 --- /dev/null +++ b/src-migrate/services/promotionProgram.ts @@ -0,0 +1,8 @@ +import { IPromotionProgram } from '~/common/types/promotionProgram'; + +export const getPromotionProgram = async ( + programId: number +): Promise<{ data: IPromotionProgram }> => { + const url = `/api/promotion-program/${programId}`; + return await fetch(url).then((res) => res.json()); +}; diff --git a/src-migrate/services/variant.ts b/src-migrate/services/variant.ts new file mode 100644 index 00000000..213187d2 --- /dev/null +++ b/src-migrate/services/variant.ts @@ -0,0 +1,14 @@ +import { CategoryPromo, IPromotion } from '~/common/types/promotion'; + +export const getVariantById = async (variantId: number) => { + const url = `/api/product-variant/${variantId}`; + return await fetch(url).then((res) => res.json()); +}; + +export const getVariantPromoByCategory = async ( + variantId: number, + type: CategoryPromo +): Promise<{ data: IPromotion[] }> => { + const url = `/api/product-variant/${variantId}/promotion/${type}`; + return await fetch(url).then((res) => res.json()); +}; -- cgit v1.2.3 From 67398e6f10d6f7729d8f1ace7005ef13d32c5ddd Mon Sep 17 00:00:00 2001 From: Rafi Zadanly Date: Thu, 4 Jan 2024 10:05:25 +0700 Subject: Update promotion program feature --- src-migrate/common/components/elements/Modal.tsx | 2 +- src-migrate/modules/cart/components/Detail.tsx | 1 - src-migrate/modules/cart/components/Item.tsx | 9 +++++---- src-migrate/modules/cart/components/ItemPromo.tsx | 3 +-- src-migrate/modules/cart/stores/useCartStore.ts | 3 ++- src-migrate/modules/cart/styles/item-promo.module.css | 6 +++--- src-migrate/modules/cart/styles/item.module.css | 2 +- .../modules/product-promo/components/AddToCart.tsx | 19 ++++++++++++++++++- src-migrate/modules/product-promo/components/Card.tsx | 10 +++++++--- src-migrate/modules/product-promo/components/Item.tsx | 14 ++++++++++---- .../modules/product-promo/components/ModalContent.tsx | 14 +++++++++----- .../modules/product-promo/styles/item.module.css | 4 ++-- .../api/product-variant/[id]/promotion/[category].tsx | 3 +-- 13 files changed, 60 insertions(+), 30 deletions(-) (limited to 'src-migrate') diff --git a/src-migrate/common/components/elements/Modal.tsx b/src-migrate/common/components/elements/Modal.tsx index 8e488a3a..ea95c0b1 100644 --- a/src-migrate/common/components/elements/Modal.tsx +++ b/src-migrate/common/components/elements/Modal.tsx @@ -37,7 +37,7 @@ const Modal = ({ const modalClassNames = clsxm( "fixed bg-white max-h-[80vh] overflow-auto p-4 pt-0 z-[60] border-gray_r-6", { - "left-1/2 -translate-x-1/2 translate-y-1/2 bottom-1/2 w-11/12 md:w-1/4 lg:w-1/3 border rounded-xl": mode === 'desktop', + "left-1/2 -translate-x-1/2 translate-y-1/2 bottom-1/2 w-11/12 md:w-[500px] border rounded-xl": mode === 'desktop', "left-0 w-full border-t bottom-0 rounded-t-xl": mode === 'mobile' }, className diff --git a/src-migrate/modules/cart/components/Detail.tsx b/src-migrate/modules/cart/components/Detail.tsx index c9de086b..ccb0bb4d 100644 --- a/src-migrate/modules/cart/components/Detail.tsx +++ b/src-migrate/modules/cart/components/Detail.tsx @@ -38,7 +38,6 @@ const CartDetail = () => { return (
- {/*
*/}
Keranjang Belanja
diff --git a/src-migrate/modules/cart/components/Item.tsx b/src-migrate/modules/cart/components/Item.tsx index 92beda86..baf48bb6 100644 --- a/src-migrate/modules/cart/components/Item.tsx +++ b/src-migrate/modules/cart/components/Item.tsx @@ -3,6 +3,9 @@ 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' @@ -10,8 +13,6 @@ import { CartItem as CartItemProps } from '~/common/types/cart' import CartItemPromo from './ItemPromo' import CartItemAction from './ItemAction' import CartItemSelect from './ItemSelect' -import { PROMO_CATEGORY } from '~/constants/promotion' -import { InfoIcon } from 'lucide-react' type Props = { item: CartItemProps @@ -37,7 +38,7 @@ const CartItem = ({ item, editable = true }: Props) => {
Selamat! Pembelian anda lebih hemat {' '} - Rp {formatCurrency((item.package_price || 0) - item.subtotal)} + Rp{formatCurrency((item.package_price || 0) * item.quantity - item.subtotal)}
@@ -61,7 +62,7 @@ const CartItem = ({ item, editable = true }: Props) => { Rp {formatCurrency((item.package_price || 0))} - Rp {formatCurrency(item.subtotal)} + Rp {formatCurrency(item.price.price)}
)} diff --git a/src-migrate/modules/cart/components/ItemPromo.tsx b/src-migrate/modules/cart/components/ItemPromo.tsx index 951d4d6a..bb286e8b 100644 --- a/src-migrate/modules/cart/components/ItemPromo.tsx +++ b/src-migrate/modules/cart/components/ItemPromo.tsx @@ -5,7 +5,6 @@ import React from 'react' import { CartProduct } from '~/common/types/cart' - type Props = { product: CartProduct } @@ -19,7 +18,7 @@ const CartItemPromo = ({ product }: Props) => {
{product.display_name}
-
+
{product.code}
diff --git a/src-migrate/modules/cart/stores/useCartStore.ts b/src-migrate/modules/cart/stores/useCartStore.ts index d3eaadb7..0643b8e6 100644 --- a/src-migrate/modules/cart/stores/useCartStore.ts +++ b/src-migrate/modules/cart/stores/useCartStore.ts @@ -48,7 +48,8 @@ const computeSummary = (cart: CartProps) => { if (!item.selected) continue; let price = 0; - if (item.cart_type === 'promotion') price = item?.package_price || 0; + if (item.cart_type === 'promotion') + price = (item?.package_price || 0) * item.quantity; else if (item.cart_type === 'product') price = item.price.price * item.quantity; diff --git a/src-migrate/modules/cart/styles/item-promo.module.css b/src-migrate/modules/cart/styles/item-promo.module.css index 17dbf1c7..5bc192c0 100644 --- a/src-migrate/modules/cart/styles/item-promo.module.css +++ b/src-migrate/modules/cart/styles/item-promo.module.css @@ -1,5 +1,5 @@ .wrapper { - @apply md:ml-16 ml-12 mt-4 flex; + @apply md:ml-12 ml-8 mt-4 flex; } .imageWrapper { @@ -14,7 +14,7 @@ } .details { - @apply ml-4 flex flex-col gap-y-1; + @apply ml-4 w-full flex flex-col gap-y-1; } .name { @@ -27,5 +27,5 @@ } .quantity { - @apply w-12 min-w-[48px] py-2.5 bg-gray-100 border border-gray-300 h-fit my-auto rounded-md ml-auto font-medium text-center; + @apply w-12 min-w-[42px] py-2.5 bg-gray-100 border border-gray-300 h-fit my-auto rounded-md ml-auto font-medium text-center; } diff --git a/src-migrate/modules/cart/styles/item.module.css b/src-migrate/modules/cart/styles/item.module.css index 6380cdad..dfbbf5e8 100644 --- a/src-migrate/modules/cart/styles/item.module.css +++ b/src-migrate/modules/cart/styles/item.module.css @@ -7,7 +7,7 @@ } .badgeType { - @apply min-w-fit p-2 flex gap-x-1.5 rounded-md border border-danger-500 text-danger-500; + @apply min-w-fit p-2 flex items-center gap-x-1.5 rounded-md border border-danger-500 text-danger-500; } .mainProdWrapper { diff --git a/src-migrate/modules/product-promo/components/AddToCart.tsx b/src-migrate/modules/product-promo/components/AddToCart.tsx index 9d856ccf..58bb2ad7 100644 --- a/src-migrate/modules/product-promo/components/AddToCart.tsx +++ b/src-migrate/modules/product-promo/components/AddToCart.tsx @@ -4,6 +4,8 @@ import { IPromotion } from '~/common/types/promotion' import { upsertUserCart } from '~/services/cart' import { getAuth } from '~/common/libs/auth' import { Button, Spinner, useToast } from '@chakra-ui/react' +import Link from 'next/link' +import { useRouter } from 'next/router' type Props = { promotion: IPromotion @@ -14,11 +16,26 @@ type Status = 'idle' | 'loading' | 'success' const ProductPromoAddToCart = ({ promotion }: Props) => { const auth = getAuth() const toast = useToast() + const router = useRouter() const [status, setStatus] = useState('idle') const handleButton = async () => { - if (typeof auth !== 'object') return + if (typeof auth !== 'object') { + const currentUrl = encodeURIComponent(router.asPath) + toast({ + title: 'Masuk Akun', + description: <> + Masuk akun untuk dapat menambahkan promo ke keranjang belanja. {' '} + Klik disini + , + status: 'error', + duration: 4000, + isClosable: true, + position: 'top', + }) + return + } if (status === 'success') return setStatus('loading') diff --git a/src-migrate/modules/product-promo/components/Card.tsx b/src-migrate/modules/product-promo/components/Card.tsx index 2874c2cc..e894c143 100644 --- a/src-migrate/modules/product-promo/components/Card.tsx +++ b/src-migrate/modules/product-promo/components/Card.tsx @@ -5,11 +5,12 @@ import { InfoIcon, PlusIcon } from "lucide-react" import { Skeleton, Tooltip } from '@chakra-ui/react' import { motion } from "framer-motion" +import { PROMO_CATEGORY } from "~/constants/promotion" +import { getVariantById } from "~/services/variant" + import { IProductVariantPromo, IPromotion } from '~/common/types/promotion' import formatCurrency from '~/common/libs/formatCurrency' import clsxm from '~/common/libs/clsxm' -import { PROMO_CATEGORY } from "~/constants/promotion" -import { getVariantById } from "~/services/variant" import ProductPromoItem from './Item' import ProductPromoAddToCart from "./AddToCart" @@ -82,7 +83,10 @@ const ProductPromoCard = ({ promotion }: Props) => { {allProducts.map((product, index) => ( <> - + products.length && promotion.type.value === 'merchandise'} + /> {index + 1 < allProducts.length && ( diff --git a/src-migrate/modules/product-promo/components/Item.tsx b/src-migrate/modules/product-promo/components/Item.tsx index 058b2f6c..15ca4878 100644 --- a/src-migrate/modules/product-promo/components/Item.tsx +++ b/src-migrate/modules/product-promo/components/Item.tsx @@ -6,15 +6,21 @@ import Image from 'next/image' import { IProductVariantPromo } from '~/common/types/promotion' type Props = { - variant: IProductVariantPromo + variant: IProductVariantPromo, + isFree?: boolean } -const ProductPromoItem = ({ variant }: Props) => { +const ProductPromoItem = ({ + variant, + isFree = false +}: Props) => { return (
- {variant.display_name} -
{variant.qty} pcs
+ {variant.display_name} +
+ {variant.qty} pcs {isFree ? '(free)' : ''} +
{variant.name}
diff --git a/src-migrate/modules/product-promo/components/ModalContent.tsx b/src-migrate/modules/product-promo/components/ModalContent.tsx index 45995af6..90cf79e7 100644 --- a/src-migrate/modules/product-promo/components/ModalContent.tsx +++ b/src-migrate/modules/product-promo/components/ModalContent.tsx @@ -1,6 +1,5 @@ import { useQuery } from "react-query" import { Skeleton } from "@chakra-ui/react" -import { motion } from "framer-motion" import { getVariantPromoByCategory } from "~/services/variant" @@ -22,10 +21,15 @@ const ProductPromoModalContent = () => { const promotions = promotionsQuery.data return ( - - {promotions?.data.map((promo) => ( - - ))} + +
+ {promotions?.data.map((promo) => ( + + ))} + {promotions?.data.length === 0 && ( +
Belum ada promo pada kategori ini
+ )} +
) } diff --git a/src-migrate/modules/product-promo/styles/item.module.css b/src-migrate/modules/product-promo/styles/item.module.css index 86127836..b6a8b2ef 100644 --- a/src-migrate/modules/product-promo/styles/item.module.css +++ b/src-migrate/modules/product-promo/styles/item.module.css @@ -1,9 +1,9 @@ .item { - @apply min-w-[110px] max-w-[110px]; + @apply w-[100px] h-[100px]; } .image { - @apply relative border border-gray_r-6 p-2.5 rounded-lg mb-3; + @apply w-full h-[100px] relative border border-gray_r-6 p-2.5 rounded-lg mb-3; } .fillDesc { diff --git a/src-migrate/pages/api/product-variant/[id]/promotion/[category].tsx b/src-migrate/pages/api/product-variant/[id]/promotion/[category].tsx index b1207c5e..745f9944 100644 --- a/src-migrate/pages/api/product-variant/[id]/promotion/[category].tsx +++ b/src-migrate/pages/api/product-variant/[id]/promotion/[category].tsx @@ -10,8 +10,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (req.method === 'GET') { const queryParams = new URLSearchParams({ q: `product_ids:${productId}`, - fq: `type_value_s:${category}`, - rows: '1' + fq: `type_value_s:${category}` }) const response = await fetch(`${SOLR_HOST}/solr/promotion_program_lines/select?${queryParams.toString()}`) -- cgit v1.2.3