summaryrefslogtreecommitdiff
path: root/src-migrate/modules
diff options
context:
space:
mode:
Diffstat (limited to 'src-migrate/modules')
-rw-r--r--src-migrate/modules/cart/components/Item.tsx20
-rw-r--r--src-migrate/modules/cart/components/ItemSelect.tsx26
-rw-r--r--src-migrate/modules/cart/stores/useCartStore.ts11
-rw-r--r--src-migrate/modules/footer-banner/index.tsx2
-rw-r--r--src-migrate/modules/product-detail/components/AddToCart.tsx171
-rw-r--r--src-migrate/modules/product-detail/components/PriceAction.tsx2
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx2
-rw-r--r--src-migrate/modules/product-promo/components/AddToCart.tsx113
-rw-r--r--src-migrate/modules/product-promo/components/Card.tsx16
-rw-r--r--src-migrate/modules/product-promo/components/Modal.tsx8
-rw-r--r--src-migrate/modules/product-promo/components/ModalContent.tsx8
-rw-r--r--src-migrate/modules/product-promo/components/Section.tsx9
-rw-r--r--src-migrate/modules/product-promo/styles/card.module.css2
-rw-r--r--src-migrate/modules/promo/components/FlashSale.tsx20
-rw-r--r--src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx20
-rw-r--r--src-migrate/modules/promo/components/Hero.tsx105
-rw-r--r--src-migrate/modules/promo/components/HeroDiskon.tsx137
-rw-r--r--src-migrate/modules/promo/components/PromoList.tsx135
-rw-r--r--src-migrate/modules/promo/components/PromotinProgram.jsx134
-rw-r--r--src-migrate/modules/promo/components/Voucher.tsx160
-rw-r--r--src-migrate/modules/promo/components/promoStore.js16
-rw-r--r--src-migrate/modules/promo/styles/hero.module.css27
-rw-r--r--src-migrate/modules/promo/styles/voucher.module.css43
-rw-r--r--src-migrate/modules/side-banner/index.tsx10
24 files changed, 1127 insertions, 70 deletions
diff --git a/src-migrate/modules/cart/components/Item.tsx b/src-migrate/modules/cart/components/Item.tsx
index 6ded6373..47893498 100644
--- a/src-migrate/modules/cart/components/Item.tsx
+++ b/src-migrate/modules/cart/components/Item.tsx
@@ -17,12 +17,14 @@ import CartItemSelect from './ItemSelect'
type Props = {
item: CartItemProps
editable?: boolean
+ pilihSemuaCart?: boolean
}
-const CartItem = ({ item, editable = true }: Props) => {
+const CartItem = ({ item, editable = true,}: Props) => {
+
return (
<div className={style.wrapper}>
- {item.cart_type === 'promotion' && (
+ {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}>
@@ -43,7 +45,9 @@ const CartItem = ({ item, editable = true }: Props) => {
)}
<div className={style.mainProdWrapper}>
- {editable && <CartItemSelect item={item} />}
+ {editable && (
+ <CartItemSelect item={item} />
+ )}
<div className='w-4' />
<CartItem.Image item={item} />
@@ -87,7 +91,6 @@ const CartItem = ({ item, editable = true }: Props) => {
{!editable && <div className={style.quantity}>{item.quantity}</div>}
</div>
</div>
-
</div>
<div className="flex flex-col">
@@ -100,13 +103,14 @@ const CartItem = ({ item, editable = true }: Props) => {
CartItem.Image = function CartItemImage({ item }: { item: CartItemProps }) {
const image = item?.image || item?.parent?.image
+ const imageProgram = item?.image_program ? item.image_program[0] : item?.parent?.image;
- return (
+ return (
<>
{item.cart_type === 'promotion' && (
<div className={style.image}>
- {image && <Image src={image} alt={item.name} width={128} height={128} />}
- {!image && <div className={style.noImage}>No Image</div>}
+ {imageProgram && <Image src={imageProgram} alt={item.name} width={128} height={128} />}
+ {!imageProgram && <div className={style.noImage}>No Image</div>}
</div>
)}
@@ -153,4 +157,4 @@ CartItem.Skeleton = function CartItemSkeleton(props: SkeletonProps & { count: nu
))
}
-export default CartItem \ No newline at end of file
+export default CartItem
diff --git a/src-migrate/modules/cart/components/ItemSelect.tsx b/src-migrate/modules/cart/components/ItemSelect.tsx
index b904a1de..2faf5172 100644
--- a/src-migrate/modules/cart/components/ItemSelect.tsx
+++ b/src-migrate/modules/cart/components/ItemSelect.tsx
@@ -13,23 +13,23 @@ type Props = {
const CartItemSelect = ({ item }: Props) => {
const auth = getAuth()
- const { loadCart } = useCartStore()
+ const { updateCartItem, cart } = useCartStore()
const [isLoad, setIsLoad] = useState<boolean>(false)
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
- if (typeof auth !== 'object') return
-
- setIsLoad(true)
- await upsertUserCart({
- userId: auth.id,
- type: item.cart_type,
- id: item.id,
- qty: item.quantity,
- selected: e.target.checked
- })
- await loadCart(auth.id)
- setIsLoad(false)
+ if (typeof auth !== 'object' || !cart) return
+
+ setIsLoad(true);
+ const updatedCartItems = cart.products.map(cartItem =>
+ cartItem.id === item.id
+ ? { ...cartItem, selected: e.target.checked }
+ : cartItem
+ );
+ // Update the entire cart
+ const updatedCart = { ...cart, products: updatedCartItems };
+ updateCartItem(updatedCart);
+ setIsLoad(false);
}
return (
diff --git a/src-migrate/modules/cart/stores/useCartStore.ts b/src-migrate/modules/cart/stores/useCartStore.ts
index 3d9a0aed..ae551846 100644
--- a/src-migrate/modules/cart/stores/useCartStore.ts
+++ b/src-migrate/modules/cart/stores/useCartStore.ts
@@ -1,5 +1,5 @@
import { create } from 'zustand';
-import { CartProps } from '~/types/cart';
+import { CartItem, CartProps } from '~/types/cart';
import { getUserCart } from '~/services/cart';
type State = {
@@ -16,6 +16,7 @@ type State = {
type Action = {
loadCart: (userId: number) => Promise<void>;
+ updateCartItem: (updateCart: CartProps) => void;
};
export const useCartStore = create<State & Action>((set, get) => ({
@@ -39,6 +40,14 @@ export const useCartStore = create<State & Action>((set, get) => ({
const summary = computeSummary(cart);
set({ summary });
},
+ updateCartItem: (updatedCart) => {
+ const cart = get().cart;
+ if (!cart) return;
+
+ set({ cart: updatedCart });
+ const summary = computeSummary(updatedCart);
+ set({ summary });
+ },
}));
const computeSummary = (cart: CartProps) => {
diff --git a/src-migrate/modules/footer-banner/index.tsx b/src-migrate/modules/footer-banner/index.tsx
index b214493d..86321815 100644
--- a/src-migrate/modules/footer-banner/index.tsx
+++ b/src-migrate/modules/footer-banner/index.tsx
@@ -10,7 +10,7 @@ const FooterBanner = () => {
queryKey: 'footerBanner',
queryFn: () => getBanner({ type: 'bottom-search-promotion' })
})
- // ubah dari static menjadid dynamic dengan menggunakan random index
+
const length = useMemo(() => fetchFooterBanner.data?.length, [fetchFooterBanner.data]);
const randomIndex = useMemo(() => getRandomInt(length), [length]);
const banner = fetchFooterBanner?.data?.[randomIndex] || false;
diff --git a/src-migrate/modules/product-detail/components/AddToCart.tsx b/src-migrate/modules/product-detail/components/AddToCart.tsx
index 097db98a..a5284637 100644
--- a/src-migrate/modules/product-detail/components/AddToCart.tsx
+++ b/src-migrate/modules/product-detail/components/AddToCart.tsx
@@ -1,19 +1,35 @@
-import { Button, useToast } from '@chakra-ui/react'
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
+import style from '../styles/price-action.module.css';
+import { Button, Link, useToast } from '@chakra-ui/react'
+import product from 'next-seo/lib/jsonld/product'
import { useRouter } from 'next/router'
-
+import { useEffect, useState } from 'react'
+import Image from '~/components/ui/image'
import { getAuth } from '~/libs/auth'
import { upsertUserCart } from '~/services/cart'
+import LazyLoad from 'react-lazy-load';
+import ProductSimilar from '../../../../src/lib/product/components/ProductSimilar';
+import { IProductDetail } from '~/types/product';
+import ImageNext from 'next/image';
+import { useProductCartContext } from '@/contexts/ProductCartContext'
+import { createSlug } from '~/libs/slug'
+import formatCurrency from '~/libs/formatCurrency'
+import { useProductDetail } from '../stores/useProductDetail';
type Props = {
variantId: number | null,
quantity?: number;
source?: 'buy' | 'add_to_cart';
+ products : IProductDetail
}
+type Status = 'idle' | 'loading' | 'success'
+
const AddToCart = ({
variantId,
quantity = 1,
- source = 'add_to_cart'
+ source = 'add_to_cart',
+ products
}: Props) => {
const auth = getAuth()
const router = useRouter()
@@ -22,40 +38,65 @@ const AddToCart = ({
isClosable: true
})
- const handleClick = async () => {
+ const {
+ askAdminUrl,
+ } = useProductDetail();
+
+ const [product, setProducts] = useState(products);
+ const [status, setStatus] = useState<Status>('idle')
+ const { productCart, setRefreshCart, setProductCart, refreshCart, isLoading, setIsloading } =
+ useProductCartContext()
+
+ const productSimilarQuery = [
+ product?.name,
+ `fq=-product_id_i:${product.id}`,
+ `fq=-manufacture_id_i:${product.manufacture?.id || 0}`,
+ ].join('&');
+ const [addCartAlert, setAddCartAlert] = useState(false);
+
+ const handleButton = async () => {
if (typeof auth !== 'object') {
const currentUrl = encodeURIComponent(router.asPath)
router.push(`/login?next=${currentUrl}`)
return;
}
-
+
if (
!variantId ||
isNaN(quantity) ||
typeof auth !== 'object'
) return;
-
- toast.promise(
- upsertUserCart({
- userId: auth.id,
+ if (status === 'success') return
+ setStatus('loading')
+ await upsertUserCart({
+ userId: auth.id,
type: 'product',
id: variantId,
qty: quantity,
selected: true,
source: source,
qtyAppend: true
- }),
- {
- loading: { title: 'Menambahkan ke keranjang', description: 'Mohon tunggu...' },
- success: { title: 'Menambahkan ke keranjang', description: 'Berhasil menambahkan ke keranjang belanja' },
- error: { title: 'Menambahkan ke keranjang', description: 'Gagal menambahkan ke keranjang belanja' },
- }
- )
-
+ })
+ setStatus('idle')
+ setRefreshCart(true);
+ setAddCartAlert(true);
+
+ toast({
+ title: 'Tambah ke keranjang',
+ description: 'Berhasil menambahkan barang ke keranjang belanja',
+ status: 'success',
+ duration: 3000,
+ isClosable: true,
+ position: 'top',
+ })
+
if (source === 'buy') {
router.push('/shop/checkout?source=buy')
}
}
+ useEffect(() => {
+ if (status === 'success') setTimeout(() => { setStatus('idle') }, 3000)
+ }, [status])
const btnConfig = {
'add_to_cart': {
@@ -69,10 +110,98 @@ const AddToCart = ({
}
return (
- <Button onClick={handleClick} colorScheme={btnConfig[source].colorScheme} className='w-full'>
- {btnConfig[source].text}
- </Button>
+ <div className='w-full'>
+ <Button onClick={handleButton} colorScheme={btnConfig[source].colorScheme} className='w-full'>
+ {btnConfig[source].text}
+ </Button>
+ <BottomPopup
+ className='!container'
+ title='Berhasil Ditambahkan'
+ active={addCartAlert}
+ close={() => {
+ setAddCartAlert(false);
+ }}
+ >
+ <div className='flex mt-4'>
+ <div className='w-[10%]'>
+ <ImageNext
+ src={product.image}
+ alt={product.name}
+ className='h-32 object-contain object-center w-full border border-gray_r-4'
+ width={80}
+ height={80}
+ />
+ </div>
+ <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'>
+ {!!product.manufacture.name ? (
+ <Link
+ href={createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString())}
+ className=' hover:underline'
+ color={"red"}
+ >
+ {product.manufacture.name}
+ </Link>
+ ) : '-'}
+ <p className='text-ellipsis overflow-hidden'>
+ {product.name}
+ </p>
+ <p>
+ {product.code}
+ </p>
+ {!!product.lowest_price && product.lowest_price.price > 0 && (
+ <>
+ <div className='flex items-end gap-x-2'>
+ {product.lowest_price.discount_percentage > 0 && (
+ <>
+ <div className='badge-solid-red'>
+ {Math.floor(product.lowest_price.discount_percentage)}%
+ </div>
+ <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'>
+ Rp {formatCurrency(product.lowest_price.price || 0)}
+ </div>
+ </>
+ )}
+ <div className='text-danger-500 font-semibold'>
+ Rp {formatCurrency(product.lowest_price.price_discount || 0)}
+ </div>
+ </div>
+ </>
+ )}
+
+ {!!product.lowest_price && product.lowest_price.price === 0 && (
+ <span>
+ Hubungi kami untuk dapatkan harga terbaik,{' '}
+ <Link
+ href={askAdminUrl}
+ target='_blank'
+ className='font-medium underline'
+ color={'red'}
+ >
+ klik disini
+ </Link>
+ </span>
+ )}
+ </div>
+ <div className='ml-3 flex items-center font-normal'>
+ <Link
+ href='/shop/cart'
+ className='flex-1 py-2 text-gray_r-12 btn-yellow'
+ >
+ Lihat Keranjang
+ </Link>
+ </div>
+ </div>
+ <div className='mt-8 mb-4'>
+ <div className='text-h-sm font-semibold mb-6'>
+ Kamu Mungkin Juga Suka
+ </div>
+ <LazyLoad>
+ <ProductSimilar query={productSimilarQuery} />
+ </LazyLoad>
+ </div>
+ </BottomPopup>
+ </div>
)
}
-export default AddToCart \ No newline at end of file
+export default AddToCart \ No newline at end of file
diff --git a/src-migrate/modules/product-detail/components/PriceAction.tsx b/src-migrate/modules/product-detail/components/PriceAction.tsx
index 81271f6e..9021264e 100644
--- a/src-migrate/modules/product-detail/components/PriceAction.tsx
+++ b/src-migrate/modules/product-detail/components/PriceAction.tsx
@@ -97,12 +97,14 @@ const PriceAction = ({ product }: Props) => {
className={style['quantity-input']}
/>
<AddToCart
+ products={product}
variantId={activeVariantId}
quantity={Number(quantityInput)}
/>
{!isApproval && (
<AddToCart
source='buy'
+ products={product}
variantId={activeVariantId}
quantity={Number(quantityInput)}
/>
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx
index de0522d5..169e1903 100644
--- a/src-migrate/modules/product-detail/components/ProductDetail.tsx
+++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx
@@ -129,7 +129,7 @@ const ProductDetail = ({ product }: Props) => {
)}
<div className='h-4 md:h-10' />
- {!!activeVariantId && !isApproval && <ProductPromoSection productId={activeVariantId} />}
+ {!!activeVariantId && !isApproval && <ProductPromoSection product={product} productId={activeVariantId} />}
<div className={style['section-card']}>
<h2 className={style['heading']}>
diff --git a/src-migrate/modules/product-promo/components/AddToCart.tsx b/src-migrate/modules/product-promo/components/AddToCart.tsx
index 87017c14..3d0955e3 100644
--- a/src-migrate/modules/product-promo/components/AddToCart.tsx
+++ b/src-migrate/modules/product-promo/components/AddToCart.tsx
@@ -9,19 +9,39 @@ import { IPromotion } from '~/types/promotion'
import DesktopView from '../../../../src/core/components/views/DesktopView';
import MobileView from '../../../../src/core/components/views/MobileView';
-
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
+import ImageNext from 'next/image';
+import Link from 'next/link'
+import LazyLoad from 'react-lazy-load'
+import ProductSimilar from '../../../../src/lib/product/components/ProductSimilar';
+import { IProductDetail } from '~/types/product';
+import { useProductCartContext } from '@/contexts/ProductCartContext'
+import { createSlug } from '~/libs/slug'
+import formatCurrency from '~/libs/formatCurrency'
+import { useProductDetail } from '../../product-detail/stores/useProductDetail';
type Props = {
promotion: IPromotion
+ product: IProductDetail
}
type Status = 'idle' | 'loading' | 'success'
-const ProductPromoAddToCart = ({ promotion }: Props) => {
+const ProductPromoAddToCart = ({product, promotion }: Props) => {
+
const auth = getAuth()
const toast = useToast()
const router = useRouter()
+ const {askAdminUrl} = useProductDetail();
const [status, setStatus] = useState<Status>('idle')
+ const { productCart, setRefreshCart, setProductCart, refreshCart, isLoading, setIsloading } =
+ useProductCartContext()
+
+ const productSimilarQuery = [
+ promotion?.name,
+ `fq=-product_id_i:${promotion.products[0].product_id}`,
+ ].join('&');
+ const [addCartAlert, setAddCartAlert] = useState(false);
const handleButton = async () => {
if (typeof auth !== 'object') {
@@ -42,7 +62,8 @@ const ProductPromoAddToCart = ({ promotion }: Props) => {
qtyAppend: true
})
setStatus('idle')
-
+ setRefreshCart(true);
+ setAddCartAlert(true);
toast({
title: 'Tambah ke keranjang',
description: 'Berhasil menambahkan barang ke keranjang belanja',
@@ -92,6 +113,92 @@ const ProductPromoAddToCart = ({ promotion }: Props) => {
{status === 'success' && <span>Berhasil</span>}
{status !== 'success' && <span>Keranjang</span>}
</Button>
+ <BottomPopup
+ className='!container'
+ title='Berhasil Ditambahkan'
+ active={addCartAlert}
+ close={() => {
+ setAddCartAlert(false);
+ }}
+ >
+ <div className='flex mt-4'>
+ <div className='w-[10%]'>
+ <ImageNext
+ src={product.image}
+ alt={product.name}
+ className='h-32 object-contain object-center w-full border border-gray_r-4'
+ width={80}
+ height={80}
+ />
+ </div>
+ <div className='ml-3 flex flex-1 items-start font-medium justify-center flex-col gap-y-1'>
+ {!!product?.manufacture?.name ? (
+ <Link
+ href={createSlug('/shop/brands/', product.manufacture.name, product.manufacture.id.toString())}
+ className=' hover:underline'
+ color={"red"}
+ >
+ {product.manufacture.name}
+ </Link>
+ ) : '-'}
+ <p className='text-ellipsis overflow-hidden'>
+ {product.name}
+ </p>
+ <p>
+ {product.code}
+ </p>
+ {!!product.lowest_price && product.lowest_price.price > 0 && (
+ <>
+ <div className='flex items-end gap-x-2'>
+ {product.lowest_price.discount_percentage > 0 && (
+ <>
+ <div className='badge-solid-red'>
+ {Math.floor(product.lowest_price.discount_percentage)}%
+ </div>
+ <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'>
+ Rp {formatCurrency(product.lowest_price.price || 0)}
+ </div>
+ </>
+ )}
+ <div className='text-danger-500 font-semibold'>
+ Rp {formatCurrency(product.lowest_price.price_discount || 0)}
+ </div>
+ </div>
+ </>
+ )}
+
+ {!!product.lowest_price && product.lowest_price.price === 0 && (
+ <span>
+ Hubungi kami untuk dapatkan harga terbaik,{' '}
+ <Link
+ href={askAdminUrl}
+ target='_blank'
+ className='font-medium underline'
+ color={'red'}
+ >
+ klik disini
+ </Link>
+ </span>
+ )}
+ </div>
+ <div className='ml-3 flex items-center font-normal'>
+ <Link
+ href='/shop/cart'
+ className='flex-1 py-2 text-gray_r-12 btn-yellow'
+ >
+ Lihat Keranjang
+ </Link>
+ </div>
+ </div>
+ <div className='mt-8 mb-4'>
+ <div className='text-h-sm font-semibold mb-6'>
+ Kamu Mungkin Juga Suka
+ </div>
+ <LazyLoad>
+ <ProductSimilar query={productSimilarQuery} />
+ </LazyLoad>
+ </div>
+ </BottomPopup>
</DesktopView>
</div>
)
diff --git a/src-migrate/modules/product-promo/components/Card.tsx b/src-migrate/modules/product-promo/components/Card.tsx
index 56e29e38..5c323276 100644
--- a/src-migrate/modules/product-promo/components/Card.tsx
+++ b/src-migrate/modules/product-promo/components/Card.tsx
@@ -15,16 +15,16 @@ import clsxm from '~/libs/clsxm'
import ProductPromoItem from './Item'
import ProductPromoAddToCart from "./AddToCart"
import ProductPromoCardCountdown from "./CardCountdown"
-
+import { IProductDetail } from '~/types/product';
import MobileView from '../../../../src/core/components/views/MobileView';
import DesktopView from '../../../../src/core/components/views/DesktopView';
type Props = {
promotion: IPromotion
-
+ product: IProductDetail
}
-const ProductPromoCard = ({ promotion}: Props) => {
+const ProductPromoCard = ({product, promotion}: Props) => {
const [products, setProducts] = useState<IProductVariantPromo[]>([])
const [freeProducts, setFreeProducts] = useState<IProductVariantPromo[]>([])
const [error, setError] = useState<string | null>(null)
@@ -90,10 +90,10 @@ const ProductPromoCard = ({ promotion}: Props) => {
<div className={style.title}>{promotion.name}</div>
<Tooltip label={PROMO_CATEGORY[promotion.type.value].description} placement="top" bgColor='red.600' p={1} rounded={6}>
- {/* <div className={style.badgeType} > */}
- {/* Paket {PROMO_CATEGORY[promotion.type.value].alias} */}
+ <div className={style.badgeType} >
+ Paket {PROMO_CATEGORY[promotion.type.value].alias}
<InfoIcon className={style.badgeType} size={25} />
- {/* </div> */}
+ </div>
</Tooltip>
</div>
@@ -132,7 +132,7 @@ const ProductPromoCard = ({ promotion}: Props) => {
</div>
<div>
- <ProductPromoAddToCart promotion={promotion} />
+ <ProductPromoAddToCart product={product} promotion={promotion} />
</div>
</div>
@@ -189,7 +189,7 @@ const ProductPromoCard = ({ promotion}: Props) => {
</div>
</div>
<div>
- <ProductPromoAddToCart promotion={promotion} />
+ <ProductPromoAddToCart product={product} promotion={promotion} />
</div>
</div>
diff --git a/src-migrate/modules/product-promo/components/Modal.tsx b/src-migrate/modules/product-promo/components/Modal.tsx
index 0de672c2..1722b9df 100644
--- a/src-migrate/modules/product-promo/components/Modal.tsx
+++ b/src-migrate/modules/product-promo/components/Modal.tsx
@@ -3,8 +3,12 @@ import { Modal } from "~/components/ui/modal"
import { useModalStore } from '../stores/useModalStore'
import ProductPromoCategoryTab from './CategoryTab'
import ProductPromoModalContent from './ModalContent'
+import { IProductDetail } from '~/types/product';
-const ProductPromoModal = () => {
+type Props = {
+ product: IProductDetail
+}
+const ProductPromoModal = ({product}:Props) => {
const { active, closeModal } = useModalStore()
return (
@@ -17,7 +21,7 @@ const ProductPromoModal = () => {
<div className='h-4' />
- <ProductPromoModalContent />
+ <ProductPromoModalContent product={product} />
</Modal>
)
}
diff --git a/src-migrate/modules/product-promo/components/ModalContent.tsx b/src-migrate/modules/product-promo/components/ModalContent.tsx
index ab5129f8..256ef61a 100644
--- a/src-migrate/modules/product-promo/components/ModalContent.tsx
+++ b/src-migrate/modules/product-promo/components/ModalContent.tsx
@@ -6,7 +6,11 @@ import { getVariantPromoByCategory } from "~/services/productVariant"
import { useModalStore } from "../stores/useModalStore"
import ProductPromoCard from "./Card"
-const ProductPromoModalContent = () => {
+import { IProductDetail } from '~/types/product';
+type Props = {
+ product: IProductDetail
+}
+const ProductPromoModalContent = ({product}:Props) => {
const { activeTab, variantId } = useModalStore()
const promotionsQuery = useQuery(
@@ -24,7 +28,7 @@ const ProductPromoModalContent = () => {
<Skeleton isLoaded={!promotionsQuery.isLoading} className='min-h-[70vh] max-h-[70vh]'>
<div className="grid grid-cols-1 gap-y-6 pb-6">
{promotions?.data.map((promo) => (
- <ProductPromoCard key={promo.id} promotion={promo} />
+ <ProductPromoCard key={promo.id} promotion={promo} product={product} />
))}
{promotions?.data.length === 0 && (
<div className="py-10 rounded-lg h-fit text-center text-body-1 font-semibold text-gray-800 bg-gray-200">Belum ada promo pada kategori ini</div>
diff --git a/src-migrate/modules/product-promo/components/Section.tsx b/src-migrate/modules/product-promo/components/Section.tsx
index 4e8a7dd5..e1719998 100644
--- a/src-migrate/modules/product-promo/components/Section.tsx
+++ b/src-migrate/modules/product-promo/components/Section.tsx
@@ -9,12 +9,14 @@ import { IPromotion } from '~/types/promotion'
import { useModalStore } from "../stores/useModalStore"
import ProductPromoCard from './Card'
import ProductPromoModal from "./Modal"
+import { IProductDetail } from '~/types/product';
type Props = {
productId: number;
+ product: IProductDetail;
}
-const ProductPromoSection = ({ productId }: Props) => {
+const ProductPromoSection = ({ product, productId }: Props) => {
const promotionsQuery = useQuery({
queryKey: [`promotions.highlight`, productId],
queryFn: async () => await fetch(`/api/product-variant/${productId}/promotion/highlight`).then((res) => res.json()) as { data: IPromotion[] }
@@ -23,14 +25,13 @@ const ProductPromoSection = ({ productId }: Props) => {
const promotions = promotionsQuery.data
const { openModal } = useModalStore()
-
return (
<SmoothRender
isLoaded={(promotions?.data && promotions?.data.length > 0) || false}
height='450px'
duration='700ms'
>
- <ProductPromoModal />
+ <ProductPromoModal product={product}/>
{promotions?.data && promotions?.data.length > 0 && (
<div className={style.titleWrapper}>
@@ -50,7 +51,7 @@ const ProductPromoSection = ({ productId }: Props) => {
>
{promotions?.data.map((promotion) => (
<div key={promotion.id} className="min-w-[400px] max-w-[400px]">
- <ProductPromoCard promotion={promotion} />
+ <ProductPromoCard product={product} promotion={promotion} />
</div>
))}
</Skeleton>
diff --git a/src-migrate/modules/product-promo/styles/card.module.css b/src-migrate/modules/product-promo/styles/card.module.css
index faa3b370..4e294f1c 100644
--- a/src-migrate/modules/product-promo/styles/card.module.css
+++ b/src-migrate/modules/product-promo/styles/card.module.css
@@ -10,7 +10,7 @@
}
.badgeType {
- @apply p-2 flex gap-x-1.5 rounded-md border border-danger-500 text-danger-500;
+ @apply p-2 flex gap-x-1.5 rounded-md border border-danger-500 text-danger-500 items-center;
}
.productSection {
diff --git a/src-migrate/modules/promo/components/FlashSale.tsx b/src-migrate/modules/promo/components/FlashSale.tsx
new file mode 100644
index 00000000..c0259396
--- /dev/null
+++ b/src-migrate/modules/promo/components/FlashSale.tsx
@@ -0,0 +1,20 @@
+import dynamic from "next/dynamic";
+import React from "react";
+import { FlashSaleSkeleton } from "@/lib/flashSale/skeleton/FlashSaleSkeleton";
+
+const FlashSale = dynamic(
+ () => import('@/lib/flashSale/components/FlashSale'),
+ {
+ loading: () => <FlashSaleSkeleton />,
+ }
+ );
+
+ const FlashSalePromo = ()=> {
+ return(
+ <>
+ <FlashSale/>
+ </>
+ )
+ }
+
+ export default FlashSalePromo \ No newline at end of file
diff --git a/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx b/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx
new file mode 100644
index 00000000..1c5cc86d
--- /dev/null
+++ b/src-migrate/modules/promo/components/FlashSaleNonDisplay.tsx
@@ -0,0 +1,20 @@
+import dynamic from "next/dynamic";
+import React from "react";
+import { FlashSaleSkeleton } from "@/lib/flashSale/skeleton/FlashSaleSkeleton";
+
+const FlashSaleNonDisplay = dynamic(
+ () => import('@/lib/flashSale/components/FlashSaleNonDisplay'),
+ {
+ loading: () => <FlashSaleSkeleton />,
+ }
+ );
+
+ const FlashSalePromo = ()=> {
+ return(
+ <>
+ <FlashSaleNonDisplay/>
+ </>
+ )
+ }
+
+ export default FlashSalePromo \ No newline at end of file
diff --git a/src-migrate/modules/promo/components/Hero.tsx b/src-migrate/modules/promo/components/Hero.tsx
new file mode 100644
index 00000000..c5f0afad
--- /dev/null
+++ b/src-migrate/modules/promo/components/Hero.tsx
@@ -0,0 +1,105 @@
+import 'swiper/css';
+
+import Image from 'next/image';
+import { useEffect, useMemo } from 'react';
+import { useQuery } from 'react-query';
+import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react';
+import style from '../styles/hero.module.css';
+import 'swiper/css/navigation';
+import 'swiper/css/pagination';
+import { Navigation, Pagination, Autoplay } from 'swiper';
+import MobileView from '../../../../src/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+import {bannerApi} from '../../../../src/api/bannerApi'
+
+interface IPromotionProgram {
+ headlineBanner: string;
+ descriptionBanner: string;
+ image: string ;
+ name: string;
+}
+
+const swiperBanner: SwiperProps = {
+ modules:[Navigation, Pagination, Autoplay],
+ autoplay: {
+ delay: 6000,
+ disableOnInteraction: false
+ },
+ loop: true,
+ className: 'h-[400px] w-full',
+ slidesPerView: 1,
+ spaceBetween: 10,
+ pagination:true,
+}
+const swiperBannerMob = {
+ autoplay: {
+ delay: 6000,
+ disableOnInteraction: false,
+ },
+ modules: [Pagination, Autoplay],
+ loop: true,
+ className: 'border border-gray_r-6 min-h-full',
+ slidesPerView: 1,
+};
+
+const Hero = () => {
+ const heroBanner = useQuery('allPromo', bannerApi({ type: 'banner-semua-promo' }));
+
+ const banners: IPromotionProgram[] = useMemo(
+ () => heroBanner?.data || [],
+ [heroBanner.data]
+ );
+
+ const swiperBannerMobile = {
+ ...swiperBannerMob,
+ pagination: { dynamicBullets: false, clickable: true },
+ };
+
+ return (
+ <>
+ <DesktopView>
+ <div className={style['wrapper']}>
+ <Swiper {...swiperBanner}>
+ {banners.map((banner, index) => (
+ <SwiperSlide key={index} className='flex flex-row'>
+ <div className={style['desc-section']}>
+ <div className={style['title']}>{banner.headlineBanner? banner.headlineBanner : "Pasti Hemat & Untung Selama Belanja di Indoteknik.com!"}</div>
+ <div className='h-4' />
+ <div className={style['subtitle']}>{banner.descriptionBanner? banner.descriptionBanner : "Cari paket yang kami sediakan dengan penawaran harga & Nikmati kemudahan dalam setiap transaksi dengan fitur lengkap Pembayaran hingga barang sampai!"}</div>
+ </div>
+ <div className={style['banner-section']}>
+ <Image
+ src={banner.image}
+ alt={banner.name}
+ width={666}
+ height={450}
+ quality={90}
+ className='w-full h-full object-fit object-center rounded-2xl' />
+ </div>
+ </SwiperSlide>
+ ))}
+ </Swiper>
+ </div>
+ </DesktopView>
+ <MobileView>
+ <Swiper {...swiperBannerMobile}>
+ {banners?.map((banner, index) => (
+ <SwiperSlide key={index}>
+ <Image
+ width={439}
+ height={150}
+ quality={100}
+ src={banner.image}
+ alt={banner.name}
+ className='w-full h-full object-cover object-center rounded-2xl'
+ />
+ </SwiperSlide>
+ ))}
+ </Swiper>
+
+ </MobileView>
+ </>
+ )
+}
+
+export default Hero \ No newline at end of file
diff --git a/src-migrate/modules/promo/components/HeroDiskon.tsx b/src-migrate/modules/promo/components/HeroDiskon.tsx
new file mode 100644
index 00000000..6d38c763
--- /dev/null
+++ b/src-migrate/modules/promo/components/HeroDiskon.tsx
@@ -0,0 +1,137 @@
+import 'swiper/css';
+
+import Image from 'next/image';
+import { useEffect, useMemo } from 'react';
+import { useQuery } from 'react-query';
+import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react';
+
+import { getBanner } from '~/services/banner';
+import style from '../styles/hero.module.css';
+import 'swiper/css/navigation';
+import { Autoplay, Navigation, Pagination } from 'swiper';
+
+const swiperBanner: SwiperProps = {
+ modules:[Navigation, Pagination, Autoplay],
+ autoplay: {
+ delay: 6000,
+ disableOnInteraction: false
+ },
+ loop: true,
+ className: 'h-[400px] w-full',
+ slidesPerView: 1,
+ spaceBetween: 10,
+ navigation:true,
+}
+const swiperBanner2: SwiperProps = {
+ modules: [Pagination, Autoplay],
+ autoplay: {
+ delay: 5000,
+ },
+ loop: true,
+ className: 'h-[400px] w-full',
+ slidesPerView: 1,
+ spaceBetween: 10,
+}
+
+const Hero = () => {
+ const bannerQuery = useQuery({
+ queryKey: ['banner.all-promo'],
+ queryFn: () => getBanner({ type: 'banner-promotion' })
+ })
+
+ const banners = useMemo(() => bannerQuery.data || [], [bannerQuery.data]);
+
+ useEffect(() => {
+ if (banners.length > 1) {
+ swiperBanner.slidesPerView = 1;
+ swiperBanner.loop = true;
+ }
+ }, [banners]);
+
+ return (
+
+ <div className="grid grid-cols-3 gap-4">
+ <div className="row-span-2 h-[446px] flex items-center ">
+ <Swiper {...swiperBanner}>
+ {banners.map((banner, index) => (
+ <SwiperSlide key={index}>
+ <Image
+ src={banner.image}
+ alt={banner.name}
+ width={666}
+ height={480}
+ className='w-[446px] h-[446px] object-fill object-center rounded-2xl'
+ />
+ </SwiperSlide>
+ ))}
+ </Swiper>
+ </div>
+ <div className="w-[400px] h-[217px] ">
+ <Swiper {...swiperBanner2}>
+ {banners.map((banner, index) => (
+ <SwiperSlide key={index}>
+ <Image
+ src={banner.image}
+ alt={banner.name}
+ width={666}
+ height={450}
+ className='w-[400px] h-[217px] object-cover object-center rounded-2xl '
+ />
+ </SwiperSlide>
+ ))}
+ </Swiper>
+ </div>
+ <div className="w-[400px] h-[217px]">
+ <Swiper {...swiperBanner2}>
+ {banners.map((banner, index) => (
+ <SwiperSlide key={index}>
+ <Image
+ src={banner.image}
+ alt={banner.name}
+ width={666}
+ height={450}
+ className='w-[400px] h-[217px] object-cover object-center rounded-2xl'
+ />
+ </SwiperSlide>
+ ))}
+ </Swiper>
+ </div>
+ <div className="w-[400px] h-[217px]">
+ <Swiper {...swiperBanner2}>
+ {banners.map((banner, index) => (
+ <SwiperSlide key={index}>
+ <Image
+ src={banner.image}
+ alt={banner.name}
+ width={666}
+ height={450}
+ className='w-[400px] h-[217px] object-cover object-center rounded-2xl'
+ />
+ </SwiperSlide>
+ ))}
+ </Swiper>
+ </div>
+ <div className="w-[400px] h-[217px]">
+ <Swiper {...swiperBanner2}>
+ {banners.map((banner, index) => (
+ <SwiperSlide key={index}>
+ <Image
+ src={banner.image}
+ alt={banner.name}
+ width={666}
+ height={450}
+ className='w-[400px] h-[217px] object-cover object-center rounded-2xl'
+ />
+ </SwiperSlide>
+ ))}
+ </Swiper>
+ </div>
+
+ </div>
+
+
+
+ )
+}
+
+export default Hero \ No newline at end of file
diff --git a/src-migrate/modules/promo/components/PromoList.tsx b/src-migrate/modules/promo/components/PromoList.tsx
new file mode 100644
index 00000000..4d0db3c2
--- /dev/null
+++ b/src-migrate/modules/promo/components/PromoList.tsx
@@ -0,0 +1,135 @@
+import React, { useEffect, useState } from 'react';
+import { Button, Skeleton } from '@chakra-ui/react'
+import clsxm from "~/libs/clsxm"
+import ProductPromoCard from '../../product-promo/components/Card';
+import { fetchPromoItemsSolr } from '../../../../src/api/promoApi';
+import { Swiper, SwiperSlide } from 'swiper/react';
+import SwiperCore, { Navigation, Pagination } from 'swiper';
+import useDevice from '@/core/hooks/useDevice';
+import LogoSpinner from '../../../../src/core/components/elements/Spinner/LogoSpinner';
+import usePromoStore from './promoStore';
+import Link from "next/link"
+import { IPromotion } from '~/types/promotion';
+interface PromoListProps {
+ selectedPromo: string; // Tipe selectedPromo ditetapkan sebagai string
+}
+
+const PromoList: React.FC<PromoListProps> = ({ selectedPromo }) => {
+ const {
+ title,
+ slug,
+ promoItems,
+ promoData,
+ isLoading,
+ setTitle,
+ setSlug,
+ setPromoItems,
+ setPromoData,
+ setIsLoading,
+ } = usePromoStore();
+
+ const { isMobile, isDesktop } = useDevice();
+
+ const swiperBanner = {
+ modules: [Navigation],
+ className: 'h-[400px] w-full',
+ slidesPerView: isMobile ? 1.1 : 3.25,
+ spaceBetween: 10,
+ navigation:isMobile? true : false,
+ allowTouchMove:isMobile? false : true,
+ };
+
+ useEffect(() => {
+ if (selectedPromo === 'Bundling') {
+ setTitle('Kombinasi Kilat Pilihan Kami!');
+ setSlug('bundling');
+ } else if (selectedPromo === 'Loading') {
+ setTitle('Belanja Borong Pilihan Kami!');
+ setSlug('discount_loading');
+ } else if (selectedPromo === 'Merchandise') {
+ setTitle('Gratis Merchandise Spesial Indoteknik');
+ setSlug('merchandise');
+ }
+ }, [selectedPromo, setTitle, setSlug]);
+
+ useEffect(() => {
+ const fetchPromotions = async () => {
+ setIsLoading(true);
+ try {
+ const items = await fetchPromoItemsSolr(`type_value_s:${slug}`, 0, 10);
+ setPromoItems(items);
+
+ const promoDataPromises = items.map(async (item) => {
+ try {
+ const response = await fetchPromoItemsSolr(`id:${item.id}`, 0, 10);
+ return response;
+ } catch (fetchError) {
+ return [];
+ }
+ });
+
+ const promoDataArray = await Promise.all(promoDataPromises);
+ const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
+ setPromoData(mergedPromoData);
+
+ } catch (error) {
+ console.error('Error fetching promo items:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ if (slug) {
+ setIsLoading(true);
+ setPromoItems([]);
+ setPromoData([]);
+ fetchPromotions();
+ }
+ }, [slug, setPromoItems, setPromoData, setIsLoading]);
+
+ return (
+ <div className='min-h-[360px]'>
+ <div className='flex justify-between items-center'>
+ <h1 className='text-h-sm md:text-h-lg font-semibold py-4'>{title}</h1>
+ <div>
+ <Link href={`/shop/promo/${slug}`} className='!text-red-500 font-semibold'>
+ Lihat Semua
+ </Link>
+ </div>
+ </div>
+ {isLoading ? (
+ <div className="loading-spinner flex justify-center">
+ <LogoSpinner width={48} height={48} />
+ </div>
+ ) : (
+ <Skeleton
+ isLoaded={!isLoading}
+ className={clsxm(
+ "flex gap-x-4 overflow-x-auto px-4 md:px-0", {
+ "min-h-[340px]": promoData[0] && promoData?.length > 0
+ })}
+ >
+ {isDesktop && (
+ <Swiper {...swiperBanner}>
+ {promoData?.map((promotion: IPromotion) => (
+ <SwiperSlide key={promotion.id}>
+ <div className="min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-full lg:w-full xl:w-full">
+ <ProductPromoCard product={promoItems} promotion={promotion} />
+ </div>
+ </SwiperSlide>
+ ))}
+ </Swiper>
+ )}
+ {isMobile && (promoData?.map((promotion: IPromotion) => (
+ <div key={promotion.id} className="min-w-[400px] max-w-[400px]">
+ <ProductPromoCard product={promoItems} promotion={promotion} />
+ </div>
+ )))}
+
+ </Skeleton>
+ )}
+ </div>
+ );
+};
+
+export default PromoList; \ No newline at end of file
diff --git a/src-migrate/modules/promo/components/PromotinProgram.jsx b/src-migrate/modules/promo/components/PromotinProgram.jsx
new file mode 100644
index 00000000..33839944
--- /dev/null
+++ b/src-migrate/modules/promo/components/PromotinProgram.jsx
@@ -0,0 +1,134 @@
+import React from 'react';
+import Image from 'next/image';
+import { InfoIcon } from "lucide-react";
+import MobileView from '../../../../src/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react';
+import 'swiper/css';
+import useDevice from '@/core/hooks/useDevice';
+
+const PromotionProgram = ({ selectedPromo, onSelectPromo }) => {
+ const { isMobile } = useDevice();
+ return (
+ <>
+ <div className="text-h-sm md:text-h-lg font-semibold py-4">Serba Serbi Promo</div>
+ <div className='px-4 sm:px-0'>
+ {/* <div className='w-full h-full '>
+ <div
+ onClick={() => onSelectPromo('Diskon')}
+ className={`border p-2 flex items-center gap-x-2 rounded-lg cursor-pointer ${selectedPromo === 'Diskon' ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`}
+ >
+ <div>
+ <Image
+ width={24}
+ height={24}
+ quality={100}
+ src='/images/icon_promo/diskon.svg'
+ alt=''
+ className='h-12 w-12 rounded'
+ />
+ </div>
+ <div>
+ <div className='flex w-full flex-row items-center justify-start'>
+ <h1 className={`mr-1 font-semibold text-base ${selectedPromo === 'Diskon' ? 'text-red-500' : 'text-gray-900'}`}>Spesial Diskon</h1>
+ <InfoIcon className='mt-[1px] text-red-500' size={14} />
+ </div>
+ <p className={`text-xs md:text-sm ${selectedPromo === 'Diskon' ? 'text-red-500' : 'text-gray-500'}`}>
+ Harga lebih murah dan pasti makin hemat belinya..
+ </p>
+ </div>
+ </div>
+ </div> */}
+
+ <Swiper slidesPerView={isMobile ? 1.3 : 3} spaceBetween={10}>
+ <SwiperSlide>
+ <div className='w-full h-full '>
+ <div
+ onClick={() => onSelectPromo('Bundling')}
+ className={`border h-full p-1 flex items-center gap-x-2 rounded-lg cursor-pointer ${selectedPromo === 'Bundling' ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`}
+ >
+ <div>
+ <Image
+ width={24}
+ height={24}
+ quality={100}
+ src='/images/icon_promo/silat.svg'
+ alt=''
+ className='h-12 w-12 rounded'
+ />
+ </div>
+ <div >
+ <div className='flex w-full flex-row items-center justify-start'>
+ <h1 className={`mr-1 font-semibold text-base ${selectedPromo === 'Bundling' ? 'text-red-500' : 'text-gray-900'}`}>Paket Silat</h1>
+ <InfoIcon className='mt-[1px] text-red-500' size={14} />
+ </div>
+ <p className={`text-xs md:text-sm ${selectedPromo === 'Bundling' ? 'text-red-500' : 'text-gray-500'}`}>
+ Pilihan bundling barang kombinasi Silat.
+ </p>
+ </div>
+ </div>
+ </div>
+ </SwiperSlide>
+ <SwiperSlide>
+ <div className='w-full h-full '>
+ <div
+ onClick={() => onSelectPromo('Loading')}
+ className={`border p-2 h-full flex items-center gap-x-2 rounded-lg cursor-pointer ${selectedPromo === 'Loading' ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`}
+ >
+ <div>
+ <Image
+ width={24}
+ height={24}
+ quality={100}
+ src='/images/icon_promo/barong.svg'
+ alt=''
+ className='h-12 w-12 rounded'
+ />
+ </div>
+ <div>
+ <div className='flex w-full flex-row items-center justify-start'>
+ <h1 className={`mr-1 font-semibold text-base ${selectedPromo === 'Loading' ? 'text-red-500' : 'text-gray-900'}`}>Paket Barong</h1>
+ <InfoIcon className='mt-[1px] text-red-500' size={14} />
+ </div>
+ <p className={`text-xs md:text-sm ${selectedPromo === 'Loading' ? 'text-red-500' : 'text-gray-500'}`}>
+ Beli banyak barang/partai barang borong.
+ </p>
+ </div>
+ </div>
+ </div>
+ </SwiperSlide>
+ <SwiperSlide>
+ <div className='w-full h-full '>
+ <div
+ onClick={() => onSelectPromo('Merchandise')}
+ className={`border p-2 h-full flex items-center gap-x-2 rounded-lg cursor-pointer ${selectedPromo === 'Merchandise' ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`}
+ >
+ <div>
+ <Image
+ width={24}
+ height={24}
+ quality={100}
+ src='/images/icon_promo/angklung.svg'
+ alt=''
+ className='h-12 w-12 rounded'
+ />
+ </div>
+ <div >
+ <div className='flex w-full flex-row items-center justify-start '>
+ <h1 className={`mr-1 font-semibold text-base ${selectedPromo === 'Merchandise' ? 'text-red-500' : 'text-gray-900'}`}>Paket Angklung</h1>
+ <InfoIcon className='mt-[1px] text-red-500' size={14} />
+ </div>
+ <p className={` m1 text-xs md:text-sm ${selectedPromo === 'Merchandise' ? 'text-red-500' : 'text-gray-500'}`}>
+ Gratis barang promosi/merchandise menang langsung.
+ </p>
+ </div>
+ </div>
+ </div>
+ </SwiperSlide>
+ </Swiper>
+ </div>
+ </>
+ );
+};
+
+export default PromotionProgram;
diff --git a/src-migrate/modules/promo/components/Voucher.tsx b/src-migrate/modules/promo/components/Voucher.tsx
new file mode 100644
index 00000000..e5877e51
--- /dev/null
+++ b/src-migrate/modules/promo/components/Voucher.tsx
@@ -0,0 +1,160 @@
+import { useMemo, useState, useEffect } from 'react';
+import { useQuery } from 'react-query';
+import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react';
+import { getVoucherAll } from '~/services/voucher';
+import style from '../styles/voucher.module.css';
+import Image from 'next/image';
+import { useToast } from '@chakra-ui/react';
+import useDevice from '@/core/hooks/useDevice';
+import useAuth from '@/core/hooks/useAuth';
+import { getVoucher } from '../../../../src/lib/checkout/api/getVoucher';
+
+interface Auth {
+ id: string;
+}
+interface Voucher {
+ id: string;
+ image: string;
+ name: string;
+ description: string;
+ code: string;
+}
+
+const VoucherComponent = () => {
+ const [listVouchers, setListVouchers] = useState<Voucher[] | null>(null);
+ const [loadingVoucher, setLoadingVoucher] = useState(true);
+ const { isMobile } = useDevice();
+ const auth = useAuth() as unknown as Auth;
+ const toast = useToast();
+
+ useEffect(() => {
+ if (!listVouchers && auth?.id) {
+ (async () => {
+ try {
+ const dataVoucher = await getVoucher(auth.id);
+ setListVouchers(dataVoucher);
+ } finally {
+ setLoadingVoucher(false);
+ }
+ })();
+ }
+ }, [auth?.id, listVouchers]);
+
+ const voucherQuery = useQuery({
+ queryKey: ['voucher.all-voucher'],
+ queryFn: getVoucherAll,
+ });
+
+ const swiperVoucher: SwiperProps = {
+ autoplay: {
+ delay: 6000,
+ disableOnInteraction: false,
+ },
+ loop: false,
+ className: 'h-[160px] w-full',
+ slidesPerView: isMobile ? 1.2 : 3.2,
+ spaceBetween: 2,
+ };
+
+ const dataVouchers = useMemo(() => voucherQuery.data || [], [voucherQuery.data]);
+
+ const vouchers = auth?.id? listVouchers : dataVouchers;
+
+
+ const copyText = (text: string) => {
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(text)
+ .then(() => {
+ toast({
+ title: 'Salin ke papan klip',
+ description: 'Kode voucher berhasil disalin',
+ status: 'success',
+ duration: 3000,
+ isClosable: true,
+ position: 'top',
+ })
+ })
+ .catch(() => {
+ fallbackCopyTextToClipboard(text);
+ });
+ } else {
+ fallbackCopyTextToClipboard(text);
+ }
+ }
+
+ const fallbackCopyTextToClipboard = (text: string) => {
+ const textArea = document.createElement("textarea");
+ textArea.value = text;
+ // Tambahkan style untuk menyembunyikan textArea secara visual
+ textArea.style.position = 'fixed';
+ textArea.style.top = '0';
+ textArea.style.left = '0';
+ textArea.style.width = '2em';
+ textArea.style.height = '2em';
+ textArea.style.padding = '0';
+ textArea.style.border = 'none';
+ textArea.style.outline = 'none';
+ textArea.style.boxShadow = 'none';
+ textArea.style.background = 'transparent';
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+ try {
+ document.execCommand('copy');
+ toast({
+ title: 'Salin ke papan klip',
+ description: 'Kode voucher berhasil disalin',
+ status: 'success',
+ duration: 3000,
+ isClosable: true,
+ position: 'top',
+ })
+ } catch (err) {
+ console.error('Fallback: Oops, unable to copy', err);
+ }
+ document.body.removeChild(textArea);
+ }
+
+ return (
+ <>
+ <div className={style['title']}>Pakai Voucher Belanja</div>
+
+ <div className='h-6' />
+
+ {voucherQuery.isLoading && (
+ <div className='grid grid-cols-3 gap-x-4 animate-pulse'>
+ {Array.from({ length: 3 }).map((_, index) => (
+ <div key={index} className='w-full h-[160px] bg-gray-200 rounded-xl' />
+ ))}
+ </div>
+ )}
+ {!voucherQuery.isLoading && (
+ <div className={style['voucher-section']}>
+ <Swiper {...swiperVoucher}>
+ {vouchers?.map((voucher) => (
+ <SwiperSlide key={voucher.id} className='pb-2'>
+ <div className={style['voucher-card']}>
+ <Image src={voucher.image} alt={voucher.name} width={128} height={128} className={style['voucher-image']} />
+
+ <div className={style['voucher-content']}>
+ <div className={style['voucher-title']}>{voucher.name}</div>
+ <div className={style['voucher-desc']}>{voucher.description}</div>
+ <div className={style['voucher-bottom']}>
+ <div>
+ <div className={style['voucher-code-desc']}>Kode Promo</div>
+ <div className={style['voucher-code']}>{voucher.code}</div>
+ </div>
+ <button className={style['voucher-copy']} onClick={() => copyText(voucher.code)}>Salin</button>
+ </div>
+ </div>
+ </div>
+ </SwiperSlide>
+ ))}
+ </Swiper>
+ </div>
+ )}
+ </>
+ )
+}
+
+export default VoucherComponent
diff --git a/src-migrate/modules/promo/components/promoStore.js b/src-migrate/modules/promo/components/promoStore.js
new file mode 100644
index 00000000..c232de00
--- /dev/null
+++ b/src-migrate/modules/promo/components/promoStore.js
@@ -0,0 +1,16 @@
+import create from 'zustand';
+
+const usePromoStore = create((set) => ({
+ title: '',
+ slug: '',
+ promoItems: [],
+ promoData: [],
+ isLoading: true,
+ setTitle: (title) => set({ title }),
+ setSlug: (slug) => set({ slug }),
+ setPromoItems: (promoItems) => set({ promoItems }),
+ setPromoData: (promoData) => set({ promoData }),
+ setIsLoading: (isLoading) => set({ isLoading }),
+}));
+
+export default usePromoStore;
diff --git a/src-migrate/modules/promo/styles/hero.module.css b/src-migrate/modules/promo/styles/hero.module.css
new file mode 100644
index 00000000..a5ba6ecc
--- /dev/null
+++ b/src-migrate/modules/promo/styles/hero.module.css
@@ -0,0 +1,27 @@
+.wrapper {
+ @apply rounded-xl w-full h-[460px] flex;
+}
+
+.desc-section {
+ @apply w-full md:w-5/12
+ flex flex-col
+ md:justify-center
+ p-6 md:pl-10;
+}
+
+.title {
+ @apply text-title-sm md:text-title-lg
+ leading-[30px] md:leading-[42px]
+ font-semibold;
+}
+
+.subtitle {
+ @apply text-body-2 leading-7 text-gray-700;
+}
+
+.banner-section {
+ @apply md:w-7/12
+ flex flex-col
+ md:justify-center
+ md:pr-10;
+}
diff --git a/src-migrate/modules/promo/styles/voucher.module.css b/src-migrate/modules/promo/styles/voucher.module.css
new file mode 100644
index 00000000..22d07f91
--- /dev/null
+++ b/src-migrate/modules/promo/styles/voucher.module.css
@@ -0,0 +1,43 @@
+.title {
+ @apply text-h-sm md:text-h-lg font-semibold;
+}
+
+.voucher-section {
+ @apply w-full;
+}
+
+.voucher-card {
+ @apply w-full md:w-11/12 h-3/4 rounded-xl border items-center border-gray-200 shadow-md p-4 flex gap-x-4 ;
+}
+
+.voucher-image {
+ @apply bg-gray-100 rounded-lg w-4/12 h-fit object-contain object-center;
+}
+
+.voucher-content {
+ @apply flex-1 flex flex-col;
+}
+
+.voucher-title {
+ @apply font-medium text-body-1 leading-6 mb-1;
+}
+
+.voucher-desc {
+ @apply text-gray-800 line-clamp-2 text-caption-1;
+}
+
+.voucher-bottom {
+ @apply flex justify-between mt-2;
+}
+
+.voucher-code-desc {
+ @apply text-gray-500 text-caption-1;
+}
+
+.voucher-code {
+ @apply text-red-700 font-medium;
+}
+
+.voucher-copy {
+ @apply bg-gray-200 hover:bg-danger-500 text-danger-500 hover:text-white transition-colors rounded-lg flex items-center justify-center px-6;
+}
diff --git a/src-migrate/modules/side-banner/index.tsx b/src-migrate/modules/side-banner/index.tsx
index 6214edfb..878b8e70 100644
--- a/src-migrate/modules/side-banner/index.tsx
+++ b/src-migrate/modules/side-banner/index.tsx
@@ -1,7 +1,7 @@
import React, { useMemo } from "react";
-import Link from "next/link";
-import { useQuery } from "react-query";
-import Image from "~/components/ui/image";
+import Link from "next/link";;
+import { useQuery } from "react-query";;
+import Image from "~/components/ui/image";;
import { getBanner } from "~/services/banner";
import { getRandomInt } from '@/utils/getRandomInt';
@@ -10,7 +10,7 @@ const SideBanner = () => {
queryKey: 'sideBanner',
queryFn: () => getBanner({ type: 'side-banner-search' })
});
- // ubah dari static menjadid dynamic dengan menggunakan random index
+
const length = useMemo(() => fetchSideBanner.data?.length, [fetchSideBanner.data]);
const randomIndex = useMemo(() => getRandomInt(length), [length]);
const banner = fetchSideBanner?.data?.[randomIndex] || false;
@@ -25,6 +25,6 @@ const SideBanner = () => {
<Image src={banner.image} alt={banner.name} width={315} height={450} className='object-cover object-center rounded-lg' />
)}
</>
- );
+ )
}
export default SideBanner;