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