summaryrefslogtreecommitdiff
path: root/src-migrate/modules/promo
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2024-07-10 16:23:35 +0700
committerit-fixcomart <it@fixcomart.co.id>2024-07-10 16:23:35 +0700
commita8dcd4d3d14d9caf64063f3ec586125238727794 (patch)
tree943ad4a0bcd25153ace381075f95ab0aec4a2edf /src-migrate/modules/promo
parent2e3c726bc8217f3960cfecec44b81303b03de72b (diff)
parentffaf9994e8c47c5a32a2091b7d0949302528ee2e (diff)
Merge branch 'feature/all-promotion' into development
Diffstat (limited to 'src-migrate/modules/promo')
-rw-r--r--src-migrate/modules/promo/components/FlashSale.tsx21
-rw-r--r--src-migrate/modules/promo/components/Hero.tsx108
-rw-r--r--src-migrate/modules/promo/components/HeroDiskon.tsx137
-rw-r--r--src-migrate/modules/promo/components/PromoList.jsx110
-rw-r--r--src-migrate/modules/promo/components/PromotinProgram.jsx134
-rw-r--r--src-migrate/modules/promo/components/Voucher.tsx127
-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
9 files changed, 723 insertions, 0 deletions
diff --git a/src-migrate/modules/promo/components/FlashSale.tsx b/src-migrate/modules/promo/components/FlashSale.tsx
new file mode 100644
index 00000000..16cb7647
--- /dev/null
+++ b/src-migrate/modules/promo/components/FlashSale.tsx
@@ -0,0 +1,21 @@
+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(
+ <>
+ <h1 className='text-h-sm md:text-h-lg font-semibold'>Bayar Setengahnya!</h1>
+ <FlashSale/>
+ </>
+ )
+ }
+
+ 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..2701250d
--- /dev/null
+++ b/src-migrate/modules/promo/components/Hero.tsx
@@ -0,0 +1,108 @@
+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 'swiper/css/pagination';
+import { Navigation, Pagination, Autoplay } from 'swiper';
+import MobileView from '../../../../src/core/components/views/MobileView';
+import DesktopView from '@/core/components/views/DesktopView';
+
+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 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.1;
+ swiperBanner.loop = true;
+ swiperBannerMobile.loop = true;
+ }
+ }, [banners]);
+
+ const swiperBannerMobile = {
+ ...swiperBannerMob,
+ pagination: { dynamicBullets: false, clickable: true },
+ };
+
+ return (
+ <>
+ <DesktopView>
+ <div className={style['wrapper']}>
+ <div className={style['desc-section']}>
+ <div className={style['title']}>Pasti Hemat & Untung Selama Belanja di Indoteknik.com!</div>
+ <div className='h-4' />
+ <div className={style['subtitle']}>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']}>
+ <Swiper {...swiperBanner}>
+ {banners.map((banner, index) => (
+ <SwiperSlide key={index}>
+ <Image
+ src={banner.image}
+ alt={banner.name}
+ width={666}
+ height={450}
+ quality={100}
+ className='w-full h-full object-fit object-center rounded-2xl' />
+ </SwiperSlide>
+ ))}
+ </Swiper>
+ </div>
+ </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.jsx b/src-migrate/modules/promo/components/PromoList.jsx
new file mode 100644
index 00000000..e6add893
--- /dev/null
+++ b/src-migrate/modules/promo/components/PromoList.jsx
@@ -0,0 +1,110 @@
+import React, { useEffect, useState } from 'react';
+import { Button, Skeleton } from '@chakra-ui/react'
+import clsxm from "~/libs/clsxm"
+import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card';
+import { fetchPromoItemsSolr } from '../../../../src/api/promoApi';
+import { Swiper, SwiperSlide } from 'swiper/react';
+import useDevice from '@/core/hooks/useDevice';
+import LogoSpinner from '../../../../src/core/components/elements/Spinner/LogoSpinner';
+import usePromoStore from './promoStore';
+import { ChevronRightIcon } from '@heroicons/react/24/outline';
+import Link from '@/core/components/elements/Link/Link'
+
+const PromoList = ({ selectedPromo }) => {
+ const {
+ title,
+ slug,
+ promoItems,
+ promoData,
+ isLoading,
+ setTitle,
+ setSlug,
+ setPromoItems,
+ setPromoData,
+ setIsLoading,
+ } = usePromoStore();
+
+ const { isMobile } = useDevice();
+
+ 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
+ })}
+ >
+ {promoData?.map((promotion) => (
+ <div key={promotion.id} className="min-w-[400px] max-w-[400px]">
+ <ProductPromoCard 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..64b6b935
--- /dev/null
+++ b/src-migrate/modules/promo/components/Voucher.tsx
@@ -0,0 +1,127 @@
+import { useMemo } from 'react'
+import { useQuery } from 'react-query'
+import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react'
+import { getVoucher } 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';
+
+const Voucher = () => {
+ const { isMobile } = useDevice();
+ const toast = useToast();
+ const voucherQuery = useQuery({
+ queryKey: ['voucher.all-voucher'],
+ queryFn: getVoucher
+ })
+
+ const swiperVoucher: SwiperProps = {
+ autoplay: {
+ delay: 6000,
+ disableOnInteraction: false
+ },
+ loop: false,
+ className: 'h-[160px] w-full',
+ slidesPerView: isMobile ? 1.2 : 3,
+ spaceBetween: 16
+ }
+
+ const vouchers = useMemo(() => voucherQuery.data || [], [voucherQuery.data]);
+
+ 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 Voucher
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..24d3aac2
--- /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 h-full rounded-xl border border-gray-200 shadow-md p-4 flex gap-x-4;
+}
+
+.voucher-image {
+ @apply bg-gray-100 rounded-lg w-4/12 h-full 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-auto;
+}
+
+.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;
+}