summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRafi Zadanly <zadanlyr@gmail.com>2023-12-22 17:33:46 +0700
committerRafi Zadanly <zadanlyr@gmail.com>2023-12-22 17:33:46 +0700
commit89f32128f37d99b490de7590e2116a9cfd853f89 (patch)
treefeb74cc6bd0030b291fbf3dbba9b89a7afd6ea31
parentc9366090153e8aba3a673b2b77cbc8acc24e59a5 (diff)
Update promotion program feature
-rw-r--r--src-migrate/common/libs/parse0
-rw-r--r--src-migrate/common/types/cart.ts4
-rw-r--r--src-migrate/common/types/checkout.ts16
-rw-r--r--src-migrate/common/types/promotion.ts9
-rw-r--r--src-migrate/common/types/promotionProgram.ts8
-rw-r--r--src-migrate/constants/promotion.ts17
-rw-r--r--src-migrate/modules/cart/components/Detail.tsx (renamed from src-migrate/modules/cart/components/CartDetail.tsx)22
-rw-r--r--src-migrate/modules/cart/components/Item.tsx109
-rw-r--r--src-migrate/modules/cart/components/ItemAction.tsx (renamed from src-migrate/modules/cart/components/CartItemAction.tsx)3
-rw-r--r--src-migrate/modules/cart/components/ItemPromo.tsx41
-rw-r--r--src-migrate/modules/cart/components/ItemSelect.tsx (renamed from src-migrate/modules/cart/components/CartItemSelect.tsx)2
-rw-r--r--src-migrate/modules/cart/components/Summary.tsx (renamed from src-migrate/modules/cart/ui/CartSummary.tsx)3
-rw-r--r--src-migrate/modules/cart/stores/useCartStore.ts4
-rw-r--r--src-migrate/modules/cart/styles/CartItem.module.css47
-rw-r--r--src-migrate/modules/cart/styles/ProductPromo.module.css24
-rw-r--r--src-migrate/modules/cart/styles/detail.module.css (renamed from src-migrate/modules/cart/styles/CartDetail.module.css)0
-rw-r--r--src-migrate/modules/cart/styles/item-action.module.css (renamed from src-migrate/modules/cart/styles/CartItemAction.module.css)0
-rw-r--r--src-migrate/modules/cart/styles/item-promo.module.css31
-rw-r--r--src-migrate/modules/cart/styles/item.module.css60
-rw-r--r--src-migrate/modules/cart/styles/summary.module.css (renamed from src-migrate/modules/cart/styles/CartSummary.module.css)0
-rw-r--r--src-migrate/modules/cart/ui/CartItem.tsx80
-rw-r--r--src-migrate/modules/cart/ui/ProductPromo.tsx33
-rw-r--r--src-migrate/modules/product-promo/components/AddToCart.tsx61
-rw-r--r--src-migrate/modules/product-promo/components/Card.tsx (renamed from src-migrate/modules/product/PromoCard.tsx)85
-rw-r--r--src-migrate/modules/product-promo/components/CardCountdown.tsx67
-rw-r--r--src-migrate/modules/product-promo/components/CategoryTab.tsx34
-rw-r--r--src-migrate/modules/product-promo/components/Item.tsx (renamed from src-migrate/modules/product/PromoProduct.tsx)14
-rw-r--r--src-migrate/modules/product-promo/components/Modal.tsx25
-rw-r--r--src-migrate/modules/product-promo/components/ModalContent.tsx33
-rw-r--r--src-migrate/modules/product-promo/components/Section.tsx (renamed from src-migrate/modules/product/PromoSection.tsx)28
-rw-r--r--src-migrate/modules/product-promo/stores/useModalStore.ts28
-rw-r--r--src-migrate/modules/product-promo/styles/card-countdown.module.css14
-rw-r--r--src-migrate/modules/product-promo/styles/card.module.css (renamed from src-migrate/modules/product/PromoCard.module.css)28
-rw-r--r--src-migrate/modules/product-promo/styles/category-tab.module.css12
-rw-r--r--src-migrate/modules/product-promo/styles/item.module.css19
-rw-r--r--src-migrate/modules/product-promo/styles/section.module.css7
-rw-r--r--src-migrate/modules/product/PromoProduct.module.css15
-rw-r--r--src-migrate/modules/product/PromoSection.module.css11
-rw-r--r--src-migrate/pages/api/product-variant/[id].tsx4
-rw-r--r--src-migrate/pages/api/product-variant/[id]/promotion/[category].tsx51
-rw-r--r--src-migrate/pages/api/promotion-program/[id].tsx43
-rw-r--r--src-migrate/services/checkout.ts7
-rw-r--r--src-migrate/services/promotionProgram.ts8
-rw-r--r--src-migrate/services/variant.ts14
-rw-r--r--src/pages/api/product-variant/[id]/promotion/[category].js2
-rw-r--r--src/pages/api/promotion-program/[id].js2
46 files changed, 821 insertions, 304 deletions
diff --git a/src-migrate/common/libs/parse b/src-migrate/common/libs/parse
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src-migrate/common/libs/parse
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/Detail.tsx
index 734c61d3..c9de086b 100644
--- a/src-migrate/modules/cart/components/CartDetail.tsx
+++ b/src-migrate/modules/cart/components/Detail.tsx
@@ -1,10 +1,14 @@
+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 '../ui/CartItem'
-import style from '../styles/CartDetail.module.css'
-import CartSummary from '../ui/CartSummary'
-import { Button, Tooltip } from '@chakra-ui/react'
+
+import CartItem from './Item'
+import CartSummary from './Summary'
const CartDetail = () => {
const auth = getAuth()
@@ -46,7 +50,7 @@ const CartDetail = () => {
</div>
</div>
- <div className='w-full md:w-1/4 pl-6'>
+ <div className='w-full md:w-1/4 md:pl-6 mt-6 md:mt-0'>
<div className='border border-gray-300 p-4 rounded-md sticky top-[180px]'>
<CartSummary {...summary} isLoaded={!!cart} />
<div className='grid grid-cols-2 gap-x-3 mt-6'>
@@ -54,7 +58,8 @@ const CartDetail = () => {
<Button
colorScheme='yellow'
w='full'
- isDisabled={hasSelectedPromo || !hasSelected}>
+ isDisabled={hasSelectedPromo || !hasSelected}
+ >
Quotation
</Button>
</Tooltip>
@@ -62,7 +67,10 @@ const CartDetail = () => {
<Button
colorScheme='red'
w='full'
- isDisabled={!hasSelected}>
+ isDisabled={!hasSelected}
+ as={Link}
+ href='/shop/checkout'
+ >
Checkout
</Button>
</Tooltip>
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 (
+ <div className={style.wrapper}>
+ {item.cart_type === 'promotion' && (
+ <div className={style.header}>
+ {item.promotion_type?.value && (
+ <Tooltip label={PROMO_CATEGORY[item.promotion_type?.value].description} placement="top" bgColor='red.600' p={2} rounded={6}>
+ <div className={style.badgeType}>
+ Paket {PROMO_CATEGORY[item.promotion_type?.value].alias}
+ <InfoIcon size={14} />
+ </div>
+ </Tooltip>
+ )}
+ <div className='w-2' />
+ <div>
+ Selamat! Pembelian anda lebih hemat {' '}
+ <span className={style.savingAmt}>
+ Rp {formatCurrency((item.package_price || 0) - item.subtotal)}
+ </span>
+ </div>
+ </div>
+ )}
+
+ <div className={style.mainProdWrapper}>
+ {editable && <CartItemSelect item={item} />}
+ <div className='w-4' />
+ <div className={style.image}>
+ {image && <Image src={image} alt={item.name} width={128} height={128} />}
+ {!image && <div className={style.noImage}>No Image</div>}
+ </div>
+
+ <div className={style.details}>
+ <div className={style.name}>{item.name}</div>
+ <div className='mt-2 flex justify-between w-full'>
+ <div className='flex flex-col gap-y-1'>
+ {item.cart_type === 'promotion' && (
+ <div className={style.discPriceSection}>
+ <span className={style.priceBefore}>
+ Rp {formatCurrency((item.package_price || 0))}
+ </span>
+ <span className={style.price}>
+ Rp {formatCurrency(item.subtotal)}
+ </span>
+ </div>
+ )}
+
+ {item.cart_type === 'product' && (
+ <>
+ <div className={style.price}>
+ Rp {formatCurrency(item.price.price)}
+ </div>
+ <div>{item.code}</div>
+ </>
+ )}
+
+ <div>
+ {item.weight} Kg
+ </div>
+ </div>
+
+ {editable && <CartItemAction item={item} />}
+ {!editable && <div className={style.quantity}>{item.quantity}</div>}
+ </div>
+ </div>
+
+ </div>
+
+ <div className="flex flex-col">
+ {item.products?.map((product) => <CartItemPromo key={product.id} product={product} />)}
+ {item.free_products?.map((product) => <CartItemPromo key={product.id} product={product} />)}
+ </div>
+ </div>
+ )
+}
+
+CartItem.Skeleton = function CartItemSkeleton(props: SkeletonProps & { count: number }) {
+ return Array.from({ length: props.count }).map((_, index) => (
+ <Skeleton key={index}
+ height='100px'
+ width='100%'
+ rounded='md'
+ {...props}
+ />
+ ))
+}
+
+export default CartItem \ No newline at end of file
diff --git a/src-migrate/modules/cart/components/CartItemAction.tsx b/src-migrate/modules/cart/components/ItemAction.tsx
index 742d1a39..3e264aef 100644
--- a/src-migrate/modules/cart/components/CartItemAction.tsx
+++ b/src-migrate/modules/cart/components/ItemAction.tsx
@@ -1,3 +1,5 @@
+import style from '../styles/item-action.module.css'
+
import React, { useEffect, useState } from 'react'
import { Spinner, Tooltip } from '@chakra-ui/react'
@@ -10,7 +12,6 @@ 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
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 (
+ <div key={product.id} className={style.wrapper}>
+ <div className={style.imageWrapper}>
+ {product?.image && <Image src={product.image} alt={product.name} width={128} height={128} className={style.image} />}
+ </div>
+
+ <div className={style.details}>
+ <div className={style.name}>{product.display_name}</div>
+ <div className='flex'>
+ <div className="flex flex-col">
+ <div className={style.code}>{product.code}</div>
+ <div>
+ <span className={style.weightLabel}>Berat Barang: </span>
+ <span>{product.package_weight} Kg</span>
+ </div>
+ </div>
+
+ <div className={style.quantity}>
+ {product.qty}
+ </div>
+ </div>
+ </div>
+
+ </div>
+ )
+}
+
+export default CartItemPromo \ No newline at end of file
diff --git a/src-migrate/modules/cart/components/CartItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx
index f44b0d7e..96e7c713 100644
--- a/src-migrate/modules/cart/components/CartItemSelect.tsx
+++ b/src-migrate/modules/cart/components/ItemSelect.tsx
@@ -1,8 +1,10 @@
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 = {
diff --git a/src-migrate/modules/cart/ui/CartSummary.tsx b/src-migrate/modules/cart/components/Summary.tsx
index 390c1c77..a835bca9 100644
--- a/src-migrate/modules/cart/ui/CartSummary.tsx
+++ b/src-migrate/modules/cart/components/Summary.tsx
@@ -1,5 +1,6 @@
+import style from '../styles/summary.module.css'
+
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'
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/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/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/CartDetail.module.css b/src-migrate/modules/cart/styles/detail.module.css
index 42d492bb..42d492bb 100644
--- a/src-migrate/modules/cart/styles/CartDetail.module.css
+++ b/src-migrate/modules/cart/styles/detail.module.css
diff --git a/src-migrate/modules/cart/styles/CartItemAction.module.css b/src-migrate/modules/cart/styles/item-action.module.css
index e4db7fa5..e4db7fa5 100644
--- a/src-migrate/modules/cart/styles/CartItemAction.module.css
+++ b/src-migrate/modules/cart/styles/item-action.module.css
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/CartSummary.module.css b/src-migrate/modules/cart/styles/summary.module.css
index 48ccec28..48ccec28 100644
--- a/src-migrate/modules/cart/styles/CartSummary.module.css
+++ b/src-migrate/modules/cart/styles/summary.module.css
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 (
- <div className={style.wrapper}>
- <div className={style.mainProdWrapper}>
- <CartItemSelect item={item} />
- <div className='w-4' />
- <div className={style.image}>
- {image && <Image src={image} alt={item.name} width={128} height={128} />}
- {!image && <div className={style.noImage}>No Image</div>}
- </div>
-
- <div className={style.details}>
- <div className={style.name}>{item.name}</div>
- <div className={style.spacing2} />
- {item.cart_type === 'promotion' && (
- <div className={style.discPriceSection}>
- <span className={style.priceBefore}>
- Rp {formatCurrency((item.package_price || 0))}
- </span>
- <span className={style.savingAmt}>
- Hemat Rp {formatCurrency((item.package_price || 0) - item.subtotal)}
- </span>
- <span className={style.price}>
- Rp {formatCurrency(item.subtotal)}
- </span>
- </div>
- )}
- {item.cart_type === 'product' && (
- <>
- <div className={style.price}>
- Rp {formatCurrency(item.price.price)}
- </div>
- <div>{item.code}</div>
- </>
- )}
- <div>
- <span className={style.weightLabel}>Berat barang: </span>
- {item.weight} Kg
- </div>
- </div>
-
- <CartItemAction item={item} />
- </div>
-
- <div className="flex flex-col">
- {item.products?.map((product) => <ProductPromo key={product.id} product={product} />)}
- {item.free_products?.map((product) => <ProductPromo key={product.id} product={product} />)}
- </div>
- </div>
- )
-}
-
-CartItem.Skeleton = function CartItemSkeleton(props: SkeletonProps & { count: number }) {
- return Array.from({ length: props.count }).map((_, index) => (
- <Skeleton key={index}
- height='100px'
- width='100%'
- rounded='md'
- {...props}
- />
- ))
-}
-
-export default CartItem \ No newline at end of file
diff --git a/src-migrate/modules/cart/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 (
- <div key={product.id} className={style.wrapper}>
- <div className={style.imageWrapper}>
- {product?.image && <Image src={product.image} alt={product.name} width={128} height={128} />}
- </div>
-
- <div className={style.details}>
- <div className={style.name}>{product.display_name}</div>
- <div className={style.code}>{product.code}</div>
- <div>
- <span className={style.weightLabel}>Berat Barang: </span>
- <span>{product.package_weight} Kg</span>
- </div>
- </div>
-
- <div className={style.quantity}>
- {product.qty}
- </div>
- </div>
- )
-}
-
-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<Status>('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 (
+ <Button
+ colorScheme='yellow'
+ px={2}
+ w='110px'
+ gap={1}
+ isDisabled={status === 'loading'}
+ onClick={handleButton}
+ >
+ {status === 'success' && <CheckIcon size={16} />}
+ {status === 'loading' && <Spinner size='xs' mr={1.5} />}
+ {status === 'idle' && <PlusIcon size={16} />}
+
+ {status === 'success' && <span>Berhasil</span>}
+ {status !== 'success' && <span>Keranjang</span>}
+ </Button>
+ )
+}
+
+export default ProductPromoAddToCart \ No newline at end of file
diff --git a/src-migrate/modules/product/PromoCard.tsx b/src-migrate/modules/product-promo/components/Card.tsx
index 8bb48155..2874c2cc 100644
--- a/src-migrate/modules/product/PromoCard.tsx
+++ b/src-migrate/modules/product-promo/components/Card.tsx
@@ -1,27 +1,32 @@
+import style from "../styles/card.module.css"
+
import React, { useEffect, useMemo, useState } from 'react'
-import style from "./PromoCard.module.css"
-import { ClockIcon, PlusIcon } from "lucide-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 PromoProduct from './PromoProduct'
-import { Skeleton, Spinner } from '@chakra-ui/react'
import clsxm from '~/common/libs/clsxm'
-import { useCountdown } from 'usehooks-ts'
+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 PromoCard = ({ promotion }: Props) => {
- // TODO: useCountdown()
+const ProductPromoCard = ({ promotion }: Props) => {
const [products, setProducts] = useState<IProductVariantPromo[]>([])
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()
+ const res = await getVariantById(product.product_id)
res.data.qty = product.qty
datas.push(res.data)
}
@@ -37,8 +42,7 @@ const PromoCard = ({ promotion }: Props) => {
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()
+ const res = await getVariantById(product.product_id)
res.data.qty = product.qty
datas.push(res.data)
}
@@ -52,40 +56,43 @@ const PromoCard = ({ promotion }: Props) => {
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',
- }
+ const allProducts = [...products, ...freeProducts]
return (
<div className={style.card}>
- <div className={clsxm(style.countdownSection, countdownClass)}>
- <span>
- <ClockIcon size={20} />
- </span>
- <span>Berakhir dalam</span>
- <div className={style.countdown}>
- <span>00</span>
- <span>01</span>
- <span>35</span>
- </div>
- </div>
+ <ProductPromoCardCountdown promotion={promotion} />
<div className='px-4 mt-4 text-caption-1'>
- <div className={style.title}>{promotion.name}</div>
+ <div className="flex justify-between items-center">
+ <div className={style.title}>{promotion.name}</div>
+
+ <Tooltip label={PROMO_CATEGORY[promotion.type.value].description} placement="top" bgColor='red.600' p={2} rounded={6}>
+ <div className={style.badgeType}>
+ Paket {PROMO_CATEGORY[promotion.type.value].alias}
+ <InfoIcon size={16} />
+ </div>
+ </Tooltip>
+ </div>
- <Skeleton className={style.productSection} isLoaded={[...products, ...freeProducts].length > 0}>
- {products.map((product) => <PromoProduct key={product.id} variant={product} />)}
- {freeProducts.map((product) => <PromoProduct key={product.id} variant={product} />)}
+ <Skeleton className={clsxm(style.productSection, { 'justify-center': allProducts.length === 2 })} isLoaded={allProducts.length > 0}>
+ {allProducts.map((product, index) => (
+ <>
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.6 }}>
+ <ProductPromoItem variant={product} />
+ </motion.div>
+ <motion.div initial={{ y: 30, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ duration: 0.5, delay: 0.1 }}>
+ {index + 1 < allProducts.length && (
+ <div className="h-fit p-1 rounded-full border border-danger-500 text-danger-500 mt-[38px]">
+ <PlusIcon size={14} strokeWidth='2px' />
+ </div>
+ )}
+ </motion.div>
+ </>
+ ))}
</Skeleton>
<div className={style.priceSection}>
@@ -101,11 +108,7 @@ const PromoCard = ({ promotion }: Props) => {
</div>
</div>
<div>
- <button className={style.addToCartBtn}>
- {/* <PlusIcon size={16} /> */}
- <Spinner size='xs' mr={1.5} />
- Keranjang
- </button>
+ <ProductPromoAddToCart promotion={promotion} />
</div>
</div>
@@ -114,4 +117,4 @@ const PromoCard = ({ promotion }: Props) => {
)
}
-export default PromoCard \ No newline at end of file
+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 (
+ <Skeleton isLoaded={query.isFetched} className={clsxm(style.countdownSection, countdownClass)}>
+ <span>
+ <ClockIcon size={20} />
+ </span>
+ <span>Berakhir dalam</span>
+ <div className={style.countdown}>
+ <span>{duration.hours().toString().padStart(2, '0')}</span>
+ <span>{duration.minutes().toString().padStart(2, '0')}</span>
+ <span>{duration.seconds().toString().padStart(2, '0')}</span>
+ </div>
+ </Skeleton>
+ )
+}
+
+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 (
+ <div className={style.tabs}>
+ {TABS.map((tab) => (
+ <button
+ key={tab.value}
+ type='button'
+ className={clsxm({
+ [style.tab]: true,
+ [style.tabActive]: activeTab === tab.value
+ })}
+ onClick={() => changeTab(tab.value)}
+ >
+ {tab.label}
+ </button>
+ ))}
+ </div>
+ )
+}
+
+export default ProductPromoCategoryTab \ No newline at end of file
diff --git a/src-migrate/modules/product/PromoProduct.tsx b/src-migrate/modules/product-promo/components/Item.tsx
index 83b05e88..058b2f6c 100644
--- a/src-migrate/modules/product/PromoProduct.tsx
+++ b/src-migrate/modules/product-promo/components/Item.tsx
@@ -1,22 +1,24 @@
+import style from '../styles/item.module.css'
+
import React from 'react'
-import style from './PromoProduct.module.css'
-import { IProductVariantPromo } from '~/common/types/promotion'
import Image from 'next/image'
+import { IProductVariantPromo } from '~/common/types/promotion'
+
type Props = {
variant: IProductVariantPromo
}
-const PromoProduct = ({ variant }: Props) => {
+const ProductPromoItem = ({ variant }: Props) => {
return (
- <div className={style.product}>
+ <div className={style.item}>
<div className={style.image}>
<Image src={variant.image} alt={variant.display_name} width={320} height={320} />
+ <div className={style.quantity}>{variant.qty} pcs</div>
</div>
- <div className={style.fillDesc}>Isi {variant.qty} barang</div>
<div className={style.name}>{variant.name}</div>
</div>
)
}
-export default PromoProduct \ No newline at end of file
+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 (
+ <Modal
+ active={active}
+ close={closeModal}
+ title='Promo Tersedia'
+ >
+ <ProductPromoCategoryTab />
+
+ <div className='h-4' />
+
+ <ProductPromoModalContent />
+ </Modal>
+ )
+}
+
+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 (
+ <Skeleton isLoaded={!promotionsQuery.isLoading} className='min-h-[60vh] grid grid-cols-1'>
+ {promotions?.data.map((promo) => (
+ <ProductPromoCard key={promo.id} promotion={promo} />
+ ))}
+ </Skeleton>
+ )
+}
+
+export default ProductPromoModalContent \ No newline at end of file
diff --git a/src-migrate/modules/product/PromoSection.tsx b/src-migrate/modules/product-promo/components/Section.tsx
index 299cbb78..47e1de29 100644
--- a/src-migrate/modules/product/PromoSection.tsx
+++ b/src-migrate/modules/product-promo/components/Section.tsx
@@ -1,15 +1,19 @@
+import style from "../styles/section.module.css"
+
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 { 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 PromoSection = ({ productId }: Props) => {
+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[] },
@@ -17,26 +21,30 @@ const PromoSection = ({ productId }: Props) => {
const promotions = promotionsQuery.data
- const handleSeeMore = () => { }
+ const { openModal } = useModalStore()
return (
<div className='w-full'>
+ <ProductPromoModal />
+
{promotions?.data && promotions?.data.length > 0 && (
<div className={style.titleWrapper}>
<span className={style.title}>Promo Tersedia</span>
- <button type='button' onClick={handleSeeMore} className={style.seeMore}>
+ <Button colorScheme="yellow" type='button' onClick={() => openModal(productId)}>
Lihat Semua
- </button>
+ </Button>
</div>
)}
- <Skeleton isLoaded={promotionsQuery.isSuccess} className="flex gap-x-4 overflow-x-auto min-h-[340px]">
+ <Skeleton isLoaded={promotionsQuery.isSuccess} className="flex gap-x-4 overflow-x-auto min-h-[340px] px-4 md:px-0">
{promotions?.data.map((promotion) => (
- <PromoCard key={promotion.id} promotion={promotion} />
+ <div key={promotion.id} className="min-w-[400px] max-w-[400px]">
+ <ProductPromoCard promotion={promotion} />
+ </div>
))}
</Skeleton>
</div>
)
}
-export default PromoSection \ No newline at end of file
+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<State, 'activeTab'> = {
+ active: false,
+ variantId: undefined,
+};
+
+export const useModalStore = create<State & Action>((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/PromoCard.module.css b/src-migrate/modules/product-promo/styles/card.module.css
index 4d98671f..a2ad9af6 100644
--- a/src-migrate/modules/product/PromoCard.module.css
+++ b/src-migrate/modules/product-promo/styles/card.module.css
@@ -1,32 +1,20 @@
.card {
@apply border border-gray_r-7
rounded-lg
- min-w-[360px]
- max-w-[360px]
+ h-fit
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;
}
+.badgeType {
+ @apply p-2 flex gap-x-1.5 rounded-md border border-danger-500 text-danger-500;
+}
+
.productSection {
- @apply flex gap-x-3 mt-4 min-h-[180px];
+ @apply flex gap-x-2 overflow-x-auto overflow-y-hidden mt-4 min-h-[160px];
}
.priceSection {
@@ -56,7 +44,3 @@
.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-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/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/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/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<any[]> = 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<any[]> = 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());
+};
diff --git a/src/pages/api/product-variant/[id]/promotion/[category].js b/src/pages/api/product-variant/[id]/promotion/[category].js
new file mode 100644
index 00000000..aef03c22
--- /dev/null
+++ b/src/pages/api/product-variant/[id]/promotion/[category].js
@@ -0,0 +1,2 @@
+import handler from '~/pages/api/product-variant/[id]/promotion/[category]';
+export default handler;
diff --git a/src/pages/api/promotion-program/[id].js b/src/pages/api/promotion-program/[id].js
new file mode 100644
index 00000000..f2bb550e
--- /dev/null
+++ b/src/pages/api/promotion-program/[id].js
@@ -0,0 +1,2 @@
+import handler from '~/pages/api/promotion-program/[id]';
+export default handler;