diff options
29 files changed, 1043 insertions, 207 deletions
diff --git a/public/images/icon_promo/angklung.svg b/public/images/icon_promo/angklung.svg new file mode 100644 index 00000000..a650d816 --- /dev/null +++ b/public/images/icon_promo/angklung.svg @@ -0,0 +1,9 @@ +<svg width="39" height="38" viewBox="0 0 39 38" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<rect x="0.666992" y="-0.00439453" width="38" height="38" fill="url(#pattern0_3825_7139)"/> +<defs> +<pattern id="pattern0_3825_7139" patternContentUnits="objectBoundingBox" width="1" height="1"> +<use xlink:href="#image0_3825_7139" transform="scale(0.00478469)"/> +</pattern> +<image id="image0_3825_7139" width="209" height="209" xlink:href="data:image/png;base64,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"/> +</defs> +</svg> diff --git a/public/images/icon_promo/barong.svg b/public/images/icon_promo/barong.svg new file mode 100644 index 00000000..22e51ff9 --- /dev/null +++ b/public/images/icon_promo/barong.svg @@ -0,0 +1,9 @@ +<svg width="39" height="38" viewBox="0 0 39 38" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<rect x="0.333008" y="-0.00439453" width="38" height="38" fill="url(#pattern0_3850_5455)"/> +<defs> +<pattern id="pattern0_3850_5455" patternContentUnits="objectBoundingBox" width="1" height="1"> +<use xlink:href="#image0_3850_5455" transform="scale(0.00478469)"/> +</pattern> +<image id="image0_3850_5455" width="209" height="209" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANEAAADRCAMAAABl5KfdAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADNQTFRF+MHE6URO8IKJ5BYi/e/w++Dh7WRs9KGm5iUx5zU/+tDT8pKX61Rd73N79rG14gYT////ng3nmAAAABF0Uk5T/////////////////////wAlrZliAAAJeUlEQVR42uzd6ZqjKhAGYFRw1/T9X+3004mGpaA+EBOSMf/OnHTCq1CyFET8fNtLXKJLdIku0SW6RJfoEpUkEuKbRGrpb7+vflHfIVLtbX+16vNFsm5u2qup5YeLJsNzN32yqOtvxKvvPlUkBu3OzLN2t3rxiSJRaZ7JroGV+DSRHuD2aGBEiVl9kkguNzpi69CTQvkZouCtUPPJofwEUcc0F6OB1eWL9IA9CD4IZg/lmUVoWXV3v5YrcgM21pnIGsozfhYZsMH4UanyRGZchmKYPCWUZxIZFxwvnHEZMoVykd0T1yjMUC4LESEBGzR1JYiMQNy95xNyiowRw5SjmzGId4rGXK3AbInj20STFqmWg63a6K7XbxJlnuMx5ozeIpoyVRPNVGUgpYvUGb2yZygXrxe1h7+bLM9+nV4ukmd0nfWnrXq1aNVn4PKYdM/t1r1aVJuzisdNpic9gicXZLFnSo+ZjF74oWiXXIz7FV30C5s+bDM81XIoNBwUCbOypD1oTY94xLu3iX6OmgzPX9f77SJrCSLO5HqKECWbKE8hoiSTMYTQhnqFiKJNPk9BItPEDAD9nqJEsMnwONMlRYl+e0cNa5Lce+4P71JEbHklb/57w1SMiCnzxN/Dv17wIAsS2aYpup2NdZc8EXOOyBPLImLhG2ZOKm5E7piM59V5yTTniey619xek/B0pshaIXpRAte5InJsenJC2tki23R+gt35It2Ua/L13aJ9KgyY3hH1vK3f9NWyqlJF937NjXtXNztRZJhUmaKKF1npn1pmkfhIkdcTPRVYiGgNePAEiXJE0mw/TVUtdVUZOa6N+CTRqKdHLet+O+S69PHz4C8S9QHRuNe4pnWeV2v0st+LRDe/aF/594wvnvkF7WeI9jvkb/z7sHf5BNEGakJpg3tDU+WL5KOsQ7isWzAU5YseJeWnSe6d3bF40YqC7vNDbfHtSN6DegONMZQYy491j9XpQnKJ56ikA1Ik73FuLmTEV0MjnqDo/hGN+h5RfzjzrDDRij01VT3JDxG1UGetM6K7nJthLVbUI4GuMzt0FX9X3ye65+c1COi53gd0WN8n6oDQvWXbTcbnVIWK6hu7krd1zJ/t6MZ/6/tEFdv3dEGPtqdO7DPIoyIA1MuovleqSDVRiYo+kRYYlPKMBcefV4i2USW69ugTVfqYzpjnVxQIaXxJImM3WJdFNFml3wa31kij5jtOCSJ7QhfZjsKKKvPyeECniKgJan47ik/Ub/+1GBnEG8gJhXX+dtQ1SXPthKg1/mlrNn+kHdTR0w0ZRebeJ4HvLPQ+YZUzb7cEQMBDLEqkJ6TeGw+8wdLbC1qJqcjWn8Oetc9A7yEE91QRJRF2r1MNZk0mQOO92WYR+bfvQhuVqWt7M0KD3ny8D+8JGFJhovBeXGBnoX98NHpIZLEHoKMiYgM2efQFu3+UElVuyZ9LYyToseVJHhUhe9q5Pb6U6DEVJInw7KlYLTIbJqICdjBVK1QxKVFHrt21fpCCtvEwopizIUJnmBCirYbZ83VrVXniSwuVQ+ABGwiI3lDuiLRqis6pCqyHEvh/KWes+K6BLTI6h9ggS/bYxiCB1SF8DnAlQ7kpsg6vwtYmZjCpDYldkRlkVCzRRcI5jKsBLljrnkgmI0T1oUNIiFD+FJn1ckCXxLbRRs2efSBSnpeAyarzm8hum9sgoh+xO7Tw4VeADeHIwP232j4m7d34vgWw4Fq53G7MAPS6RChYJZ+3YIfy7QOJzlTHHx6wp0HtlXMKnOcgqJ5JphRzUSHpZR1TIZ5XRmttRlOtA6I2KWBjodzbmeq0KuR8qTZONsOH9J19INzhR9aUX/fcRPdWjPqTotO+Wei5W7P0N9XJI9pnL3KeT2Cf1TmFWv4jva6tf1+VOaSdgtVa0aLllKRs8y55GsvE5EAOY7if1tKi/oRE8xWodc4oGfsT41kgKZHKvxmAjnZ0pVZtE+OxLoGgRML6pOU10VsrZEc8wloBtE5QdPCJ5PcEgo9cl2ov6zDXAp2pDogWYLNkrKd6XCzrH/3dDSECh2db2wQXTlQjmyEjPc+eKmr6QT3SmdwnRD9HTUSx6dFEvIkoGSQ6ZLLPj3BHfLop6uRbslSgyPnrQx57VJ54RgW9VRMWhTezIx79T+yZExF/9oFv62mEKNrk9VDzdZFnH5hvl84ACBRZT+awSYXeSs6p4qbQWyNFTEFhOr2SBZrCb4sWQSa2evrW5gAT95YEkW0S0Z7QaiNTYJ6cJPIGZTfMT/gaHxfE7LGIp1omirwm8FEcXhH2fAb22EoWkSa4a8GscVOfg3aXDojsL1kVft4Cu2pvmcQKd/8OifyDHq7rB+QhePePMt3ZgyLaxHdlAZHHxHbPD4tcE9I1h0SECRhuZBCl9J1BUcrB2llE2pIHOBaARdpxvuBCTybRNrmHrl5EiLbJ6h4cZmYTLcf3tnjvUtS5W9lE9Xmin0t0iS7Rp4rarxNVl+gSvUkkLtEl+g6R+jqRuESX6BL9H6L+60TVd4qGrxNVl+gS/S+i9hTRcF5kmP0iFXXEUpRIIlsK00RjIGPw8RgEj9yPEW17d4cxu2j7aDLzdttigCWf4SIV+0uEEaItq9qTHS2biFMkUJF9ZDlggkX7yra+JcsoUheRqIWJVJtwqDwo0lbq9WL4NmWwJkSkPGnpjAkS6ZkHRlGtIq1wQh0vMrOJpmlATYDI2FRlxhu7SHBCHSciMr7QhDpWZHyQnWgvkKLEizwXBjMxIuZDBHZ5I0XSu9MueHkREXtRBFBl1mhRHUhP45OJAiLgJgukGVCZEpCILjFn8oqgzBCwabumoGhiOh9hk0cEZrqEwu8YyggLR4aZ6UwFE/NIEZy5E35EBrL2CFGn/Q6T5DoGARMhishE4roxXpMj+jvtCR0CBU2OyCwE04Hnu5oekyXaWkbUGcOeH7GzRJG7YaAzwKnNKIbo2dIjN26TidWGSN9bD6WnYUM2ohprIj1yRe9nJEyaiH2IpIoI0y4yInHKWQGOaRcleGLO3bIe1w/RgZ+G9fYCH6IkT9zZaEbpG/t8gkM7aI3Sux/dRwSc9PPrMv88mGojtyznEXlNOXY4e0yx53jEb+AkTLl+HkzNhz1pp49appxnH1i55Cm7cdNKox0+kNNjmRJ3FyfG2/umduLnfY6/xr8fqu/nxJ8fPXCFR3Har20eusk/3/a6RJfoEl2iS3SJLtEZr38CDABd2iX1EdXhcwAAAABJRU5ErkJggg=="/> +</defs> +</svg> diff --git a/public/images/icon_promo/diskon.svg b/public/images/icon_promo/diskon.svg new file mode 100644 index 00000000..23d24875 --- /dev/null +++ b/public/images/icon_promo/diskon.svg @@ -0,0 +1,9 @@ +<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<rect y="-0.00439453" width="38" height="38" fill="url(#pattern0_3853_5915)"/> +<defs> +<pattern id="pattern0_3853_5915" patternContentUnits="objectBoundingBox" width="1" height="1"> +<use xlink:href="#image0_3853_5915" transform="scale(0.00478469)"/> +</pattern> +<image id="image0_3853_5915" width="209" height="209" xlink:href="data:image/png;base64,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"/> +</defs> +</svg> diff --git a/public/images/icon_promo/silat.svg b/public/images/icon_promo/silat.svg new file mode 100644 index 00000000..112ecaff --- /dev/null +++ b/public/images/icon_promo/silat.svg @@ -0,0 +1,9 @@ +<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<rect y="-0.00439453" width="38" height="38" fill="url(#pattern0_3821_6700)"/> +<defs> +<pattern id="pattern0_3821_6700" patternContentUnits="objectBoundingBox" width="1" height="1"> +<use xlink:href="#image0_3821_6700" transform="scale(0.005)"/> +</pattern> +<image id="image0_3821_6700" width="200" height="200" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADNQTFRF+MHE8IKJ6URO5iUx9KGm/e/w5BYi7WRs++Dh5zU/9rG1+tDT73N78pKX61Rd4gYT////3hCh9wAAABF0Uk5T/////////////////////wAlrZliAAADrklEQVR42uzd65KjIBAFYGgF4iXR93/arYw3FNTZjRqOe/pnYtX4VY0a+wKqvUkoQgghhBBCCDkIUpX6lHiqx5UQUzTnhbsQUjdnRnkdRE6F6OsgxakQdx3kVEcjhPwrJHL77K8e2bjD9oe48Bv3LUjkGL1/vfaHRB6vihBCCCGEEEIISR6SO3EKH5Jn3ckpbEjPmFEAIR7Do8BBFoyRAgaJMHoKFESy1fe/QpAgs8jERj6Fg2T5+2OLDvlhvHOTAQUKMjBiFCCIzwgpMJAlY0lJHFJuMOYUkzakSwuvMTxK3SYOMVrqnbqAyd0rKk0K8tEfuBmkUGfEFyDnhM1vAmnsXSDTk4sQQsAhWaRe179xykbs1gNVCg9E2f+Lu79kCSGEEEIIIYSQTciFbU4RSP6Sp8GHdGULqw02ZKq+TBRAyLyINFDgIGEtrKOAQeIlvTcFCpKvlvSs/gASSYP1/ZVuI1PWH/IMv3nuQn4RBz0Qz+pFkUhJz7SlwEOGfL8SaIhftlhSgCDL6sucAgOJFZF8SuIQt1PSGylF4pCq2Svp9ZQ8cUirXuJ2SnpVLZK3qUM+CUJmkNKJiK7QIeXwK08qZMjj5T2paoMKebjY+w8cZMkYKVgQ5daGDB9QELvx5pMhQWb/T3XpvtuceQiku8IjFwwWZEqfBRQkiJ/QDCg4kDkjoKBAQsaCUqUNqbYYPiVr04b8rKKwzhgptkwd0lZqd9kRo5Rpk4ecU3ojhBBCCCGEEEIIIYQQQggh5H+EHD715jgIQwghhBDyC4iNjLRZRMhGv9ZWNhZqWoEQQgghhBBCEoTIwVH8FeShtToIcv0gzHQKlfRr82FDpg6KiQIImbfnDd3XcJBw9qVrmQeDqOi01ZsCBXmuDo1l+bFTb8MddH9LgsSn3urLpt6sfpjYWOJBkMvG9/oG9ZCCBXHjlkTBkpxIEDfbWWlBwYG4YIOoGSVxyGudMaekPvXWfWXd6nZdA0UnDmlz22SbPZ+teRbxBTk5LHZTiNFZU+TwkOEa9IZTESH+DX6k4EGWvx56ChrERJZY75Zeh4LEGD0FClKsl8oyQYIsTj27wdRbl3iKvMqBQcb8WUCBgvgZzSUFCCLLc51RYCASKwF4lMQh1RbDp1iTNqSVTcZE0W3ikDbXz73to6tFiSZNyDmlN0IIIYQQQgghhBBCCCGEEEIIuQICFoQAQOQukBckpAohJaJjarfxsoPlxnbfaYbUJgbBDkIIIYQQQr4SfwQYAF0sNCgAdJuiAAAAAElFTkSuQmCC"/> +</defs> +</svg> diff --git a/src-migrate/constants/menu.ts b/src-migrate/constants/menu.ts index d1adebca..e3e7b0c6 100644 --- a/src-migrate/constants/menu.ts +++ b/src-migrate/constants/menu.ts @@ -2,6 +2,9 @@ import { SecondaryNavItemProps } from '~/types/nav'; export const SECONDARY_MENU_ITEMS: SecondaryNavItemProps[] = [ { + label: 'Semua Promo', + href: '/shop/promo', + },{ label: 'Semua Brand', href: '/shop/brands', }, 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/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; +} 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; diff --git a/src-migrate/pages/shop/promo/index.tsx b/src-migrate/pages/shop/promo/index.tsx new file mode 100644 index 00000000..7c4df2c1 --- /dev/null +++ b/src-migrate/pages/shop/promo/index.tsx @@ -0,0 +1,40 @@ +import dynamic from 'next/dynamic' +import React, { useState } from 'react' +import { LazyLoadComponent } from 'react-lazy-load-image-component' +import Hero from '~/modules/promo/components/Hero' +import PromotionProgram from '~/modules/promo/components/PromotinProgram' +import Voucher from '~/modules/promo/components/Voucher' +import FlashSale from '../../../modules/promo/components/FlashSale' +const PromoList = dynamic(() => import('../../../modules/promo/components/PromoList')); + + + +const PromoPage = () => { + const [selectedPromo, setSelectedPromo] = useState('Bundling'); + return ( + <> + <LazyLoadComponent> + <Hero /> + </LazyLoadComponent> + + + <LazyLoadComponent> + <PromotionProgram + selectedPromo={selectedPromo} + onSelectPromo={setSelectedPromo} + /> + <PromoList selectedPromo={selectedPromo} /> + </LazyLoadComponent> + + <LazyLoadComponent> + <FlashSale /> + </LazyLoadComponent> + <h1 className='h-4'></h1> + <LazyLoadComponent> + <Voucher /> + </LazyLoadComponent> + </> + ) +} + +export default PromoPage
\ No newline at end of file diff --git a/src-migrate/services/voucher.ts b/src-migrate/services/voucher.ts new file mode 100644 index 00000000..447b448e --- /dev/null +++ b/src-migrate/services/voucher.ts @@ -0,0 +1,8 @@ +import odooApi from '~/libs/odooApi'; +import { IVoucher } from '~/types/voucher'; + +export const getVoucher = async (): Promise<IVoucher[]> => { + const url = `/api/v1/voucher`; + + return await odooApi('GET', url); +}; diff --git a/src-migrate/types/banner.ts b/src-migrate/types/banner.ts index dbccc378..e1604ad4 100644 --- a/src-migrate/types/banner.ts +++ b/src-migrate/types/banner.ts @@ -1,8 +1,8 @@ export interface IBanner { - background_color: string | false; - group_by_week: number | false; image: string; name: string; sequence: number; - url: string; + url: string | false; + group_by_week: number | false; + background_color: string | false; } diff --git a/src-migrate/types/voucher.ts b/src-migrate/types/voucher.ts new file mode 100644 index 00000000..3e90f449 --- /dev/null +++ b/src-migrate/types/voucher.ts @@ -0,0 +1,8 @@ +export interface IVoucher { + id: number; + image: string; + name: string; + code: string; + description: string | false; + remaining_time: string; +} diff --git a/src/api/promoApi.js b/src/api/promoApi.js index 0e82c8b9..967aeab2 100644 --- a/src/api/promoApi.js +++ b/src/api/promoApi.js @@ -13,11 +13,13 @@ export const fetchPromoItems = async (type) => { } }; -export const fetchPromoItemsSolr = async (type) => { +export const fetchPromoItemsSolr = async (type, start, rows) => { // let query = type ? `type_value_s:${type}` : '*:*'; let sort ='sort=if(exists(sequence_i),0,1) asc, sequence_i asc, if(exists(total_qty_sold_f), total_qty_sold_f, -1) desc'; let start = 0 let rows = 100 + // let start = 0 + // let rows = 10 try { const queryParams = new URLSearchParams({ q: type }); const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}&rows=${rows}&start=${start}&${sort}`); diff --git a/src/core/components/elements/Footer/PromoOffer.tsx b/src/core/components/elements/Footer/PromoOffer.tsx new file mode 100644 index 00000000..a5432b6a --- /dev/null +++ b/src/core/components/elements/Footer/PromoOffer.tsx @@ -0,0 +1,112 @@ + +import React from "react"; +// import { useGeneralSetting } from "@/common/state-management/general-setting"; +import { FormEvent, useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import style from "../Footer/style/promoOffer.module.css" +const PromoOffer = ()=>{ + // const { data, isLoading, fetchData } = useGeneralSetting(); + const [formData, setFormData] = useState<FormData>({ + email: "", + name: "", + telephone: "", + message: "", + }); + + useEffect(() => { + // fetchData(); + }, []); + + type FormData = { + email: string; + name: string; + telephone: string; + message: string; + }; + + const [errors, setErrors] = useState({ + email: false, + name: false, + message: false, + }); + + +const handleGetOffer = async (e: FormEvent) => { + e.preventDefault(); + let loadingToast; + try { + loadingToast = toast.loading("Mengirimkan formulir..."); + const response = await fetch("/api/contactus", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ...formData, from: "newsletter" }), + }); + + if (response.ok) { + toast.dismiss(loadingToast); + toast.success("Terima kasih telah menghubungi kami"); + setFormData({ + email: "", + name: "", + telephone: "", + message: "", + }); + } else { + toast.dismiss(loadingToast); + toast.error("Gagal mengirimkan formulir. Silakan coba lagi nanti."); + } + } catch (error) { + toast.dismiss(loadingToast); + console.error("Gagal mengirimkan formulir", error); + toast.error("Terjadi kesalahan. Silakan coba lagi nanti."); + } + }; + + + + + return( + <div className=" py-3 mb-3"> + <div className="md:flex container mx-auto md:justify-between md:items-center md:gap-x-20"> + <div className=""> + <span className="text-black font-semibold text-sm md:text-lg"> + Dapatkan Promo Menarik Setiap Bulan{" "} + </span> + <p> + Promo produk dengan penawaran terbatas setiap bulannya! + </p> + </div> + <div className=" flex-1 flex items-center h-full justify-end text-sm text-slate-950"> + <form onSubmit={handleGetOffer} className={style['form-input']}> + <div className="flex justify-start w-full"> + <div className="flex justify-end"> + <input + type="email" + value={formData.email} + onChange={(e) => + setFormData({ ...formData, email: e.target.value }) + } + className={style['input']} + placeholder="Masukkan email anda disini" + /> + <button + type="submit" + className={style['button']} + > + Dapatkan + </button> + </div> + + + </div> + </form> + + </div> + </div> + </div> + ) + }; + + export default PromoOffer; diff --git a/src/core/components/elements/Footer/style/promoOffer.module.css b/src/core/components/elements/Footer/style/promoOffer.module.css new file mode 100644 index 00000000..3184182d --- /dev/null +++ b/src/core/components/elements/Footer/style/promoOffer.module.css @@ -0,0 +1,39 @@ +.form-input { + @apply + h-full + w-[514px] + text-slate-950 + flex + justify-center; +} + +.input{ + @apply w-[320px] + sm:w-[320px] + md:w-[500px] + xl:w-[514px] + lg:w-[514px] + 2xl:w-[514px] + text-black + py-2 + h-11 + md:py-3 + px-4 + bg-[#FDF1C7] + rounded-3xl + focus:outline-none + ; +} + +.button{ + @apply bg-[#FAD147] + absolute + py-1.5 + rounded-3xl + text-black + md:py-2.5 + px-4 + h-11 + z-0 + ; +}
\ No newline at end of file diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index 308f2623..e73a35ee 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -171,6 +171,17 @@ const NavbarDesktop = () => { </button> <div className='w-6/12 flex px-1 divide-x divide-gray_r-6'> <Link + href='/shop/promo' + className={`${ + router.asPath === '/shop/promo' && 'bg-gray_r-3' + } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`} + // className='p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition' + target='_blank' + rel='noreferrer' + > + Semua Promo + </Link> + <Link href='/shop/brands' className={`${ router.asPath === '/shop/brands' && 'bg-gray_r-3' @@ -201,9 +212,7 @@ const NavbarDesktop = () => { </Link> <Link href='/video' - className={`${ - router.asPath === '/video' && 'bg-gray_r-3' - } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition`} + className='p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition' target='_blank' rel='noreferrer' > diff --git a/src/core/components/elements/Sidebar/Sidebar.jsx b/src/core/components/elements/Sidebar/Sidebar.jsx index 38fcdef8..cde68515 100644 --- a/src/core/components/elements/Sidebar/Sidebar.jsx +++ b/src/core/components/elements/Sidebar/Sidebar.jsx @@ -117,6 +117,9 @@ const Sidebar = ({ active, close }) => { </> )} </div> + <SidebarLink className={itemClassName} href='/shop/promo'> + Semua Promo + </SidebarLink> <SidebarLink className={itemClassName} href='/shop/brands'> Semua Brand </SidebarLink> diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx index b204df8e..5034fd28 100644 --- a/src/lib/home/components/PromotionProgram.jsx +++ b/src/lib/home/components/PromotionProgram.jsx @@ -13,10 +13,9 @@ const BannerSection = () => { <div className='flex justify-between items-center mb-4 '> <div className='font-semibold sm:text-h-lg'>Promo Tersedia</div> {isDesktop && ( - <div></div> - // <Link href='/shop/promo' className='!text-red-500 font-semibold'> - // Lihat Semua - // </Link> + <Link href='/shop/promo' className='!text-red-500 font-semibold'> + Lihat Semua + </Link> )} </div> {isDesktop && (promotionProgram.data && diff --git a/src/pages/shop/promo/[slug].tsx b/src/pages/shop/promo/[slug].tsx index bd69c071..aaee1249 100644 --- a/src/pages/shop/promo/[slug].tsx +++ b/src/pages/shop/promo/[slug].tsx @@ -91,7 +91,7 @@ export default function PromoDetail() { setCurrentPage(pageNumber) try { - const items = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug}`); + const items = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug}`,0,100); setPromoItems(items); if (items.length === 0) { @@ -147,16 +147,16 @@ export default function PromoDetail() { const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`); const product = response.response.docs[0]; const product_id = product.id; - const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} AND ${combinedQueryPrice}`); + const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} AND ${combinedQueryPrice}`,0,100); return response2; }else if(combinedQuery){ const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`); const product = response.response.docs[0]; const product_id = product.id; - const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} `); + const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} `,0,100); return response2; } else { - const response = await fetchPromoItemsSolr(`id:${item.id}`); + const response = await fetchPromoItemsSolr(`id:${item.id}`,0,100); return response; } } catch (fetchError) { diff --git a/src/pages/shop/promo/index.jsx b/src/pages/shop/promo/index.jsx new file mode 100644 index 00000000..01a11aad --- /dev/null +++ b/src/pages/shop/promo/index.jsx @@ -0,0 +1,40 @@ +import Seo from '@/core/components/Seo' +import BasicLayout from '@/core/components/layouts/BasicLayout'; +import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'; +import Link from 'next/link'; +import Promo from '~/pages/shop/promo'; + +import React from 'react'; + +const PromoPage = () => { + return ( + <BasicLayout> + <Seo title='Promo Indoteknik.com' /> + <div className='container mx-auto py-4 md:py-6 pb-0'> + <Breadcrumb> + <BreadcrumbItem> + <BreadcrumbLink + as={Link} + href='/' + className='!text-danger-500 whitespace-nowrap' + > + Home + </BreadcrumbLink> + </BreadcrumbItem> + + <BreadcrumbItem isCurrentPage> + <BreadcrumbLink className='whitespace-nowrap'> + Promo + </BreadcrumbLink> + </BreadcrumbItem> + </Breadcrumb> + + <div className='h-10' /> + + <Promo /> + </div> + </BasicLayout> + ); +}; + +export default PromoPage; diff --git a/src/pages/shop/promo/index.tsx b/src/pages/shop/promo/index.tsx deleted file mode 100644 index 7ec4f6b0..00000000 --- a/src/pages/shop/promo/index.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import dynamic from 'next/dynamic' -import { useEffect, useState } from 'react' -import { useRouter } from 'next/router' -import Seo from '../../../core/components/Seo.jsx' -import Promocrumb from '../../../lib/promo/components/Promocrumb.jsx' -import { fetchPromoItemsSolr } from '../../../api/promoApi.js' -import LogoSpinner from '../../../core/components/elements/Spinner/LogoSpinner.jsx' -import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card.tsx' -import { IPromotion } from '../../../../src-migrate/types/promotion.ts' -import React from 'react' -import { SolrResponse } from "../../../../src-migrate/types/solr.ts"; - -const BasicLayout = dynamic(() => import('../../../core/components/layouts/BasicLayout.jsx')) - -export default function Promo() { - const router = useRouter() - const { slug = '' } = router.query - const [promoItems, setPromoItems] = useState<any[]>([]) - const [promoData, setPromoData] = useState<IPromotion[] | null>(null) - const [loading, setLoading] = useState(true) - const [currentPage, setCurrentPage] = useState(1) - const [fetchingData, setFetchingData] = useState(false) - - useEffect(() => { - const loadPromo = async () => { - try { - const items = await fetchPromoItemsSolr(`*:*`) - - - setPromoItems(items) - - - if (items.length === 0) { - setPromoData([]) - setLoading(false); - return; - } - - const promoDataPromises = items.map(async (item) => { - const queryParams = new URLSearchParams({ q: `id:${item.id}` }) - - - try { - const response = await fetch(`/solr/promotion_program_lines/select?${queryParams.toString()}`) - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) - } - - const data: SolrResponse<any[]> = await response.json() - - - const promotions = await map(data.response.docs) - return promotions; - } catch (fetchError) { - console.error("Error fetching promotion data:", fetchError) - return []; - } - }); - - const promoDataArray = await Promise.all(promoDataPromises); - const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); - setPromoData(mergedPromoData); - setTimeout(() => setLoading(false), 120); // Menambahkan delay 200ms sebelum mengubah status loading - } catch (loadError) { - console.error("Error loading promo items:", loadError) - setLoading(false); - } - } - - if (slug) { - loadPromo() - } - }, [slug]) - - const map = async (promotions: any[]): Promise<IPromotion[]> => { - const result: IPromotion[] = [] - - for (const promotion of promotions) { - const data: IPromotion = { - id: promotion.id, - program_id: promotion.program_id_i, - name: promotion.name_s, - type: { - value: promotion.type_value_s, - label: promotion.type_label_s, - }, - limit: promotion.package_limit_i, - limit_user: promotion.package_limit_user_i, - limit_trx: promotion.package_limit_trx_i, - price: promotion.price_f, - total_qty: promotion.total_qty_i, - products: JSON.parse(promotion.products_s), - free_products: JSON.parse(promotion.free_products_s), - } - - result.push(data) - } - - return result - } - - - - - useEffect(() => { - const handleScroll = () => { - if ( - !fetchingData && - window.innerHeight + document.documentElement.scrollTop >= 0.95 * document.documentElement.offsetHeight - ) { - // User has scrolled to 95% of page height - - setTimeout(() => setFetchingData(true), 120); - setCurrentPage((prevPage) => prevPage + 1) - } - } - - window.addEventListener('scroll', handleScroll) - return () => window.removeEventListener('scroll', handleScroll) - }, [fetchingData]) - - useEffect(() => { - if (fetchingData) { - // Fetch more data - // You may need to adjust this logic according to your API - fetchMoreData() - } - }, [fetchingData]) - - const fetchMoreData = async () => { - try { - // Add a delay of approximately 150ms - setTimeout(async () => { - // Fetch more data - // Update promoData state with the new data - }, 150) - } catch (error) { - console.error('Error fetching more data:', error) - } finally { - setTimeout(() => setFetchingData(false), 120); - - } - } - - const visiblePromotions = promoData?.slice(0, currentPage * 12) - - return ( - <BasicLayout> - <Seo - title={`Promo ${Array.isArray(slug) ? slug[0] : slug} Terkini`} - description='B2B Marketplace MRO & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & Harga Kompetitif' - /> - {/* <Promocrumb brandName={capitalizeFirstLetter(Array.isArray(slug) ? slug[0] : slug)} /> */} - <div className='container mx-auto mt-1 flex mb-1'> - <div className=''> - <h1 className='font-semibold'>Semua Promo di Indoteknik</h1> - </div> - </div> - {loading ? ( - <div className='container flex justify-center my-4'> - <LogoSpinner width={48} height={48} /> - </div> - ) : promoData && promoItems.length >= 1 ? ( - <> - <div className='flex flex-wrap justify-center'> - {visiblePromotions?.map((promotion) => ( - <div key={promotion.id} className="min-w-[40px] max-w-[400px] mr-[20px] mb-[20px] sm:w-full md:w-1/2 lg:w-1/3 xl:w-1/4"> - <ProductPromoCard promotion={promotion} /> - </div> - ))} - </div> - {fetchingData && ( - <div className='container flex justify-center my-4'> - <LogoSpinner width={48} height={48} /> - </div> - )} - </> - ) : ( - <div className="text-center my-8"> - <p>Belum ada promo pada kategori ini</p> - </div> - )} - </BasicLayout> - ) -} |
