diff options
| author | IT Fixcomart <it@fixcomart.co.id> | 2024-08-02 03:25:54 +0000 |
|---|---|---|
| committer | trisusilo <tri.susilo@altama.co.id> | 2024-08-02 03:25:54 +0000 |
| commit | 99a5965e1e5320e9acbc9718c3642ffae85bec57 (patch) | |
| tree | 6a9aeca9b6833d7b87d0ea0a3d79d6bd15862751 /src-migrate/modules | |
| parent | 12c7841770052aefceda899db52b3e6b6d0b5e92 (diff) | |
| parent | 35204954ac02efd1497715dec3d2695fdd7976f8 (diff) | |
Merged in Feature/all-promotion (pull request #202)
Feature/all promotion
Approved-by: trisusilo
Diffstat (limited to 'src-migrate/modules')
| -rw-r--r-- | src-migrate/modules/cart/components/Item.tsx | 20 | ||||
| -rw-r--r-- | src-migrate/modules/cart/components/ItemSelect.tsx | 33 | ||||
| -rw-r--r-- | src-migrate/modules/footer-banner/index.tsx | 2 | ||||
| -rw-r--r-- | src-migrate/modules/product-promo/components/Card.tsx | 6 | ||||
| -rw-r--r-- | src-migrate/modules/product-promo/styles/card.module.css | 2 | ||||
| -rw-r--r-- | src-migrate/modules/promo/components/FlashSale.tsx | 20 | ||||
| -rw-r--r-- | src-migrate/modules/promo/components/Hero.tsx | 105 | ||||
| -rw-r--r-- | src-migrate/modules/promo/components/HeroDiskon.tsx | 137 | ||||
| -rw-r--r-- | src-migrate/modules/promo/components/PromoList.tsx | 135 | ||||
| -rw-r--r-- | src-migrate/modules/promo/components/PromotinProgram.jsx | 134 | ||||
| -rw-r--r-- | src-migrate/modules/promo/components/Voucher.tsx | 160 | ||||
| -rw-r--r-- | src-migrate/modules/promo/components/promoStore.js | 16 | ||||
| -rw-r--r-- | src-migrate/modules/promo/styles/hero.module.css | 27 | ||||
| -rw-r--r-- | src-migrate/modules/promo/styles/voucher.module.css | 43 | ||||
| -rw-r--r-- | src-migrate/modules/side-banner/index.tsx | 10 |
15 files changed, 822 insertions, 28 deletions
diff --git a/src-migrate/modules/cart/components/Item.tsx b/src-migrate/modules/cart/components/Item.tsx index 6ded6373..74180fc9 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, pilihSemuaCart }: 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} itemSelected={pilihSemuaCart} /> + )} <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..6b6b8f2b 100644 --- a/src-migrate/modules/cart/components/ItemSelect.tsx +++ b/src-migrate/modules/cart/components/ItemSelect.tsx @@ -1,5 +1,5 @@ import { Checkbox, Spinner } from '@chakra-ui/react' -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { getAuth } from '~/libs/auth' import { CartItem } from '~/types/cart' @@ -9,15 +9,17 @@ import { useCartStore } from '../stores/useCartStore' type Props = { item: CartItem + itemSelected?: boolean } -const CartItemSelect = ({ item }: Props) => { +const CartItemSelect = ({ item, itemSelected }: Props) => { const auth = getAuth() const { loadCart } = useCartStore() const [isLoad, setIsLoad] = useState<boolean>(false) + const [isChecked, setIsChecked] = useState<boolean>(itemSelected ?? true) - const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => { + const handleChange = async (isChecked: boolean) => { if (typeof auth !== 'object') return setIsLoad(true) @@ -26,29 +28,40 @@ const CartItemSelect = ({ item }: Props) => { type: item.cart_type, id: item.id, qty: item.quantity, - selected: e.target.checked + selected: isChecked, }) await loadCart(auth.id) setIsLoad(false) } + useEffect(() => { + if (typeof itemSelected === 'boolean') { + setIsChecked(itemSelected) + handleChange(itemSelected) + } + }, [itemSelected]) + + const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const { checked } = e.target + setIsChecked(checked) + handleChange(checked) + } + return ( <div className='w-6 my-auto'> - {isLoad && ( - <Spinner className='my-auto' size='sm' /> - )} + {isLoad && <Spinner className='my-auto' size='sm' />} {!isLoad && ( <Checkbox borderColor='gray.600' colorScheme='red' size='lg' - isChecked={item.selected} - onChange={handleChange} + isChecked={isChecked} + onChange={handleCheckboxChange} /> )} </div> ) } -export default CartItemSelect
\ No newline at end of file +export default CartItemSelect 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-promo/components/Card.tsx b/src-migrate/modules/product-promo/components/Card.tsx index 56e29e38..728d23ca 100644 --- a/src-migrate/modules/product-promo/components/Card.tsx +++ b/src-migrate/modules/product-promo/components/Card.tsx @@ -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> 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/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..42725034 --- /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 promotion={promotion} /> + </div> + </SwiperSlide> + ))} + </Swiper> + )} + {isMobile && (promoData?.map((promotion: IPromotion) => ( + <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..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; |
