diff options
| author | it-fixcomart <it@fixcomart.co.id> | 2024-11-29 10:06:30 +0700 |
|---|---|---|
| committer | it-fixcomart <it@fixcomart.co.id> | 2024-11-29 10:06:30 +0700 |
| commit | c7177ab35e35abac0aa322b054ad10d5690bf8de (patch) | |
| tree | c3da3010e31a4fe453520dfc33fa5e0e52daa0b5 /src | |
| parent | 88198c83806b69ce6ab7815cee5c139536849c9f (diff) | |
| parent | 952421c810b53ec4d25ad5ef605bae1bd1d5d616 (diff) | |
Merge branch 'new-release' into CR/quotation-noPo
Diffstat (limited to 'src')
40 files changed, 1571 insertions, 921 deletions
diff --git a/src/components/ui/HeroBanner.jsx b/src/components/ui/HeroBanner.jsx index 64838b85..2eea5915 100644 --- a/src/components/ui/HeroBanner.jsx +++ b/src/components/ui/HeroBanner.jsx @@ -6,7 +6,7 @@ import 'swiper/css/pagination'; import { Swiper, SwiperSlide } from 'swiper/react'; import Image from 'next/image'; -import { useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useQuery } from 'react-query'; import { bannerApi } from '@/api/bannerApi'; @@ -27,7 +27,20 @@ const swiperBanner = { }; const HeroBanner = () => { - const heroBanner = useQuery('heroBanner', bannerApi({ type: 'index-a-1' })); + // const heroBanner = useQuery('heroBanner', bannerApi({ type: 'index-a-1' })); + const [data, setData] = useState(null); + useEffect(() => { + const fetchData = async () => { + const res = await fetch(`/api/hero-banner?type=index-a-1`); + const { data } = await res.json(); + if (data) { + setData(data); + } + }; + + fetchData(); + }, []); + const heroBanner = data; const swiperBannerMobile = { ...swiperBanner, @@ -44,9 +57,9 @@ const HeroBanner = () => { }; const BannerComponent = useMemo(() => { - if (!heroBanner.data) return null; + if (!heroBanner) return null; - return heroBanner.data.map((banner, index) => ( + return heroBanner.map((banner, index) => ( <SwiperSlide key={index}> <Link href={banner.url} className='w-full h-auto'> <Image @@ -56,22 +69,22 @@ const HeroBanner = () => { width={1152} height={768} className='w-full h-auto' - priority={index === 0} - loading={index === 0 ? 'eager' : 'lazy'} - placeholder="blur" - blurDataURL="/images/indoteknik-placeholder.png" - sizes="(max-width: 768px) 100vw, 50vw" + priority={index === 0} + loading={index === 0 ? 'eager' : 'lazy'} + placeholder='blur' + blurDataURL='/images/indoteknik-placeholder.png' + sizes='(max-width: 768px) 100vw, 50vw' /> </Link> </SwiperSlide> )); - }, [heroBanner.data]); + }, [heroBanner]); return ( <> <MobileView> <SmoothRender - isLoaded={heroBanner.data?.length > 0} + isLoaded={heroBanner?.length > 0} height='68vw' duration='750ms' delay='100ms' @@ -81,7 +94,7 @@ const HeroBanner = () => { </MobileView> <DesktopView> - {heroBanner.data?.length > 0 && ( + {heroBanner?.length > 0 && ( <Swiper {...swiperBannerDesktop}>{BannerComponent}</Swiper> )} </DesktopView> diff --git a/src/components/ui/HeroBannerSecondary.jsx b/src/components/ui/HeroBannerSecondary.jsx index a7b32a4a..6074c9a6 100644 --- a/src/components/ui/HeroBannerSecondary.jsx +++ b/src/components/ui/HeroBannerSecondary.jsx @@ -1,39 +1,58 @@ -import Link from '@/core/components/elements/Link/Link' -import { getRandomInt } from '@/utils/getRandomInt' -import Image from 'next/image' -import { useMemo } from 'react' -import { useQuery } from 'react-query' -import { HeroBannerSkeleton } from '../skeleton/BannerSkeleton' -import { bannerApi } from '@/api/bannerApi' +import Link from '@/core/components/elements/Link/Link'; +import { getRandomInt } from '@/utils/getRandomInt'; +import Image from 'next/image'; +import { useMemo, useEffect, useState } from 'react'; +import { useQuery } from 'react-query'; +import { HeroBannerSkeleton } from '../skeleton/BannerSkeleton'; +import { bannerApi } from '@/api/bannerApi'; const HeroBannerSecondary = () => { - const heroBannerSecondary = useQuery('heroBannerSecondary', bannerApi({ type: 'index-a-2' })) + const [heroBannerSecondary, setHeroBannerSecondary] = useState([]); + const [isLoading, setIsLoading] = useState(false); + // const heroBannerSecondary = useQuery( + // 'heroBannerSecondary', + // bannerApi({ type: 'index-a-2' }) + // ); - const randomIndex = useMemo(() => { - if (!heroBannerSecondary.data) return null - const length = heroBannerSecondary.data?.length - return getRandomInt(length) - }, [heroBannerSecondary.data]) + useEffect(() => { + const fetchData = async () => { + setIsLoading(true); + const res = await fetch(`/api/hero-banner?type=index-a-2`); + const { data } = await res.json(); + if (data) { + setHeroBannerSecondary(data); + } + setIsLoading(false); + }; + + fetchData(); + }, []); - if (heroBannerSecondary.isLoading) return <HeroBannerSkeleton /> + const randomIndex = useMemo(() => { + if (!heroBannerSecondary) return null; + const length = heroBannerSecondary?.length; + return getRandomInt(length); + }, [heroBannerSecondary]); + if (isLoading) return <HeroBannerSkeleton />; return ( - heroBannerSecondary.data && randomIndex !== null && ( - <Link href={heroBannerSecondary.data[randomIndex].url} className="h-full"> + heroBannerSecondary && + randomIndex !== null && ( + <Link href={heroBannerSecondary[randomIndex]?.url} className='h-full'> <Image - src={heroBannerSecondary.data[randomIndex].image} + src={heroBannerSecondary[randomIndex]?.image} width={512} height={1024} - alt={heroBannerSecondary.data[randomIndex].name} - className="object-cover object-center h-full" - loading="lazy" - placeholder="blur" - blurDataURL="/images/indoteknik-placeholder.png" - sizes="(max-width: 768px) 100vw, 50vw" + alt={heroBannerSecondary[randomIndex]?.name} + className='object-cover object-center h-full' + loading='lazy' + placeholder='blur' + blurDataURL='/images/indoteknik-placeholder.png' + sizes='(max-width: 768px) 100vw, 50vw' /> </Link> ) ); -} +}; -export default HeroBannerSecondary +export default HeroBannerSecondary; diff --git a/src/core/components/elements/Footer/BasicFooter.jsx b/src/core/components/elements/Footer/BasicFooter.jsx index 4688b15b..05dc4d8c 100644 --- a/src/core/components/elements/Footer/BasicFooter.jsx +++ b/src/core/components/elements/Footer/BasicFooter.jsx @@ -264,7 +264,7 @@ const InformationCenter = () => ( <li className='text-gray_r-12/80 flex items-center'> <PhoneArrowUpRightIcon className='w-[18px] mr-2' /> <a href='tel:02129338828' target='_blank' rel='noreferrer'> - (021) 2933-8828 + (021) 29338828 </a> </li> <li className='text-gray_r-12/80 flex items-center'> diff --git a/src/core/components/elements/Footer/SimpleFooter.jsx b/src/core/components/elements/Footer/SimpleFooter.jsx index 371b1652..1f5e13e7 100644 --- a/src/core/components/elements/Footer/SimpleFooter.jsx +++ b/src/core/components/elements/Footer/SimpleFooter.jsx @@ -13,7 +13,7 @@ const SimpleFooter = () => ( <ul className='flex flex-col gap-y-2'> <li className='text-gray_r-12/80 flex items-center'> <PhoneArrowUpRightIcon className='w-[18px] mr-2' /> - <a href='tel:02129338828'>(021) 2933-8828 / 29</a> + <a href='tel:02129338828'>(021) 29338828</a> </li> <li className='text-gray_r-12/80 flex items-center'> <EnvelopeIcon className='w-[18px] mr-2' /> diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index 04cf76d1..fa3df5bf 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -391,7 +391,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/youtube.webp' - alt='Youtube - Indoteknik.com' + // alt='Youtube - Indoteknik.com' width={24} height={24} /> @@ -403,7 +403,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/tiktok.png' - alt='TikTok - Indoteknik.com' + // alt='TikTok - Indoteknik.com' width={24} height={24} /> @@ -423,7 +423,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/Facebook.png' - alt='Facebook - Indoteknik.com' + // alt='Facebook - Indoteknik.com' width={24} height={24} /> @@ -435,7 +435,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/Instagram.png' - alt='Instagram - Indoteknik.com' + // alt='Instagram - Indoteknik.com' width={24} height={24} /> @@ -447,7 +447,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/Linkedin.png' - alt='Linkedin - Indoteknik.com' + // alt='Linkedin - Indoteknik.com' width={24} height={24} /> @@ -459,7 +459,7 @@ const SocialMedias = () => ( > <NextImage src='/images/socials/g_maps.png' - alt='Maps - Indoteknik.com' + // alt='Maps - Indoteknik.com' width={24} height={24} /> diff --git a/src/core/components/elements/Navbar/TopBanner.jsx b/src/core/components/elements/Navbar/TopBanner.jsx index f438ae67..709495ce 100644 --- a/src/core/components/elements/Navbar/TopBanner.jsx +++ b/src/core/components/elements/Navbar/TopBanner.jsx @@ -1,22 +1,37 @@ import Image from 'next/image'; -import { useQuery } from 'react-query';import useDevice from '@/core/hooks/useDevice' +import { useQuery } from 'react-query'; +import useDevice from '@/core/hooks/useDevice'; import odooApi from '@/core/api/odooApi'; import SmoothRender from '~/components/ui/smooth-render'; import Link from '../Link/Link'; import { background } from '@chakra-ui/react'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; const TopBanner = ({ onLoad = () => {} }) => { - const { isDesktop, isMobile } = useDevice() - const topBanner = useQuery({ - queryKey: 'topBanner', - queryFn: async () => await odooApi('GET', '/api/v1/banner?type=top-banner'), - refetchOnWindowFocus: false, - }); + const [topBanner, setTopBanner] = useState([]); + const { isDesktop, isMobile } = useDevice(); + + useEffect(() => { + const fetchData = async () => { + const res = await fetch(`/api/hero-banner?type=top-banner`); + const { data } = await res.json(); + if (data) { + setTopBanner(data); + } + }; + + fetchData(); + }, []); + + // const topBanner = useQuery({ + // queryKey: 'topBanner', + // queryFn: async () => await odooApi('GET', '/api/v1/banner?type=top-banner'), + // refetchOnWindowFocus: false, + // }); // const backgroundColor = topBanner.data?.[0]?.backgroundColor || 'transparent'; - const hasData = topBanner.data?.length > 0; - const data = topBanner.data?.[0] || null; + const hasData = topBanner?.length > 0; + const data = topBanner?.[0] || null; useEffect(() => { if (hasData) { @@ -31,17 +46,15 @@ const TopBanner = ({ onLoad = () => {} }) => { duration='700ms' delay='300ms' className='h-auto' - > + > <Link - href={data?.url} - className="block bg-cover bg-center h-3 md:h-6 lg:h-[36px]" - style={{ - backgroundImage: `url('${data?.image}')`, - }} - > - </Link> - - </SmoothRender> + href={data?.url} + className='block bg-cover bg-center h-3 md:h-6 lg:h-[36px]' + style={{ + backgroundImage: `url('${data?.image}')`, + }} + ></Link> + </SmoothRender> ); }; diff --git a/src/core/components/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx index c4674344..1b62bf05 100644 --- a/src/core/components/layouts/BasicLayout.jsx +++ b/src/core/components/layouts/BasicLayout.jsx @@ -8,6 +8,7 @@ import odooApi from '@/core/api/odooApi'; import whatsappUrl from '@/core/utils/whatsappUrl'; import Navbar from '../elements/Navbar/Navbar'; import styles from './BasicLayout.module.css'; // Import modul CSS +import useDevice from '@/core/hooks/useDevice'; const AnimationLayout = dynamic(() => import('./AnimationLayout'), { ssr: false, @@ -23,6 +24,9 @@ const BasicLayout = ({ children }) => { const [highlight, setHighlight] = useState(false); const [buttonPosition, setButtonPosition] = useState(null); const [wobble, setWobble] = useState(false); + const [isProductPage, setIsProductPage] = useState(false); + + const { isDesktop, isMobile } = useDevice(); const router = useRouter(); const buttonRef = useRef(null); @@ -43,13 +47,16 @@ const BasicLayout = ({ children }) => { setUrlPath(router.asPath); } + if (router.pathname.includes('/shop/product/')) { + setIsProductPage(true); + } }, [product, router]); useEffect(() => { const handleMouseOut = (event) => { const rect = buttonRef.current.getBoundingClientRect(); if (event.clientY <= 0) { - setButtonPosition(rect) + setButtonPosition(rect); setHighlight(true); } else { setHighlight(false); @@ -92,13 +99,15 @@ const BasicLayout = ({ children }) => { return ( <> - {highlight && buttonPosition && ( + {highlight && buttonPosition && ( <div className={styles['overlay-highlight']} style={{ - '--button-x': `${buttonPosition.x + buttonPosition.width / 2}px`, + '--button-x': `${buttonPosition.x + buttonPosition.width / 2}px`, '--button-y': `${buttonPosition.y + buttonPosition.height / 2}px`, - '--button-radius': `${Math.max(buttonPosition.width, buttonPosition.height) / 2}px` + '--button-radius': `${ + Math.max(buttonPosition.width, buttonPosition.height) / 2 + }px`, }} onAnimationEnd={() => setHighlight(false)} /> @@ -106,11 +115,25 @@ const BasicLayout = ({ children }) => { <Navbar /> <AnimationLayout> {children} - <div className='fixed bottom-4 right-4 sm:bottom-14 sm:right-10 z-50'> - <div className='flex flex-row items-center'> - <a href={whatsappUrl(templateWA, payloadWA, urlPath)} className='flex flex-row items-center' rel='noopener noreferrer' target='_blank'> - <span className={`text-green-300 text-lg font-bold mr-4 ${wobble ? 'animate-wobble' : ''}`} onAnimationEnd={() => setWobble(false)}> - Whatsapp + <div + className={`fixed ${ + isMobile && isProductPage ? 'bottom-40' : 'bottom-16' + } right-4 sm:bottom-14 sm:right-10 z-50`} + > + <div className='flex flex-row items-center'> + <a + href={whatsappUrl(templateWA, payloadWA, urlPath)} + className='flex flex-row items-center' + rel='noopener noreferrer' + target='_blank' + > + <span + className={`text-green-300 text-lg font-bold mr-4 ${ + wobble ? 'animate-wobble' : '' + }`} + onAnimationEnd={() => setWobble(false)} + > + {isDesktop && 'Whatsapp'} </span> </a> <a diff --git a/src/core/utils/whatsappUrl.js b/src/core/utils/whatsappUrl.js index 7a129aa6..c840e105 100644 --- a/src/core/utils/whatsappUrl.js +++ b/src/core/utils/whatsappUrl.js @@ -2,28 +2,31 @@ import { getAuth } from "./auth" const whatsappUrl = (template = 'default', payload, urlPath = null) => { let user = getAuth() - if(!user){ - if(urlPath) return `/login?next=${urlPath}` - if(!urlPath) return '/login' - } + // if(!user){ + // if(urlPath) return `/login?next=${urlPath}` + // if(!urlPath) return '/login' + // } let parentName = user.parentName || '-' let url = 'https://wa.me/6281717181922' let text = 'Hallo Indoteknik.com,' + if(user){ + text += `Saya ${user.name}, Saya dari ${parentName}` + } switch (template) { case 'product': - text += ` Saya ${user.name} , Saya dari ${parentName} Saya mencari barang dibawah ini\n\n: Brand = ${payload?.manufacture}\n\n Item Name = ${payload?.name}\n\nLink : ${payload?.url}` + text += ` Saya mencari barang dibawah ini\n\n: Brand = ${payload?.manufacture}\n\n Item Name = ${payload?.name}\n\nLink : ${payload?.url}` break case 'productWeight': - text += ` Saya ${user.name} , Saya dari ${parentName} Saya mencari barang dibawah ini\n\n: Brand = ${payload?.manufacture}\n\n Item Name = ${payload?.name}\n\nLink : ${payload?.url}` + text += ` Saya mencari barang dibawah ini\n\n: Brand = ${payload?.manufacture}\n\n Item Name = ${payload?.name}\n\nLink : ${payload?.url}` break case 'productSearch': - text += `Saya lagi cari-cari produk ${payload?.name}, bisa bantu saya cari produknya?` + text += ` Saya lagi cari-cari produk ${payload?.name}, bisa bantu saya cari produknya?` break case null: - text += `Saya ${user.name}, Saya dari ${parentName} Bisa tolong bantu kebutuhan saya?` + text += ` Bisa tolong bantu kebutuhan saya?` break; default: - text += `Saya ${user.name}, Saya dari ${parentName} Bisa tolong bantu kebutuhan saya?` + text += ` Bisa tolong bantu kebutuhan saya?` break } if (text) url += `?text=${encodeURI(text)}` diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 0e180d9c..6fb5cdb4 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -37,6 +37,18 @@ const SELF_PICKUP_ID = 32; const { checkoutApi } = require('../api/checkoutApi'); const { getProductsCheckout } = require('../api/checkoutApi'); +function convertToInternational(number) { + if (typeof number !== 'string') { + throw new Error("Input harus berupa string"); + } + + if (number.startsWith('08')) { + return '+62' + number.slice(2); + } + + return number; +} + const Checkout = () => { const router = useRouter(); const query = router.query.source ?? null; @@ -413,7 +425,12 @@ const Checkout = () => { Math.round(parseInt(finalShippingAmt * 1.1) / 1000) * 1000; const finalGT = GT < 0 ? 0 : GT; setGrandTotal(finalGT); - }, [biayaKirim, cartCheckout?.grandTotal, activeVoucher, activeVoucherShipping]); + }, [ + biayaKirim, + cartCheckout?.grandTotal, + activeVoucher, + activeVoucherShipping, + ]); const checkout = async () => { const file = poFile.current.files[0]; @@ -442,6 +459,7 @@ const Checkout = () => { const productOrder = products.map((product) => ({ product_id: product.id, quantity: product.quantity, + available_quantity: product?.availableQuantity, })); let data = { // partner_shipping_id: auth.partnerId, @@ -483,6 +501,13 @@ const Checkout = () => { transaction_id: isCheckouted.id, }); + gtag('set', 'user_data', { + email: auth.email, + phone_number: convertToInternational(auth.mobile) ?? convertToInternational(auth.phone), + }); + + gtag('config', 'AW-954540379', { ' allow_enhanced_conversions':true } ) ; + for (const product of products) deleteItemCart({ productId: product.id }); if (grandTotal > 0) { const payment = await axios.post( @@ -500,7 +525,7 @@ const Checkout = () => { } } - /* const midtrans = async () => { + /* const midtrans = async () => { for (const product of products) deleteItemCart({ productId: product.id }); if (grandTotal > 0) { const payment = await axios.post( @@ -1192,7 +1217,11 @@ const Checkout = () => { <div className='text-gray_r-11'> Biaya Kirim <p className='text-xs mt-1'>{etdFix}</p> </div> - <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)}</div> + <div> + {currencyFormat( + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} + </div> </div> {activeVoucherShipping && voucherShippingAmt && ( <div className='flex gap-x-2 justify-between'> @@ -1493,7 +1522,11 @@ const Checkout = () => { Biaya Kirim <p className='text-xs mt-1'>{etdFix}</p> </div> - <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000) }</div> + <div> + {currencyFormat( + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000 + )} + </div> </div> {activeVoucherShipping && voucherShippingAmt && ( <div className='flex gap-x-2 justify-between'> diff --git a/src/lib/flashSale/components/FlashSale.jsx b/src/lib/flashSale/components/FlashSale.jsx index 89c46de4..6d90cad7 100644 --- a/src/lib/flashSale/components/FlashSale.jsx +++ b/src/lib/flashSale/components/FlashSale.jsx @@ -2,10 +2,8 @@ import Image from 'next/image'; import { useEffect, useState } from 'react'; import CountDown from '@/core/components/elements/CountDown/CountDown'; -import productSearchApi from '@/lib/product/api/productSearchApi'; import ProductSlider from '@/lib/product/components/ProductSlider'; -import flashSaleApi from '../api/flashSaleApi'; import { FlashSaleSkeleton } from '../skeleton/FlashSaleSkeleton'; const FlashSale = () => { @@ -14,10 +12,14 @@ const FlashSale = () => { useEffect(() => { const loadFlashSales = async () => { - const dataFlashSales = await flashSaleApi(); - setFlashSales(dataFlashSales); + const res = await fetch('/api/flashsale-header'); + const { data } = await res.json(); + if (data) { + setFlashSales(data); + } setIsLoading(false); }; + loadFlashSales(); }, []); @@ -53,7 +55,10 @@ const FlashSale = () => { height={48} className='w-full rounded mb-4 block sm:hidden' /> - <FlashSaleProduct flashSaleId={flashSale.pricelistId} /> + <FlashSaleProduct + flashSaleId={flashSale.pricelistId} + duration={flashSale.duration} + /> </div> </div> ))} @@ -63,19 +68,24 @@ const FlashSale = () => { ); }; -const FlashSaleProduct = ({ flashSaleId }) => { +const FlashSaleProduct = ({ flashSaleId, duration }) => { const [products, setProducts] = useState(null); - useEffect(() => { + const data_search = new URLSearchParams({ + query: `fq=flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500&orderBy=flashsale-price-asc&source=similar`, + operation: 'AND', + duration: `${duration}`, + }); const loadProducts = async () => { - const dataProducts = await productSearchApi({ - query: `fq=flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500&orderBy=flashsale-price-asc`, - operation: 'AND', - }); - setProducts(dataProducts.response); + const res = await fetch( + `/api/search-flashsale?${data_search.toString()}` + ); + const { data } = await res.json(); + setProducts(data.response); }; + loadProducts(); - }, [flashSaleId]); + }, []); return <ProductSlider products={products} />; }; diff --git a/src/lib/flashSale/components/FlashSaleNonDisplay.jsx b/src/lib/flashSale/components/FlashSaleNonDisplay.jsx index c91de2be..4b420fac 100644 --- a/src/lib/flashSale/components/FlashSaleNonDisplay.jsx +++ b/src/lib/flashSale/components/FlashSaleNonDisplay.jsx @@ -56,7 +56,7 @@ const FlashSaleProduct = ({ flashSaleId }) => { useEffect(() => { const loadProducts = async () => { const dataProducts = await productSearchApi({ - query: `fq=-flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=25&orderBy=flashsale-discount-desc`, + query: `fq=-flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=25&orderBy=flashsale-discount-desc&source=similar`, operation: 'AND', }); setProducts(dataProducts.response); diff --git a/src/lib/home/components/BannerSection.jsx b/src/lib/home/components/BannerSection.jsx index 60d38f8f..303b5c4b 100644 --- a/src/lib/home/components/BannerSection.jsx +++ b/src/lib/home/components/BannerSection.jsx @@ -11,27 +11,33 @@ const BannerSection = () => { const [shouldFetch, setShouldFetch] = useState(false); useEffect(() => { - const localData = localStorage.getItem('Homepage_bannerSection'); - if (localData) { - setData(JSON.parse(localData)); - }else{ - setShouldFetch(true); - } - }, []); - - // const fetchBannerSection = async () => await bannerSectionApi(); - const getBannerSection = useQuery('bannerSection', bannerApi({ type: 'home-banner' }), { - enabled: shouldFetch, - onSuccess: (data) => { + const fetchCategoryData = async () => { + const res = await fetch('/api/banner-section'); + const { data } = await res.json(); if (data) { - localStorage.setItem('Homepage_bannerSection', JSON.stringify(data)); setData(data); } - }, - }); + }; - const bannerSection = data; + fetchCategoryData(); + }, []); + // const fetchBannerSection = async () => await bannerSectionApi(); + const getBannerSection = useQuery( + 'bannerSection', + bannerApi({ type: 'home-banner' }), + { + enabled: shouldFetch, + onSuccess: (data) => { + if (data) { + localStorage.setItem('Homepage_bannerSection', JSON.stringify(data)); + setData(data); + } + }, + } + ); + + const bannerSection = data; return ( bannerSection && bannerSection?.length > 0 && ( diff --git a/src/lib/home/components/CategoryDynamic.jsx b/src/lib/home/components/CategoryDynamic.jsx index e62575f7..cc4f42b7 100644 --- a/src/lib/home/components/CategoryDynamic.jsx +++ b/src/lib/home/components/CategoryDynamic.jsx @@ -1,81 +1,33 @@ -import React, { useEffect, useState, useCallback } from 'react'; -import { - fetchCategoryManagementSolr, - fetchCategoryManagementVersion, -} from '../api/categoryManagementApi'; +import React, { useEffect, useState } from 'react'; +import { fetchCategoryManagementSolr } from '../api/categoryManagementApi'; +import { Skeleton } from '@chakra-ui/react'; import NextImage from 'next/image'; import Link from 'next/link'; import { createSlug } from '@/core/utils/slug'; -import { Skeleton } from '@chakra-ui/react'; import { Swiper, SwiperSlide } from 'swiper/react'; import 'swiper/css'; import 'swiper/css/navigation'; import 'swiper/css/pagination'; import { Pagination } from 'swiper'; -const saveToLocalStorage = (key, data, version) => { - const now = new Date(); - const item = { - value: data, - version: version, - lastFetchedTime: now.getTime(), - }; - localStorage.setItem(key, JSON.stringify(item)); -}; - -const getFromLocalStorage = (key) => { - const itemStr = localStorage.getItem(key); - if (!itemStr) return null; - - const item = JSON.parse(itemStr); - return item; -}; - -const getElapsedTime = (lastFetchedTime) => { - const now = new Date(); - return now.getTime() - lastFetchedTime; -}; - const CategoryDynamic = () => { const [categoryManagement, setCategoryManagement] = useState([]); - const [isLoading, setIsLoading] = useState(false); - - const loadBrand = useCallback(async () => { - const cachedData = getFromLocalStorage('homepage_categoryDynamic'); - - if (cachedData) { - // Hitung selisih waktu antara saat ini dengan waktu terakhir data di-fetch - const elapsedTime = getElapsedTime(cachedData.lastFetchedTime); - - if (elapsedTime < 24 * 60 * 60 * 1000) { - setCategoryManagement(cachedData.value); - return; - } - } + const [isLoading, setIsLoading] = useState(true); - const latestVersion = await fetchCategoryManagementVersion(); - if (cachedData && cachedData.version === latestVersion) { - // perbarui waktu - saveToLocalStorage( - 'homepage_categoryDynamic', - cachedData.value, - latestVersion - ); - setCategoryManagement(cachedData.value); - } else { + useEffect(() => { + const fetchCategoryData = async () => { setIsLoading(true); - const items = await fetchCategoryManagementSolr(); + const res = await fetch('/api/category-management'); + const { data } = await res.json(); + if (data) { + setCategoryManagement(data); + } setIsLoading(false); + }; - saveToLocalStorage('homepage_categoryDynamic', items, latestVersion); - setCategoryManagement(items); - } + fetchCategoryData(); }, []); - useEffect(() => { - loadBrand(); - }, [loadBrand]); - const swiperBanner = { modules: [Pagination], classNames: 'mySwiper', @@ -90,104 +42,99 @@ const CategoryDynamic = () => { return ( <div> {categoryManagement && - categoryManagement?.map((category) => { - return ( - <Skeleton key={category.id} isLoaded={!isLoading}> - <div key={category.id}> - <div className='bagian-judul flex flex-row justify-start items-center gap-3 mb-4 mt-4'> - <h1 className='font-semibold text-[14px] sm:text-h-lg mr-2'> - {category.name} - </h1> - <Link - href={createSlug( - '/shop/category/', - category?.name, - category?.category_id - )} - className='!text-red-500 font-semibold' - > - Lihat Semua - </Link> - </div> + categoryManagement.map((category) => ( + <Skeleton key={category.id} isLoaded={!isLoading}> + <div key={category.id}> + <div className='bagian-judul flex flex-row justify-start items-center gap-3 mb-4 mt-4'> + <h1 className='font-semibold text-[14px] sm:text-h-lg mr-2'> + {category.name} + </h1> + <Link + href={createSlug( + '/shop/category/', + category?.name, + category?.category_id + )} + className='!text-red-500 font-semibold' + > + Lihat Semua + </Link> + </div> - {/* Swiper for SubCategories */} - <Swiper {...swiperBanner}> - {category.categories.map((subCategory) => { - return ( - <SwiperSlide key={subCategory.id}> - <div className='border rounded justify-start items-start '> - <div className='p-3'> - <div className='flex flex-row border rounded mb-2 justify-start items-center'> - <NextImage - src={ - subCategory.image - ? subCategory.image - : '/images/noimage.jpeg' - } - alt={subCategory.name} - width={90} - height={30} - className='object-fit p-4' - /> - <div className='bagian-judul flex flex-col justify-center items-start gap-2 ml-2'> - <h2 className='font-semibold text-lg mr-2'> - {subCategory?.name} - </h2> + <Swiper {...swiperBanner}> + {category?.categories?.map((subCategory) => ( + <SwiperSlide key={subCategory.id}> + <div className='border rounded justify-start items-start '> + <div className='p-3'> + <div className='flex flex-row border rounded mb-2 justify-start items-center'> + <NextImage + src={ + subCategory.image + ? subCategory.image + : '/images/noimage.jpeg' + } + alt={subCategory.name} + width={90} + height={30} + className='object-fit p-4' + /> + <div className='bagian-judul flex flex-col justify-center items-start gap-2 ml-2'> + <h2 className='font-semibold text-lg mr-2'> + {subCategory?.name} + </h2> + <Link + href={createSlug( + '/shop/category/', + subCategory?.name, + subCategory?.id_level_2 + )} + className='!text-red-500 font-semibold' + > + Lihat Semua + </Link> + </div> + </div> + <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px] min-h-[240px] content-start'> + {subCategory.child_frontend_id_i.map( + (childCategory) => ( + <div key={childCategory.id} className=''> <Link href={createSlug( '/shop/category/', - subCategory?.name, - subCategory?.id_level_2 + childCategory?.name, + childCategory?.id_level_3 )} - className='!text-red-500 font-semibold' + className='flex flex-row gap-2 border rounded group hover:border-red-500' > - Lihat Semua + <NextImage + src={ + childCategory.image + ? childCategory.image + : '/images/noimage.jpeg' + } + alt={childCategory.name} + className='p-2 ml-1' + width={40} + height={40} + /> + <div className='bagian-judul flex flex-col justify-center items-center gap-2 break-words line-clamp-2 group-hover:text-red-500'> + <h3 className='font-semibold line-clamp-2 group-hover:text-red-500 text-sm mr-2'> + {childCategory.name} + </h3> + </div> </Link> </div> - </div> - <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px] min-h-[240px] content-start'> - {subCategory.child_frontend_id_i.map( - (childCategory) => ( - <div key={childCategory.id} className=''> - <Link - href={createSlug( - '/shop/category/', - childCategory?.name, - childCategory?.id_level_3 - )} - className='flex flex-row gap-2 border rounded group hover:border-red-500' - > - <NextImage - src={ - childCategory.image - ? childCategory.image - : '/images/noimage.jpeg' - } - alt={childCategory.name} - className='p-2 ml-1' - width={40} - height={40} - /> - <div className='bagian-judul flex flex-col justify-center items-center gap-2 break-words line-clamp-2 group-hover:text-red-500'> - <h3 className='font-semibold line-clamp-2 group-hover:text-red-500 text-sm mr-2'> - {childCategory.name} - </h3> - </div> - </Link> - </div> - ) - )} - </div> - </div> + ) + )} </div> - </SwiperSlide> - ); - })} - </Swiper> - </div> - </Skeleton> - ); - })} + </div> + </div> + </SwiperSlide> + ))} + </Swiper> + </div> + </Skeleton> + ))} </div> ); }; diff --git a/src/lib/home/components/CategoryDynamicMobile.jsx b/src/lib/home/components/CategoryDynamicMobile.jsx index 55654b0e..67ae6f5f 100644 --- a/src/lib/home/components/CategoryDynamicMobile.jsx +++ b/src/lib/home/components/CategoryDynamicMobile.jsx @@ -9,71 +9,26 @@ import { fetchCategoryManagementVersion, } from '../api/categoryManagementApi'; -const saveToLocalStorage = (key, data, version) => { - const now = new Date(); - const item = { - value: data, - version: version, - lastFetchedTime: now.getTime(), - }; - localStorage.setItem(key, JSON.stringify(item)); -}; - -const getFromLocalStorage = (key) => { - const itemStr = localStorage.getItem(key); - if (!itemStr) return null; - - const item = JSON.parse(itemStr); - return item; -}; - -const getElapsedTime = (lastFetchedTime) => { - const now = new Date(); - return now.getTime() - lastFetchedTime; -}; - const CategoryDynamicMobile = () => { const [selectedCategory, setSelectedCategory] = useState({}); const [categoryManagement, setCategoryManagement] = useState([]); const [isLoading, setIsLoading] = useState(false); - const loadCategoryManagement = useCallback(async () => { - const cachedData = getFromLocalStorage('homepage_categoryDynamic'); - - if (cachedData) { - // Hitung selisih waktu antara saat ini dengan waktu terakhir data di-fetch - const elapsedTime = getElapsedTime(cachedData.lastFetchedTime); - - if (elapsedTime < 24 * 60 * 60 * 1000) { - setCategoryManagement(cachedData.value); - return; - } - } - - const latestVersion = await fetchCategoryManagementVersion(); - if (cachedData && cachedData.version === latestVersion) { - // perbarui waktu - saveToLocalStorage( - 'homepage_categoryDynamic', - cachedData.value, - latestVersion - ); - setCategoryManagement(cachedData.value); - } else { + useEffect(() => { + const fetchCategoryData = async () => { setIsLoading(true); - const items = await fetchCategoryManagementSolr(); + const res = await fetch('/api/category-management'); + const { data } = await res.json(); + if (data) { + setCategoryManagement(data); + } setIsLoading(false); + }; - saveToLocalStorage('homepage_categoryDynamic', items, latestVersion); - setCategoryManagement(items); - } + fetchCategoryData(); }, []); useEffect(() => { - loadCategoryManagement(); - }, [loadCategoryManagement]); - - useEffect(() => { if (categoryManagement?.length > 0) { const initialSelections = categoryManagement.reduce((acc, category) => { if (category.categories.length > 0) { diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx index eefced60..b7a30503 100644 --- a/src/lib/home/components/PreferredBrand.jsx +++ b/src/lib/home/components/PreferredBrand.jsx @@ -1,49 +1,50 @@ -import { Swiper, SwiperSlide } from 'swiper/react' -import { Navigation, Pagination, Autoplay } from 'swiper'; -import { useCallback, useEffect, useState } from 'react' -import usePreferredBrand from '../hooks/usePreferredBrand' -import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton' -import BrandCard from '@/lib/brand/components/BrandCard' -import useDevice from '@/core/hooks/useDevice' -import Link from '@/core/components/elements/Link/Link' -import axios from 'axios' +import { Swiper, SwiperSlide } from 'swiper/react'; +import { Navigation, Pagination, Autoplay } from 'swiper'; +import { useCallback, useEffect, useState } from 'react'; +import usePreferredBrand from '../hooks/usePreferredBrand'; +import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton'; +import BrandCard from '@/lib/brand/components/BrandCard'; +import useDevice from '@/core/hooks/useDevice'; +import Link from '@/core/components/elements/Link/Link'; +import axios from 'axios'; const PreferredBrand = () => { - let query = '' - let params = 'prioritas' - const [isLoading, setIsLoading] = useState(true) - const [startWith, setStartWith] = useState(null) - const [manufactures, setManufactures] = useState([]) + let query = ''; + let params = 'prioritas'; + const [isLoading, setIsLoading] = useState(true); + const [startWith, setStartWith] = useState(null); + const [manufactures, setManufactures] = useState([]); const loadBrand = useCallback(async () => { - setIsLoading(true) - const name = startWith ? `${startWith}*` : '' - const result = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?rows=20`) - - setIsLoading(false) - setManufactures((manufactures) => [...result.data]) - }, [startWith]) + setIsLoading(true); + const name = startWith ? `${startWith}*` : ''; + const result = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/preferredBrand?rows=20` + ); + setIsLoading(false); + setManufactures((manufactures) => [...result.data]); + }, [startWith]); const toggleStartWith = (alphabet) => { - setManufactures([]) + setManufactures([]); if (alphabet == startWith) { - setStartWith(null) - return + setStartWith(null); + return; } - setStartWith(alphabet) - } + setStartWith(alphabet); + }; useEffect(() => { - loadBrand() - }, []) + loadBrand(); + }, []); // const { preferredBrands } = usePreferredBrand(query) - const { isMobile, isDesktop } = useDevice() + const { isMobile, isDesktop } = useDevice(); const swiperBanner = { - modules:[Navigation, Pagination, Autoplay], + modules: [Navigation, Pagination, Autoplay], autoplay: { delay: 4000, - disableOnInteraction: false + disableOnInteraction: false, }, loop: true, className: 'h-[70px] md:h-[100px] w-full', @@ -53,13 +54,17 @@ const PreferredBrand = () => { dynamicBullets: true, dynamicMainBullets: isMobile ? 6 : 8, clickable: true, - } - } - const preferredBrandsData = manufactures ? manufactures.slice(0, 20) : [] + }, + }; + const preferredBrandsData = manufactures ? manufactures.slice(0, 20) : []; return ( <div className='px-4 sm:px-0'> <div className='flex justify-between items-center mb-4'> - <h1 className='font-semibold text-[14px] sm:text-h-lg'><Link href='/shop/brands' className='!text-black font-semibold'>Brand Pilihan</Link></h1> + <h1 className='font-semibold text-[14px] sm:text-h-lg'> + <Link href='/shop/brands' className='!text-black font-semibold'> + Brand Pilihan + </Link> + </h1> {isDesktop && ( <Link href='/shop/brands' className='!text-red-500 font-semibold'> Lihat Semua @@ -79,7 +84,7 @@ const PreferredBrand = () => { )} </div> </div> - ) -} + ); +}; -export default PreferredBrand
\ No newline at end of file +export default PreferredBrand; diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx index 7433e7f0..562fa138 100644 --- a/src/lib/home/components/PromotionProgram.jsx +++ b/src/lib/home/components/PromotionProgram.jsx @@ -10,32 +10,50 @@ const BannerSection = () => { const { isMobile, isDesktop } = useDevice(); const [data, setData] = useState(null); const [shouldFetch, setShouldFetch] = useState(false); - useEffect(() => { - const localData = localStorage.getItem('Homepage_promotionProgram'); - if (localData) { - setData(JSON.parse(localData)); - }else{ - setShouldFetch(true); - } - },[]) - - const getPromotionProgram = useQuery( - 'promotionProgram', - bannerApi({ type: 'banner-promotion' }),{ - enabled: shouldFetch, - onSuccess: (data) => { - if (data) { - localStorage.setItem('Homepage_promotionProgram', JSON.stringify(data)); - setData(data); - } + const fetchData = async () => { + const res = await fetch(`/api/hero-banner?type=banner-promotion`); + const { data } = await res.json(); + if (data) { + setData(data); } - } - ); + }; + + fetchData(); + }, []); + + // useEffect(() => { + // const localData = localStorage.getItem('Homepage_promotionProgram'); + // if (localData) { + // setData(JSON.parse(localData)); + // } else { + // setShouldFetch(true); + // } + // }, []); - const promotionProgram = data + // const getPromotionProgram = useQuery( + // 'promotionProgram', + // bannerApi({ type: 'banner-promotion' }), + // { + // enabled: shouldFetch, + // onSuccess: (data) => { + // if (data) { + // localStorage.setItem( + // 'Homepage_promotionProgram', + // JSON.stringify(data) + // ); + // setData(data); + // } + // }, + // } + // ); - if (getPromotionProgram?.isLoading && !data) { + const promotionProgram = data; + + // if (getPromotionProgram?.isLoading && !data) { + // return <BannerPromoSkeleton />; + // } + if (!data) { return <BannerPromoSkeleton />; } @@ -62,24 +80,22 @@ const BannerSection = () => { </Link> )} </div> - {isDesktop && - promotionProgram && - promotionProgram?.length > 0 && ( - <div className='grid grid-cols-3 sm:grid-cols-3 gap-4 rounded-md'> - {promotionProgram?.map((banner) => ( - <Link key={banner.id} href={banner.url}> - <Image - width={439} - height={150} - quality={85} - src={banner.image} - alt={banner.name} - className='h-auto w-full rounded hover:scale-105 transition duration-500 ease-in-out' - /> - </Link> - ))} - </div> - )} + {isDesktop && promotionProgram && promotionProgram?.length > 0 && ( + <div className='grid grid-cols-3 sm:grid-cols-3 gap-4 rounded-md'> + {promotionProgram?.map((banner) => ( + <Link key={banner.id} href={banner.url}> + <Image + width={439} + height={150} + quality={85} + src={banner.image} + alt={banner.name} + className='h-auto w-full rounded hover:scale-105 transition duration-500 ease-in-out' + /> + </Link> + ))} + </div> + )} {isMobile && ( <Swiper slidesPerView={1.1} spaceBetween={8} freeMode> diff --git a/src/lib/product/components/Product/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx index a4cc62dd..5dfd452b 100644 --- a/src/lib/product/components/Product/ProductDesktopVariant.jsx +++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx @@ -1,14 +1,12 @@ -import { Box, Skeleton, Tooltip } from '@chakra-ui/react'; +import { Box, Button, Skeleton, Tooltip } from '@chakra-ui/react'; import { HeartIcon } from '@heroicons/react/24/outline'; -import { Info } from 'lucide-react'; +import { Info, MessageCircleIcon, Share2Icon } from 'lucide-react'; import { useRouter } from 'next/router'; import { useCallback, useEffect, useRef, useState } from 'react'; import { toast } from 'react-hot-toast'; -import { MessageCircleIcon, Share2Icon } from 'lucide-react'; import AddToWishlist from '../../../../../src-migrate/modules/product-detail/components/AddToWishlist'; import { RWebShare } from 'react-web-share'; import LazyLoad from 'react-lazy-load'; -import { Button } from '@chakra-ui/react'; import { useProductCartContext } from '@/contexts/ProductCartContext'; import odooApi from '@/core/api/odooApi'; import Image from '@/core/components/elements/Image/Image'; @@ -21,11 +19,16 @@ import currencyFormat from '@/core/utils/currencyFormat'; import { createSlug } from '@/core/utils/slug'; import whatsappUrl from '@/core/utils/whatsappUrl'; import { getAuth } from '~/libs/auth'; -import SimilarBottom from '~/modules/product-detail/components/SimilarBottom'; + +import ImageNext from 'next/image'; import productSimilarApi from '../../api/productSimilarApi'; import ProductCard from '../ProductCard'; import ProductSimilar from '../ProductSimilar'; +import ProductPromoSection from '~/modules/product-promo/components/Section'; +import SimilarBottom from '~/modules/product-detail/components/SimilarBottom'; + const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST; + const ProductDesktopVariant = ({ product, wishlist, @@ -44,25 +47,20 @@ const ProductDesktopVariant = ({ const { setRefreshCart } = useProductCartContext(); - useEffect(() => { - const createdAskUrl = whatsappUrl({ - template: 'product', - payload: { - manufacture: product.manufacture.name, - productName: product.name, - url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath, - }, - fallbackUrl: router.asPath, - }); + const [quantityInput, setQuantityInput] = useState(1); - setAskAdminUrl(createdAskUrl); - }, [router.asPath, product.manufacture.name, product.name, setAskAdminUrl]); + const createdAskUrl = whatsappUrl({ + template: 'product', + payload: { + manufacture: product.manufacture.name, + productName: product.name, + url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath, + }, + fallbackUrl: router.asPath, + }); const getLowestPrice = useCallback(() => { const lowest = product.price; - /* const lowest = prices.reduce((lowest, price) => { - return price.priceDiscount < lowest.priceDiscount ? price : lowest - }, prices[0])*/ return lowest; }, [product]); @@ -95,7 +93,7 @@ const ProductDesktopVariant = ({ router.push(`/login?next=/shop/product/${slug}?srsltid=${srsltid}`); return; } - const quantity = variantQuantityRefs.current[product.id].value; + const quantity = quantityInput; if (!validQuantity(quantity)) return; updateItemCart({ productId: product.id, @@ -149,6 +147,45 @@ const ProductDesktopVariant = ({ router.push(`/shop/checkout?source=buy`); }; + const handleButton = async (variant) => { + const quantity = quantityInput; + let isLoggedIn = typeof auth === 'object'; + + if (!isLoggedIn) { + const currentUrl = encodeURIComponent(router.asPath); + await router.push(`/login?next=${currentUrl}`); + + // Tunggu login berhasil, misalnya dengan memantau perubahan status auth. + const authCheckInterval = setInterval(() => { + const newAuth = getAuth(); + if (typeof newAuth === 'object') { + isLoggedIn = true; + auth = newAuth; // Update nilai auth setelah login + clearInterval(authCheckInterval); + } + }, 500); // Periksa status login setiap 500ms + + await new Promise((resolve) => { + const checkLogin = setInterval(() => { + if (isLoggedIn) { + clearInterval(checkLogin); + resolve(null); + } + }, 500); + }); + } + if (!validQuantity(quantity)) return; + + updateItemCart({ + productId: variant, + quantity, + programLineId: null, + selected: true, + source: 'buy', + }); + router.push('/shop/quotation?source=buy'); + }; + const variantSectionRef = useRef(null); const goToVariantSection = () => { if (variantSectionRef.current) { @@ -204,87 +241,45 @@ const ProductDesktopVariant = ({ <Image src={product.image + '?variant=True'} alt={product.name} - className='h-[430px] object-contain object-center w-full border border-gray_r-4' + className='w-full h-[350px]' /> </div> - <div className='w-7/12 px-4 py-4'> + <div className='w-7/12 px-6'> <h1 className='text-title-md leading-10 font-medium'> {product?.name} </h1> <div className='mt-10'> - <div className='flex p-3'> - <div className='w-4/12 text-gray_r-12/70'>Nomor SKU</div> - <div className='w-8/12'>SKU-{product.id}</div> - </div> <div className='flex p-3 bg-gray_r-4'> - <div className='w-4/12 text-gray_r-12/70'>Part Number</div> - <div className='w-8/12'>{product.code || '-'}</div> + <div className='w-4/12 text-gray_r-12/70'>Item Code</div> + <div className='w-8/12'>{product.code}</div> </div> - <div className='flex p-3'> + <div className='flex p-3 items-center '> <div className='w-4/12 text-gray_r-12/70'>Manufacture</div> <div className='w-8/12'> - {product.manufacture?.name ? ( - <Link - href={createSlug( - '/shop/brands/', - product.manufacture?.name, - product.manufacture?.id - )} - > - {product.manufacture?.name} - </Link> - ) : ( - <div>-</div> - )} - </div> - </div> - - <div className='flex p-3 items-center bg-gray_r-4'> - <div className='w-4/12 text-gray_r-12/70'> - Persiapan Barang - </div> - <div className='w-8/12'> - {!product?.sla && <Skeleton width='20%' height='16px' />} - {product?.sla && ( - <Tooltip - placement='top' - label={`Masa Persiapan Barang ${product?.sla?.slaDate}`} - > - <Box className='w-fit flex items-center gap-x-2'> - {product?.sla?.slaDate} - <Info size={16} /> - </Box> - </Tooltip> - )} + <Link + href={createSlug( + '/shop/brands/', + product.manufacture.name, + product.manufacture.id.toString() + )} + > + {product?.manufacture.logo ? ( + <Image + width={100} + src={product.manufacture.logo} + alt={product.manufacture.name} + /> + ) : ( + <p className='font-bold text-red-500'> + {product.manufacture.name} + </p> + )} + </Link> </div> </div> - <div className='flex p-3'> - <div className='w-4/12 text-gray_r-12/70'>Stock</div> - <div className='w-8/12'> - {!product?.sla && <Skeleton width='10%' height='16px' />} - {product?.sla?.qty > 0 && <span>{product?.sla?.qty}</span>} - {product?.sla?.qty == 0 && ( - <a - href={whatsappUrl('product', { - name: product.name, - manufacture: product?.manufacture?.name, - url: createSlug( - '/shop/product/', - product.name, - product.id, - true - ), - })} - className='text-danger-500 font-medium' - > - Tanya Admin - </a> - )} - </div> - </div> - <div className='flex p-3 bg-gray_r-4'> + <div className='flex p-3 bg-gray_r-4 '> <div className='w-4/12 text-gray_r-12/70'>Berat Barang</div> <div className='w-8/12'> {product?.weight > 0 && <span>{product?.weight} KG</span>} @@ -306,58 +301,55 @@ const ProductDesktopVariant = ({ )} </div> </div> - </div> - <div className='h-6' /> - <div className='flex gap-x-5'> - <Button - as={Link} - href={askAdminUrl} - variant='link' - target='_blank' - colorScheme='gray' - leftIcon={<MessageCircleIcon size={18} />} - > - Ask Admin - </Button> - - <AddToWishlist productId={product.id} /> + <div className='flex p-3 items-center '> + <div className='w-4/12 text-gray_r-12/70'>Terjual</div> + <div className='w-8/12'>-</div> + </div> - <RWebShare - data={{ - text: 'Check out this product', - title: `${product.name} - Indoteknik.com`, - url: SELF_HOST + router.asPath, - }} - > - <Button - variant='link' - colorScheme='gray' - leftIcon={<Share2Icon size={18} />} - > - Share - </Button> - </RWebShare> + <div className='flex p-3 items-center bg-gray_r-4 '> + <div className='w-4/12 text-gray_r-12/70'> + Persiapan Barang + </div> + <div className='w-8/12'> + {!product?.sla && <Skeleton width='20%' height='16px' />} + {product?.sla && ( + <Tooltip + placement='top' + label={`Masa Persiapan Barang ${product?.sla?.slaDate}`} + > + <Box className='w-fit flex items-center gap-x-2'> + {product?.sla?.slaDate} + <Info size={16} /> + </Box> + </Tooltip> + )} + </div> + </div> </div> </div> - <div className='py-4 md:p-6 md:bg-gray-50 rounded-xl w-[99%]'> - <h2 className='text-h-md md:text-h-lg font-medium'> - Informasi Produk - </h2> - <div className='h-4' /> - <div - className='leading-relaxed text-gray-700' - dangerouslySetInnerHTML={{ - __html: - !product.parent.description || - product.parent.description == '<p><br></p>' - ? 'Belum ada deskripsi' - : product.parent.description, - }} - /> + <div className='p-4 md:p-6 w-full'> + <ProductPromoSection product={product} productId={product.id} /> + + <div className='p-4 md:p-6 md:bg-gray-50 rounded-xl'> + <h2 className='text-h-md md:text-h-lg font-medium'> + Informasi Produk + </h2> + <div className='h-4' /> + <div + className='leading-relaxed text-gray-700' + dangerouslySetInnerHTML={{ + __html: + !product.parent.description || + product.parent.description == '<p><br></p>' + ? 'Belum ada deskripsi' + : product.parent.description, + }} + /> + </div> </div> </div> - <div className='w-[25%]'> + <div className='w-[33%]'> {product?.isFlashsale > 0 && product?.price?.discountPercentage > 0 ? ( <> @@ -415,27 +407,137 @@ const ProductDesktopVariant = ({ )} </h3> )} - <div className='flex gap-x-3 mt-4'> - <input - type='number' - className='form-input w-16 py-2 text-center bg-gray_r-1' - ref={setVariantQuantityRef(product.id)} - defaultValue={1} - /> - <button - type='button' + <div className='flex justify-between items-center py-5 px-3'> + <div className='relative flex items-center'> + <button + type='button' + className='absolute left-0 px-2 py-1 h-full text-gray-500' + onClick={() => + setQuantityInput( + String(Math.max(1, Number(quantityInput) - 1)) + ) + } + > + - + </button> + <input + type='number' + id='quantity' + min={1} + value={quantityInput} + onChange={(e) => setQuantityInput(e.target.value)} + className=' w-24 h-10 text-center border border-gray-300 rounded focus:outline-none' + /> + <button + type='button' + className='absolute right-0 px-2 py-1 h-full text-gray-500' + onClick={() => + setQuantityInput(String(Number(quantityInput) + 1)) + } + > + + + </button> + </div> + <div> + <Skeleton + isLoaded={!isLoadingSLA} + h='21px' + // w={16} + className={ + product?.sla?.qty < 10 ? 'text-red-600 font-medium' : '' + } + > + Stock : {product?.sla?.qty}{' '} + </Skeleton> + </div> + <div> + {product?.sla?.qty > 0 && ( + <Link href='/panduan-pick-up-service' className='group'> + <Image + src='/images/PICKUP-NOW.png' + className='group-hover:scale-105 transition-transform duration-200 w-28' + alt='pickup now' + /> + </Link> + )} + </div> + </div> + <div className='flex gap-x-3'> + <Button onClick={() => handleAddToCart(product.id)} - className='flex-1 py-2 btn-yellow' + className='w-full' + colorScheme='yellow' > Keranjang - </button> - <button - type='button' + </Button> + <Button onClick={() => handleBuy(product.id)} - className='flex-1 py-2 btn-solid-red' + className='w-full' + colorScheme='red' > Beli - </button> + </Button> + </div> + <Button + onClick={() => handleButton(product.id)} + color={'red'} + colorScheme='white' + className='w-full border-2 p-2 gap-1 mt-2 hover:bg-slate-100 flex items-center' + > + <ImageNext + src='/images/writing.png' + alt='penawaran instan' + className='' + width={25} + height={25} + /> + Penawaran Harga Instan + </Button> + <div className='flex py-5'> + <div className='flex gap-x-5 items-center justify-center'> + <Button + as={Link} + href={createdAskUrl} + variant='link' + target='_blank' + colorScheme='gray' + leftIcon={<MessageCircleIcon size={18} />} + > + Ask Admin + </Button> + + <span>|</span> + + <button + className='flex items-center gap-x-1' + onClick={toggleWishlist} + > + {wishlist.data?.productTotal > 0 ? ( + <HeartIcon className='w-6 fill-danger-500 text-danger-500' /> + ) : ( + <HeartIcon className='w-6' /> + )} + Wishlist + </button> + + <span>|</span> + + <RWebShare + data={{ + text: 'Check out this product', + title: `${product.name} - Indoteknik.com`, + url: SELF_HOST + router.asPath, + }} + > + <Button + variant='link' + colorScheme='gray' + leftIcon={<Share2Icon size={18} />} + > + Share + </Button> + </RWebShare> + </div> </div> <div className='border border-gray_r-6 overflow-auto mt-4'> <div className='font-medium text-center p-4 bg-gray_r-1 border-b border-gray_r-6 sticky top-0 z-10'> diff --git a/src/lib/product/components/Product/ProductMobileVariant.jsx b/src/lib/product/components/Product/ProductMobileVariant.jsx index 790dbbe0..de5c3f10 100644 --- a/src/lib/product/components/Product/ProductMobileVariant.jsx +++ b/src/lib/product/components/Product/ProductMobileVariant.jsx @@ -1,10 +1,10 @@ -import { Skeleton } from '@chakra-ui/react'; +import { Button, Skeleton } from '@chakra-ui/react'; import { HeartIcon } from '@heroicons/react/24/outline'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { toast } from 'react-hot-toast'; import LazyLoad from 'react-lazy-load'; - +import ImageNext from 'next/image'; import odooApi from '@/core/api/odooApi'; import Divider from '@/core/components/elements/Divider/Divider'; import Image from '@/core/components/elements/Image/Image'; @@ -133,6 +133,20 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { router.push(`/shop/checkout?source=buy`); }; + const handleButton = (variant) => { + const quantity = quantityInput; + if (!validQuantity(quantity)) return; + + updateItemCart({ + productId: variant, + quantity, + programLineId: null, + selected: true, + source: 'buy', + }); + router.push('/shop/quotation?source=buy'); + }; + const productSimilarQuery = [ product?.name, `fq=-product_id_i:${product.id}`, @@ -154,44 +168,14 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { return ( <MobileView> - <Image - src={product.image + '?variant=True'} - alt={product.name} - className='h-72 object-contain object-center w-full border-b border-gray_r-4' - /> - - <div className='p-4'> - <div className='flex items-end mb-2'> - {product.manufacture?.name ? ( - <Link - href={createSlug( - '/shop/brands/', - product.manufacture?.name, - product.manufacture?.id - )} - > - {product.manufacture?.name} - </Link> - ) : ( - <div>-</div> - )} - <button type='button' className='ml-auto' onClick={toggleWishlist}> - {wishlist.data?.productTotal > 0 ? ( - <HeartIcon className='w-6 fill-danger-500 text-danger-500' /> - ) : ( - <HeartIcon className='w-6' /> - )} - </button> - </div> - <h1 className='font-medium text-h-lg leading-8 md:text-title-md md:leading-10 mb-3'> - {activeVariant?.name} - </h1> - + <div + className={`px-4 block md:sticky md:top-[150px] md:py-6 fixed bottom-0 left-0 right-0 bg-white p-2 z-10 pb-6 pt-6 rounded-lg shadow-[rgba(0,0,4,0.1)_0px_-4px_4px_0px] `} + > {activeVariant.isFlashSale && activeVariant?.price?.discountPercentage > 0 ? ( <> <div className='flex gap-x-1 items-center'> - <div className='badge-solid-red'> + <div className='bg-danger-500 px-2 py-1.5 rounded text-white text-caption-2'> {activeVariant?.price?.discountPercentage}% </div> <div className='text-gray_r-11 line-through text-caption-1'> @@ -209,7 +193,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { </div> </> ) : ( - <h3 className='text-danger-500 font-semibold mt-1'> + <div className='text-danger-500 font-semibold mt-1 text-3xl'> {activeVariant?.price?.price > 0 ? ( <> {currencyFormat(activeVariant?.price?.price)} @@ -239,39 +223,84 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { </a> </span> )} - </h3> + </div> )} + <div className=''> + <div className='mt-4 mb-2'>Jumlah</div> + <div className='flex gap-x-3'> + <div className='w-2/12'> + <input + name='quantity' + type='number' + className='form-input' + value={quantity} + onChange={(e) => setQuantity(e.target.value)} + /> + </div> + <button + type='button' + className='btn-yellow flex-1' + onClick={handleClickCart} + > + Keranjang + </button> + <button + type='button' + className='btn-solid-red flex-1' + onClick={handleClickBuy} + > + Beli + </button> + </div> + <Button + onClick={() => handleButton(product.id)} + color={'red'} + colorScheme='white' + className='w-full border-2 p-2 gap-1 mt-2 hover:bg-slate-100 flex items-center' + > + <ImageNext + src='/images/writing.png' + alt='penawaran instan' + className='' + width={25} + height={25} + /> + Penawaran Harga Instan + </Button> + </div> </div> - - <Divider /> + <Image + src={product.image + '?variant=True'} + alt={product.name} + className='h-72 object-contain object-center w-full border-b border-gray_r-4' + /> <div className='p-4'> - <div className='mt-4 mb-2'>Jumlah</div> - <div className='flex gap-x-3'> - <div className='w-2/12'> - <input - name='quantity' - type='number' - className='form-input' - value={quantity} - onChange={(e) => setQuantity(e.target.value)} - /> - </div> - <button - type='button' - className='btn-yellow flex-1' - onClick={handleClickCart} - > - Keranjang - </button> - <button - type='button' - className='btn-solid-red flex-1' - onClick={handleClickBuy} - > - Beli + <div className='flex items-end mb-2'> + {product.manufacture?.name ? ( + <Link + href={createSlug( + '/shop/brands/', + product.manufacture?.name, + product.manufacture?.id + )} + > + {product.manufacture?.name} + </Link> + ) : ( + <div>-</div> + )} + <button type='button' className='ml-auto' onClick={toggleWishlist}> + {wishlist.data?.productTotal > 0 ? ( + <HeartIcon className='w-6 fill-danger-500 text-danger-500' /> + ) : ( + <HeartIcon className='w-6' /> + )} </button> </div> + <h1 className='font-medium text-h-lg leading-8 md:text-title-md md:leading-10 mb-3'> + {activeVariant?.name} + </h1> </div> <Divider /> diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index d3b50302..3e6a6913 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -10,12 +10,13 @@ import { sellingProductFormat } from '@/core/utils/formatValue'; import { createSlug } from '@/core/utils/slug'; import whatsappUrl from '@/core/utils/whatsappUrl'; import useUtmSource from '~/hooks/useUtmSource'; +import useDevice from '@/core/hooks/useDevice'; const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { const router = useRouter(); const utmSource = useUtmSource(); const [discount, setDiscount] = useState(0); - + const { isDesktop, isMobile } = useDevice(); let voucherPastiHemat = 0; voucherPastiHemat = product?.newVoucherPastiHemat[0]; @@ -26,9 +27,13 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { }); const image = useMemo(() => { - if (product.image) return product.image + '?ratio=square'; - return '/images/noimage.jpeg'; - }, [product.image]); + if (!isDesktop && product.image_mobile) { + return product.image_mobile + '?ratio=square'; + } else { + if (product.image) return product.image + '?ratio=square'; + return '/images/noimage.jpeg'; + } + }, [product.image, product.image_mobile]); const URL = { product: @@ -143,7 +148,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { <div className='p-2 sm:p-3 pb-3 text-caption-2 sm:text-body-2 leading-5'> <div className='flex justify-between '> {product?.manufacture?.name ? ( - <Link href={URL.manufacture} className='mb-1 mt-1'> + <Link href={URL.manufacture} className='mb-1 mt-1 truncate'> {product.manufacture.name} </Link> ) : ( diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx index cf0ad41f..5a2f63a5 100644 --- a/src/lib/quotation/components/Quotation.jsx +++ b/src/lib/quotation/components/Quotation.jsx @@ -39,9 +39,12 @@ const { getProductsCheckout } = require('@/lib/checkout/api/checkoutApi'); const Quotation = () => { const router = useRouter(); const auth = useAuth(); + const query = router.query.source ?? null; const { data: cartCheckout } = useQuery('cartCheckout', () => - getProductsCheckout() + getProductsCheckout({ + source: query, + }) ); const { setRefreshCart } = useProductCartContext(); diff --git a/src/lib/shipment/components/Shipments.jsx b/src/lib/shipment/components/Shipments.jsx index 115bbd3a..20dbb013 100644 --- a/src/lib/shipment/components/Shipments.jsx +++ b/src/lib/shipment/components/Shipments.jsx @@ -1,62 +1,83 @@ -import DesktopView from '@/core/components/views/DesktopView' -import MobileView from '@/core/components/views/MobileView' -import Menu from '@/lib/auth/components/Menu' -import { EllipsisVerticalIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline' -import ImageNext from 'next/image' -import { useRouter } from 'next/router' -import { useQuery } from 'react-query' -import _, { forEach } from 'lodash-contrib' -import Spinner from '@/core/components/elements/Spinner/Spinner' -import Manifest from '@/lib/treckingAwb/component/Manifest' -import { useState } from 'react' -import Pagination from '@/core/components/elements/Pagination/Pagination' -import Link from 'next/link' -import TransactionStatusBadge from '@/lib/transaction/components/TransactionStatusBadge' +import DesktopView from '@/core/components/views/DesktopView'; +import MobileView from '@/core/components/views/MobileView'; +import Menu from '@/lib/auth/components/Menu'; +import { + EllipsisVerticalIcon, + MagnifyingGlassIcon, +} from '@heroicons/react/24/outline'; +import ImageNext from 'next/image'; +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { useQuery } from 'react-query'; +import _, { forEach } from 'lodash-contrib'; +import Spinner from '@/core/components/elements/Spinner/Spinner'; +import Manifest from '@/lib/treckingAwb/component/Manifest'; +import { useState } from 'react'; +import Pagination from '@/core/components/elements/Pagination/Pagination'; +import Link from 'next/link'; +import TransactionStatusBadge from '@/lib/transaction/components/TransactionStatusBadge'; -const { listShipments } = require('../api/listShipment') +const { listShipments } = require('../api/listShipment'); const Shipments = () => { - const router = useRouter() - const { q = '', page = 1 } = router.query - const [paramStatus, setParamStatus] = useState(null) - - const limit = 15 + const router = useRouter(); + const { q = '', page = 1, status = null } = router.query; + const [paramStatus, setParamStatus] = useState(status); + const limit = 15; const query = { q: q, status: paramStatus, offset: (page - 1) * limit, - limit - } - const [inputQuery, setInputQuery] = useState(q) - const queryString = _.toQuery(query) + limit, + }; + const [inputQuery, setInputQuery] = useState(q); + const queryString = _.toQuery(query); const { data: shipments } = useQuery('shipments' + queryString, () => listShipments({ query: queryString }) - ) - const [idAWB, setIdAWB] = useState(null) + ); + const [idAWB, setIdAWB] = useState(null); - const pageCount = Math.ceil(shipments?.pickingTotal / limit) - let pageQuery = _.omit(query, ['limit', 'offset', 'context']) - pageQuery = _.pickBy(pageQuery, _.identity) - pageQuery = _.toQuery(pageQuery) + const pageCount = Math.ceil(shipments?.pickingTotal / limit); + let pageQuery = _.omit(query, ['limit', 'offset', 'context']); + pageQuery = _.pickBy(pageQuery, _.identity); + pageQuery = _.toQuery(pageQuery); const closePopup = () => { - setIdAWB(null) - } + setIdAWB(null); + }; const handleSubmit = async (e) => { - e.preventDefault() - router.push(`${router.pathname}?q=${inputQuery}`) - } + e.preventDefault(); + router.push(`${router.pathname}?q=${inputQuery}`); + }; const filterStatus = async (status) => { if (status === paramStatus) { - setParamStatus(null) + setParamStatus(null); } else { - setParamStatus(status) + setParamStatus(status); } - } + }; + + useEffect(() => { + const resetQuery = () => { + const newQuery = { + status: paramStatus || undefined, + q: '', + page: 1, + }; + router.push({ + pathname: router.pathname, + query: newQuery, + }); + }; + + if (paramStatus !== status) { + resetQuery(); + } + }, [paramStatus]); return ( <> <MobileView> @@ -84,7 +105,10 @@ const Shipments = () => { </form> {shipments?.pickings.map((shipment) => ( - <div className='p-4 shadow border border-gray_r-3 rounded-md' key={shipment.id}> + <div + className='p-4 shadow border border-gray_r-3 rounded-md' + key={shipment.id} + > <div className='flex justify-between items-center mb-3'> <div className='text-caption-2 text-gray_r-11'> <p> @@ -93,7 +117,9 @@ const Shipments = () => { {shipment.carrierName || '-'} </span> </p> - <p className='mt-2'>No. Resi : {shipment.trackingNumber || '-'}</p> + <p className='mt-2'> + No. Resi : {shipment.trackingNumber || '-'} + </p> </div> <div className='flex justify-between'> {shipment?.status === 'completed' && ( @@ -116,11 +142,17 @@ const Shipments = () => { <hr /> <div className='flex justify-between mt-2 items-center mb-5'> <div> - <span className='text-caption-2 text-gray_r-11'>No. Transaksi</span> + <span className='text-caption-2 text-gray_r-11'> + No. Transaksi + </span> <Link href={`/my/transactions/${shipment.saleOrder.id}`}> - <h2 className='text-danger-500 mt-1 mb-2'>{shipment.saleOrder.name}</h2> + <h2 className='text-danger-500 mt-1 mb-2'> + {shipment.saleOrder.name} + </h2> </Link> - <span className='text-caption-2 text-gray_r-11'>{shipment.date}</span> + <span className='text-caption-2 text-gray_r-11'> + {shipment.date} + </span> </div> <div> <button @@ -136,7 +168,11 @@ const Shipments = () => { onClick={() => setIdAWB(shipment.id)} className='flex items-center mt-1 gap-x-1 min-w-full' > - <ImageNext src={`/images/BOX_DELIVERY_GREEN.svg`} width={20} height={20} /> + <ImageNext + src={`/images/BOX_DELIVERY_GREEN.svg`} + width={20} + height={20} + /> <p className='text-sm text-green-700 truncate'> {shipment.lastManifest.description} </p> @@ -148,7 +184,7 @@ const Shipments = () => { <Pagination pageCount={pageCount} currentPage={parseInt(page)} - url={router.pathname + pageQuery} + url={`${router.pathname}${pageQuery ? '?' + pageQuery : ''}`} className='mt-2 mb-2' /> </div> @@ -176,7 +212,8 @@ const Shipments = () => { <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' /> </svg> <div> - Lacak pengiriman untuk setiap transaksi anda semakin mudah di Indoteknik.com + Lacak pengiriman untuk setiap transaksi anda semakin mudah di + Indoteknik.com </div> </div> <div className='flex justify-between gap-x-5'> @@ -190,7 +227,9 @@ const Shipments = () => { </div> <div className='p-4 bg-white border border-gray_r-6 rounded'> <div className='flex mb-6 items-center justify-between'> - <h1 className='text-title-sm font-semibold'>Detail Pengiriman</h1> + <h1 className='text-title-sm font-semibold'> + Detail Pengiriman + </h1> <form className='flex gap-x-2' onSubmit={handleSubmit}> <input type='text' @@ -199,7 +238,10 @@ const Shipments = () => { value={inputQuery} onChange={(e) => setInputQuery(e.target.value)} /> - <button className='btn-light bg-transparent px-3' type='submit'> + <button + className='btn-light bg-transparent px-3' + type='submit' + > <MagnifyingGlassIcon className='w-6' /> </button> </form> @@ -254,7 +296,7 @@ const Shipments = () => { <Pagination pageCount={pageCount} currentPage={parseInt(page)} - url={router.pathname + pageQuery} + url={`${router.pathname}${pageQuery ? '?' + pageQuery : ''}`} className='mt-2 mb-2' /> </div> @@ -263,16 +305,16 @@ const Shipments = () => { <Manifest idAWB={idAWB} closePopup={closePopup} /> </DesktopView> </> - ) -} + ); +}; const CardStatus = ({ device, paramStatus, shipments, filterStatus }) => { - const status = [`pending`, `shipment`, `completed`] + const status = [`pending`, `shipment`, `completed`]; return ( <> {status.map((value) => { - const statusData = getStatusLabel(device, value, shipments) + const statusData = getStatusLabel(device, value, shipments); if (device === 'desktop') { return ( <div @@ -282,13 +324,15 @@ const CardStatus = ({ device, paramStatus, shipments, filterStatus }) => { }`} onClick={() => filterStatus(value)} > - <h2 className='mb-2 text-lg font-bold tracking-tight'>{statusData.label}</h2> + <h2 className='mb-2 text-lg font-bold tracking-tight'> + {statusData.label} + </h2> {statusData.image} <h1 className='text-xl font-bold'> {statusData.shipCount} <span className='text-sm'>Pesanan</span> </h1> </div> - ) + ); } else { return ( <div @@ -305,15 +349,15 @@ const CardStatus = ({ device, paramStatus, shipments, filterStatus }) => { <span className='truncate'>{statusData.shipCount}</span> {'>'} </h1> </div> - ) + ); } })} </> - ) -} + ); +}; const getStatusLabel = (device, status, shipments) => { - let images = null + let images = null; switch (status) { case 'pending': if (device === 'desktop') { @@ -328,40 +372,48 @@ const getStatusLabel = (device, status, shipments) => { /> </div> </div> - ) + ); } else { images = ( <div> <ImageNext src='/images/BOX(1).svg' width={15} height={20} /> </div> - ) + ); } return { label: 'Pending', shipCount: shipments?.summary?.pendingCount, - image: images - } + image: images, + }; case 'shipment': if (device === 'desktop') { images = ( <div className='bg-yellow-100 border border-yellow-200 rounded-sm p-1 w-20 mb-2'> <div> - <ImageNext src='/images/BOX_DELIVER_(1).svg' width={30} height={20} /> + <ImageNext + src='/images/BOX_DELIVER_(1).svg' + width={30} + height={20} + /> </div> </div> - ) + ); } else { images = ( <div> - <ImageNext src='/images/BOX_DELIVER_(1).svg' width={18} height={20} /> + <ImageNext + src='/images/BOX_DELIVER_(1).svg' + width={18} + height={20} + /> </div> - ) + ); } return { label: 'Pengiriman', shipCount: shipments?.summary?.shipmentCount, - image: images - } + image: images, + }; case 'completed': if (device === 'desktop') { images = ( @@ -375,22 +427,22 @@ const getStatusLabel = (device, status, shipments) => { /> </div> </div> - ) + ); } else { images = ( <div> <ImageNext src='/images/open-box(1).svg' width={16} height={20} /> </div> - ) + ); } return { label: 'Pesanan Tiba', shipCount: shipments?.summary?.completedCount, - image: images - } + image: images, + }; default: - return 'Status Tidak Dikenal' + return 'Status Tidak Dikenal'; } -} +}; -export default Shipments +export default Shipments; diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx index 6277e3e0..b1e92f1b 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -886,6 +886,10 @@ const Transaction = ({ id }) => { ? `| ${product?.attributes.join(', ')}` : ''} </div> + <div className='text-[10px] text-red-500 italic mt-2'> + {product.availableQuantity} barang ini bisa di + pickup maksimal pukul 16.00 + </div> </div> </td> {/* <td> diff --git a/src/lib/treckingAwb/component/Manifest.jsx b/src/lib/treckingAwb/component/Manifest.jsx index fbc95702..02d0bc7a 100644 --- a/src/lib/treckingAwb/component/Manifest.jsx +++ b/src/lib/treckingAwb/component/Manifest.jsx @@ -1,16 +1,16 @@ -import odooApi from '@/core/api/odooApi' -import BottomPopup from '@/core/components/elements/Popup/BottomPopup' -import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner' -import { getAuth } from '@/core/utils/auth' -import { useEffect, useState } from 'react' -import { toast } from 'react-hot-toast' -import ImageNext from 'next/image' -import { list } from 'postcss' +import odooApi from '@/core/api/odooApi'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner'; +import { getAuth } from '@/core/utils/auth'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-hot-toast'; +import ImageNext from 'next/image'; +import { list } from 'postcss'; const Manifest = ({ idAWB, closePopup }) => { - const [manifests, setManifests] = useState(null) - const [isLoading, setIsLoading] = useState(false) - + const [manifests, setManifests] = useState(null); + const [isLoading, setIsLoading] = useState(false); + console.log('manifests', manifests); const formatCustomDate = (date) => { const months = [ 'Jan', @@ -24,61 +24,60 @@ const Manifest = ({ idAWB, closePopup }) => { 'Sep', 'Oct', 'Nov', - 'Dec' - ] + 'Dec', + ]; - const parts = date.split(' ') // Pisahkan tanggal dan waktu - const [datePart, timePart] = parts - const [yyyy, mm, dd] = datePart.split('-') - const [hh, min] = timePart.split(':') + const parts = date.split(' '); // Pisahkan tanggal dan waktu + const [datePart, timePart] = parts; + const [yyyy, mm, dd] = datePart.split('-'); + const [hh, min] = timePart.split(':'); - const monthAbbreviation = months[parseInt(mm, 10) - 1] + const monthAbbreviation = months[parseInt(mm, 10) - 1]; - return `${dd} ${monthAbbreviation} ${hh}:${min}` - } + return `${dd} ${monthAbbreviation} ${hh}:${min}`; + }; const getManifest = async () => { - setIsLoading(true) - const auth = getAuth() - let list - if(auth){ + setIsLoading(true); + const auth = getAuth(); + let list; + if (auth) { list = await odooApi( 'GET', `/api/v1/partner/${auth.partnerId}/stock-picking/${idAWB}/tracking` - ) - }else{ - list = await odooApi( - 'GET', - `/api/v1/stock-picking/${idAWB}/tracking` - ) + ); + } else { + list = await odooApi('GET', `/api/v1/stock-picking/${idAWB}/tracking`); } - setManifests(list) - setIsLoading(false) - } + setManifests(list); + setIsLoading(false); + }; useEffect(() => { if (idAWB) { - getManifest() + getManifest(); } else { - setManifests(null) + setManifests(null); } - }, [idAWB]) + }, [idAWB]); - const [copied, setCopied] = useState(false) + const [copied, setCopied] = useState(false); const handleCopyClick = () => { - const textToCopy = manifests?.waybillNumber - navigator.clipboard.writeText(textToCopy) - setCopied(true) - toast.success('No Resi Berhasil di Copy') - setTimeout(() => setCopied(false), 2000) // Reset copied state after 2 seconds - } + const textToCopy = manifests?.waybillNumber; + navigator.clipboard.writeText(textToCopy); + setCopied(true); + toast.success('No Resi Berhasil di Copy'); + setTimeout(() => setCopied(false), 2000); // Reset copied state after 2 seconds + }; return ( <> {isLoading && ( <BottomPopup active={true} close=''> - <div className='leading-7 text-gray_r-12/80 flex justify-center'>Mohon Tunggu</div> + <div className='leading-7 text-gray_r-12/80 flex justify-center'> + Mohon Tunggu + </div> <div className='container flex justify-center my-4'> <LogoSpinner width={48} height={48} /> </div> @@ -111,11 +110,14 @@ const Manifest = ({ idAWB, closePopup }) => { </div> <div className=''> <h1 className='text-body-1'> - Estimasi tiba pada <span className='text-gray_r-11 text-sm'>({manifests?.eta})</span> + Estimasi tiba pada{' '} + <span className='text-gray_r-11 text-sm'>({manifests?.eta})</span> </h1> <h1 className='text-sm mt-2 mb-3'> Dikirim Menggunakan{' '} - <span className='text-red-500 font-semibold'>{manifests?.deliveryOrder.carrier}</span> + <span className='text-red-500 font-semibold'> + {manifests?.deliveryOrder.carrier} + </span> </h1> {manifests?.waybillNumber && ( <div className='flex justify-between items-center'> @@ -154,10 +156,16 @@ const Manifest = ({ idAWB, closePopup }) => { {manifests.delivered == true && index == 0 ? ( <div class={`absolute w-6 h-6 rounded-full mt-1.5 -left-3 border ${ - index == 0 ? 'bg-green-100 border-green-100' : 'bg-gray_r-7 border-white' + index == 0 + ? 'bg-green-100 border-green-100' + : 'bg-gray_r-7 border-white' }`} > - <ImageNext src='/images/open-box(1).svg' width={30} height={20} /> + <ImageNext + src='/images/open-box(1).svg' + width={30} + height={20} + /> </div> ) : ( <div @@ -167,7 +175,9 @@ const Manifest = ({ idAWB, closePopup }) => { {manifests.delivered != true && ( <div class={`absolute w-3 h-3 rounded-full mt-1.5 -left-1.5 border ${ - index == 0 ? 'bg-green-600 border-green-600' : 'bg-gray_r-7 border-white' + index == 0 + ? 'bg-green-600 border-green-600' + : 'bg-gray_r-7 border-white' } `} /> )} @@ -176,9 +186,15 @@ const Manifest = ({ idAWB, closePopup }) => { {formatCustomDate(manifest.datetime)} </time> {manifests.delivered == true && index == 0 && ( - <p class={`leading-6 font-semibold text-sm text-green-600 `}>Sudah Sampai</p> + <p + class={`leading-6 font-semibold text-sm text-green-600 `} + > + Sudah Sampai + </p> )} - <p class={`leading-6 text-[12px] text-gray_r-11`}>{manifest.description}</p> + <p class={`leading-6 text-[12px] text-gray_r-11`}> + {manifest.description} + </p> </li> </> ))} @@ -187,7 +203,7 @@ const Manifest = ({ idAWB, closePopup }) => { </BottomPopup> )} </> - ) -} + ); +}; -export default Manifest +export default Manifest; diff --git a/src/lib/variant/components/VariantCard.jsx b/src/lib/variant/components/VariantCard.jsx index 68cdf54f..08b7a97e 100644 --- a/src/lib/variant/components/VariantCard.jsx +++ b/src/lib/variant/components/VariantCard.jsx @@ -103,30 +103,42 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { </div> </div> </div> - </div> <div className='w-8/12 flex flex-col'> - <p className='product-card__title wrap-line-ellipsis-2'>{product.parent.name}</p> + <p className='product-card__title wrap-line-ellipsis-2'> + {product.parent.name} + </p> <p className='text-caption-2 text-gray_r-11 mt-1'> {product.code || '-'} - {product.attributes.length > 0 ? ` ・ ${product.attributes.join(', ')}` : ''} + {product.attributes.length > 0 + ? ` ・ ${product.attributes.join(', ')}` + : ''} </p> <p className='text-caption-2 text-gray_r-11 mt-1'> Berat Item : {product?.weight} Kg x {product?.quantity} Barang </p> + <p className='text-[10px] text-red-500 italic mt-2'> + {product.availableQuantity} barang ini bisa di pickup maksimal pukul + 16.00 + </p> <div className='flex flex-wrap gap-x-1 items-center mt-auto'> {product.hasFlashsale && ( <> <p className='text-caption-2 text-gray_r-11 line-through'> {currencyFormat(product.price.price)} </p> - <span className='badge-red'>{product.price.discountPercentage}%</span> + <span className='badge-red'> + {product.price.discountPercentage}% + </span> </> )} </div> <p className='text-caption-2 text-gray_r-11 mt-1'> {product.price.priceDiscount > 0 - ? currencyFormat(product.price.priceDiscount) + ' × ' + product.quantity + ' Barang' + ? currencyFormat(product.price.priceDiscount) + + ' × ' + + product.quantity + + ' Barang' : ''} </p> <p className='text-caption-2 text-gray_r-12 font-bold mt-2'> diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index bcb41dd6..f52aa5f7 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -85,7 +85,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) { return ( <SessionProvider session={session}> <ScrollToTop /> - + <AnimatePresence> {animateLoader && ( <motion.div diff --git a/src/pages/_document.jsx b/src/pages/_document.jsx index 6af6294f..4b67c3f9 100644 --- a/src/pages/_document.jsx +++ b/src/pages/_document.jsx @@ -115,6 +115,19 @@ export default function MyDocument() { }} /> + <Script + async + id='gtag-config' + strategy='afterInteractive' + dangerouslySetInnerHTML={{ + __html: ` + gtag('config', 'AW-954540379/fCU8CI3Y8OoZENvClMcD', { + 'phone_conversion_number': '(021) 29338828' + }); + `, + }} + /> + {/* <Script id='tawk-script-tag' strategy='afterInteractive' diff --git a/src/pages/api/banner-section.js b/src/pages/api/banner-section.js new file mode 100644 index 00000000..7d7040c0 --- /dev/null +++ b/src/pages/api/banner-section.js @@ -0,0 +1,44 @@ +import odooApi from '@/core/api/odooApi'; +import { createClient } from 'redis'; + +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + try { + await connectRedis(); + const cacheKey = 'hero-banner'; + // await client.del(cacheKey); + let cachedData = await client.get(cacheKey); + + if (cachedData) { + const data = JSON.parse(cachedData); + return res.status(200).json({ data }); + } else { + const dataBannerSections = await odooApi( + 'GET', + '/api/v1/banner?type=home-banner' + ); + + // Simpan hasil fetch ke Redis dengan masa kadaluarsa 3 hari (259200 detik) + await client.set( + cacheKey, + JSON.stringify(dataBannerSections), + 'EX', + 259200 + ); + + return res.status(200).json({ data: dataBannerSections }); + } + } catch (error) { + console.error('Error interacting with Redis or fetching data:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } +} diff --git a/src/pages/api/category-management.js b/src/pages/api/category-management.js new file mode 100644 index 00000000..f05d8644 --- /dev/null +++ b/src/pages/api/category-management.js @@ -0,0 +1,85 @@ +import { createClient } from 'redis'; +// import { fetchCategoryManagementSolr } from '../../lib/home/api/categoryManagementApi'; +const client = createClient(); +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + try { + await connectRedis(); + // await client.del('homepage_categoryDynamic'); + + let cachedData; + if (req.method === 'GET') { + cachedData = await client.get('homepage_categoryDynamic'); + + if (!cachedData) { + const items = await fetchCategoryManagementSolr(); + await client.set( + 'homepage_categoryDynamic', + JSON.stringify(items), + 'EX', + 259200 // Expiry 3 hari + ); + cachedData = await client.get('homepage_categoryDynamic'); + } + const data = cachedData ? JSON.parse(cachedData) : null; + res.status(200).json({ data }); + } else { + res.setHeader('Allow', ['GET']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } + } catch (error) { + console.error('Error interacting with Redis:', error); + res.status(500).json({ error: 'Error interacting with Redis' }); + } +} + +const fetchCategoryManagementSolr = async () => { + let sort = 'sort=sequence_i asc'; + try { + const response = await fetch( + `http://34.101.189.218:8983/solr/category_management/query?q=*:*&q.op=OR&indent=true&${sort}&&rows=20` + ); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + const promotions = await map(data.response.docs); + return promotions; + } catch (error) { + console.error('Error fetching promotion data:', error); + return []; + } +}; +const map = async (promotions) => { + return promotions.map((promotion) => { + let parsedCategories = promotion.categories.map((category) => { + // Parse string JSON utama + let parsedCategory = JSON.parse(category); + + // Parse setiap elemen di child_frontend_id_i jika ada + if (parsedCategory.child_frontend_id_i) { + parsedCategory.child_frontend_id_i = + parsedCategory.child_frontend_id_i.map((child) => JSON.parse(child)); + } + + return parsedCategory; + }); + let productMapped = { + id: promotion.id, + name: promotion.name_s, + image: promotion.image_s, + sequence: promotion.sequence_i, + numFound: promotion.numFound_i, + categories: parsedCategories, + category_id: promotion.category_id_i, + }; + return productMapped; + }); +}; diff --git a/src/pages/api/flashsale-header.js b/src/pages/api/flashsale-header.js new file mode 100644 index 00000000..31f8efdd --- /dev/null +++ b/src/pages/api/flashsale-header.js @@ -0,0 +1,40 @@ +import odooApi from '@/core/api/odooApi'; +import { createClient } from 'redis'; + +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + try { + await connectRedis(); + const cacheKey = `flashsale_header`; + // await client.del(cacheKey); + let cachedData = await client.get(cacheKey); + + if (cachedData) { + const data = JSON.parse(cachedData); + return res.status(200).json({ data }); + } else { + const flashSale = await odooApi('GET', `/api/v1/flashsale/header`); + + await client.set( + cacheKey, + JSON.stringify(flashSale), + 'EX', + flashSale.duration + ); + cachedData = await client.get(cacheKey); + return res.status(200).json({ data: cachedData }); + } + } catch (error) { + console.error('Error interacting with Redis or fetching data:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } +} diff --git a/src/pages/api/hero-banner.js b/src/pages/api/hero-banner.js new file mode 100644 index 00000000..7a348cfa --- /dev/null +++ b/src/pages/api/hero-banner.js @@ -0,0 +1,45 @@ +import odooApi from '@/core/api/odooApi'; +import { createClient } from 'redis'; + +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + const { type } = req.query; + try { + await connectRedis(); + const cacheKey = `homepage_bannerSection_${type}`; + // await client.del(cacheKey); + let cachedData = await client.get(cacheKey); + + if (cachedData) { + const data = JSON.parse(cachedData); + return res.status(200).json({ data }); + } else { + const dataBannerSections = await odooApi( + 'GET', + `/api/v1/banner?type=${type}` + ); + + // Simpan hasil fetch ke Redis dengan masa kadaluarsa 3 hari (259200 detik) + await client.set( + cacheKey, + JSON.stringify(dataBannerSections), + 'EX', + 259200 + ); + cachedData = await client.get(cacheKey); + return res.status(200).json({ data: cachedData }); + } + } catch (error) { + console.error('Error interacting with Redis or fetching data:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } +} diff --git a/src/pages/api/page-content.js b/src/pages/api/page-content.js new file mode 100644 index 00000000..a6514505 --- /dev/null +++ b/src/pages/api/page-content.js @@ -0,0 +1,43 @@ +import { createClient } from 'redis'; +import { getPageContent } from '~/services/pageContent'; +// import { fetchCategoryManagementSolr } from '../../lib/home/api/categoryManagementApi'; +const client = createClient(); +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + const { path } = req.query; + try { + await connectRedis(); + // await client.del('onbording-popup'); + + let cachedData; + if (req.method === 'GET') { + cachedData = await client.get(`page-content:${path}`); + + if (!cachedData) { + const items = await getPageContent({ path }); + await client.set( + `page-content:${path}`, + JSON.stringify(items), + 'EX', + 604800 // Expiry 1 minggu + ); + cachedData = await client.get(`page-content:${path}`); + } + const data = cachedData ? JSON.parse(cachedData) : null; + res.status(200).json({ data }); + } else { + res.setHeader('Allow', ['GET']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } + } catch (error) { + console.error('Error interacting with Redis:', error); + res.status(500).json({ error: 'Error interacting with Redis' }); + } +} diff --git a/src/pages/api/search-flashsale.js b/src/pages/api/search-flashsale.js new file mode 100644 index 00000000..d9e56c83 --- /dev/null +++ b/src/pages/api/search-flashsale.js @@ -0,0 +1,45 @@ +import odooApi from '@/core/api/odooApi'; +import { createClient } from 'redis'; +import _ from 'lodash-contrib'; +import axios from 'axios'; + +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + const { query, operation, duration } = req.query; + try { + await connectRedis(); + const cacheKey = `flashsale_product`; + // await client.del(cacheKey); + let cachedData = await client.get(cacheKey); + + if (cachedData) { + const data = JSON.parse(cachedData); + return res.status(200).json({ data }); + } else { + const dataProductSearch = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}&operation=${operation}]` + ); + + await client.set( + cacheKey, + JSON.stringify(dataProductSearch.data), + 'EX', + duration + ); + cachedData = await client.get(cacheKey); + return res.status(200).json({ data: cachedData }); + } + } catch (error) { + console.error('Error interacting with Redis or fetching data:', error); + return res.status(500).json({ error: 'Internal Server Error' }); + } +} diff --git a/src/pages/api/shop/brands.js b/src/pages/api/shop/brands.js index 9c2824b3..d56e4b13 100644 --- a/src/pages/api/shop/brands.js +++ b/src/pages/api/shop/brands.js @@ -1,8 +1,20 @@ import axios from 'axios'; +import { createClient } from 'redis'; const SOLR_HOST = process.env.SOLR_HOST; +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; export default async function handler(req, res) { + await connectRedis(); + try { let params = '*:*'; let sort = @@ -11,12 +23,12 @@ export default async function handler(req, res) { if (req.query.params) { rows = 100; - switch (req?.query?.params) { + switch (req.query.params) { case 'level_s': params = 'level_s:prioritas'; break; case 'search': - params = `name_s:"${req?.query?.q.toLowerCase()}"`; + params = `name_s:"${req.query.q.toLowerCase()}"`; sort = ''; rows = 1; break; @@ -24,11 +36,11 @@ export default async function handler(req, res) { params = `name_s:${req.query.params}`.toLowerCase(); } } - if(req.query.rows) rows = req.query.rows; - + if (req.query.rows) rows = req.query.rows; + const url = `${SOLR_HOST}/solr/brands/select?q=${params}&q.op=OR&indent=true&rows=${rows}&${sort}`; - let brands = await axios(url); - let dataBrands = responseMap(brands.data.response.docs); + const brands = await axios(url); + const dataBrands = responseMap(brands.data.response.docs); res.status(200).json(dataBrands); } catch (error) { @@ -39,13 +51,11 @@ export default async function handler(req, res) { const responseMap = (brands) => { return brands.map((brand) => { - let brandMapping = { + return { id: brand.id, name: brand.display_name_s, logo: brand.image_s || '', - sequance: brand.sequence_i || '', + sequence: brand.sequence_i || '', }; - - return brandMapping; }); }; diff --git a/src/pages/api/shop/preferredBrand.js b/src/pages/api/shop/preferredBrand.js new file mode 100644 index 00000000..4cb35c84 --- /dev/null +++ b/src/pages/api/shop/preferredBrand.js @@ -0,0 +1,61 @@ +import axios from 'axios'; +import { createClient } from 'redis'; + +const SOLR_HOST = process.env.SOLR_HOST; +const client = createClient(); + +client.on('error', (err) => console.error('Redis Client Error', err)); + +const connectRedis = async () => { + if (!client.isOpen) { + await client.connect(); + } +}; + +export default async function handler(req, res) { + await connectRedis(); + + try { + let params = '*:*'; + let sort = + 'sort=if(exists(sequence_i),0,1) asc,sequence_i asc, if(exists(image_s),0,1) asc '; + let rows = 20; + + if (req.query.params) { + rows = 20; + switch (req.query.params) { + case 'level_s': + params = 'level_s:prioritas'; + break; + case 'search': + params = `name_s:"${req.query.q.toLowerCase()}"`; + sort = ''; + rows = 1; + break; + default: + params = `name_s:${req.query.params}`.toLowerCase(); + } + } + if (req.query.rows) rows = req.query.rows; + + const url = `${SOLR_HOST}/solr/brands/select?q=${params}&q.op=OR&indent=true&rows=${rows}&${sort}`; + const brands = await axios(url); + const dataBrands = responseMap(brands.data.response.docs); + + res.status(200).json(dataBrands); + } catch (error) { + console.error('Error fetching data from Solr:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +} + +const responseMap = (brands) => { + return brands.map((brand) => { + return { + id: brand.id, + name: brand.display_name_s, + logo: brand.image_s || '', + sequence: brand.sequence_i || '', + }; + }); +}; diff --git a/src/pages/api/shop/product-detail.js b/src/pages/api/shop/product-detail.js index 247f2a04..faa96028 100644 --- a/src/pages/api/shop/product-detail.js +++ b/src/pages/api/shop/product-detail.js @@ -8,7 +8,7 @@ export default async function handler(req, res) { ) let productVariants = await axios( process.env.SOLR_HOST + - `/solr/variants/select?q=template_id_i:${req.query.id}&q.op=OR&indent=true&rows=100&fq=-publish_b:false` + `/solr/variants/select?q=template_id_i:${req.query.id}&q.op=OR&indent=true&rows=100&fq=-publish_b:false AND price_tier1_v2_f:[1 TO *]` ) let auth = req.query.auth === 'false' ? JSON.parse(req.query.auth) : req.query.auth let result = productMappingSolr(productTemplate.data.response.docs, auth || false) diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index ace281f7..63ec7ca0 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -20,7 +20,6 @@ export default async function handler(req, res) { } = req.query; let { stock = '' } = req.query; - let paramOrderBy = ''; switch (orderBy) { case 'flashsale-discount-desc': @@ -73,8 +72,9 @@ export default async function handler(req, res) { const formattedQuery = `(${newQ .split(' ') - .map((term) => `${term}*`) + .map((term) => (term.length < 2 ? term : `${term}*`)) // Tambahkan '*' hanya jika panjang kata >= 2 .join(' ')})`; + const mm = checkQ.length > 2 ? checkQ.length > 5 @@ -88,11 +88,23 @@ export default async function handler(req, res) { 'price_tier1_v2_f:[1 TO *]', ]; - if (fq && source != 'similar') { - filterQueries.push(fq); + if (fq && source != 'similar' && typeof fq != 'string') { + // filterQueries.push(fq); + fq.push(...filterQueries); } const fq_ = filterQueries.join(' AND '); + let keywords = newQ; + if (source === 'similar' || checkQ.length < 3) { + if (checkQ.length < 2 || checkQ[1].length < 2) { + keywords = newQ; + } else { + keywords = newQ + '*'; + } + } else { + keywords = formattedQuery; + } + let offset = (page - 1) * limit; let parameter = [ 'facet.field=manufacture_name_s', @@ -101,13 +113,7 @@ export default async function handler(req, res) { 'indent=true', `facet.query=${escapeSolrQuery(q)}`, `q.op=OR`, - `q=${ - source == 'similar' || checkQ.length < 3 - ? checkQ.length < 2 - ? newQ - : newQ + '*' - : formattedQuery - }`, + `q=${keywords}`, `defType=edismax`, 'qf=name_s description_clean_t category_name manufacture_name_s variants_code_t variants_name_t category_id_ids default_code_s manufacture_id_i category_id_i ', `start=${parseInt(offset)}`, @@ -152,13 +158,12 @@ export default async function handler(req, res) { if (stock) parameter.push(`fq=stock_total_f:{1 TO *}`); // Single fq in url params - // if (typeof fq === 'string') parameter.push(`fq=${encodeURIComponent(fq)}`); + if (typeof fq === 'string') parameter.push(`fq=${encodeURIComponent(fq)}`); // Multi fq in url params if (Array.isArray(fq)) parameter = parameter.concat( fq.map((val) => `fq=${encodeURIComponent(val)}`) ); - let result = await axios( process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&') ); diff --git a/src/pages/api/shop/variant-detail.js b/src/pages/api/shop/variant-detail.js index 08ce75b8..af3525b3 100644 --- a/src/pages/api/shop/variant-detail.js +++ b/src/pages/api/shop/variant-detail.js @@ -1,21 +1,28 @@ -import { productMappingSolr, variantsMappingSolr } from '@/utils/solrMapping' -import axios from 'axios' +import { productMappingSolr, variantsMappingSolr } from '@/utils/solrMapping'; +import axios from 'axios'; export default async function handler(req, res) { try { let productVariants = await axios( process.env.SOLR_HOST + `/solr/variants/select?q=id:${req.query.id}&q.op=OR&indent=true` - ) - let auth = req.query.auth === 'false' ? JSON.parse(req.query.auth) : req.query.auth + ); + let template_id = productVariants.data.response.docs[0].template_id_i; + let auth = + req.query.auth === 'false' ? JSON.parse(req.query.auth) : req.query.auth; let productTemplate = await axios( - process.env.SOLR_HOST + `/solr/product/select?q=id:${req.query.id}&q.op=OR&indent=true` - ) - let result = variantsMappingSolr(productTemplate.data.response.docs, productVariants.data.response.docs, auth || false) - - res.status(200).json(result) + process.env.SOLR_HOST + + `/solr/product/select?q=id:${template_id}&q.op=OR&indent=true` + ); + let result = variantsMappingSolr( + productTemplate.data.response.docs, + productVariants.data.response.docs, + auth || false + ); + + res.status(200).json(result); } catch (error) { - console.error('Error fetching data from Solr:', error) - res.status(500).json({ error: 'Internal Server Error' }) + console.error('Error fetching data from Solr:', error); + res.status(500).json({ error: 'Internal Server Error' }); } -}
\ No newline at end of file +} diff --git a/src/pages/index.jsx b/src/pages/index.jsx index cc4d68db..2ec1231a 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -45,18 +45,19 @@ const FlashSale = dynamic( } ); -const ProgramPromotion = dynamic(() => - import('@/lib/home/components/PromotionProgram'), -{ - loading: () => <BannerPromoSkeleton />, -} +const ProgramPromotion = dynamic( + () => import('@/lib/home/components/PromotionProgram'), + { + loading: () => <BannerPromoSkeleton />, + } ); const BannerSection = dynamic(() => import('@/lib/home/components/BannerSection') -); -const CategoryHomeId = dynamic(() => - import('@/lib/home/components/CategoryHomeId'), {ssr: false} +); +const CategoryHomeId = dynamic( + () => import('@/lib/home/components/CategoryHomeId'), + { ssr: false } ); const CategoryDynamic = dynamic(() => @@ -64,17 +65,18 @@ const CategoryDynamic = dynamic(() => ); const CategoryDynamicMobile = dynamic(() => -import('@/lib/home/components/CategoryDynamicMobile') + import('@/lib/home/components/CategoryDynamicMobile') ); -const CustomerReviews = dynamic(() => - import('@/lib/review/components/CustomerReviews'), {ssr: false} +const CustomerReviews = dynamic( + () => import('@/lib/review/components/CustomerReviews'), + { ssr: false } ); // need to ssr:false -const ServiceList = dynamic(() => import('@/lib/home/components/ServiceList'), {ssr: false}); // need to ssr: false +const ServiceList = dynamic(() => import('@/lib/home/components/ServiceList'), { + ssr: false, +}); // need to ssr: false - - -export default function Home({categoryId}) { +export default function Home({ categoryId }) { const bannerRef = useRef(null); const wrapperRef = useRef(null); @@ -85,123 +87,110 @@ export default function Home({categoryId}) { bannerRef.current?.querySelector(':first-child')?.clientHeight + 'px'; }; - useEffect(() => { - const loadCategories = async () => { - const getCategories = await odooApi('GET', '/api/v1/category/child?partner_id='+{categoryId}) - if(getCategories){ - setDataCategories(getCategories) - } - } - loadCategories() - }, []) - - const [dataCategories, setDataCategories] = useState([]) return ( <> - <BasicLayout> - <Seo - title='Indoteknik.com: B2B Industrial Supply & Solution' - description='Temukan pilihan produk B2B Industri & Alat Teknik untuk Perusahaan, UMKM & Pemerintah dengan lengkap, mudah dan transparan.' - additionalMetaTags={[ - { - name: 'keywords', - content: 'indoteknik, indoteknik.com, toko teknik, toko perkakas, jual genset, jual fogging, jual krisbow, harga krisbow, harga alat safety, harga pompa air', - }, - ]} /> - - <PagePopupIformation /> - - <DesktopView> - <div className='container mx-auto'> - <div - className='flex min-h-[400px] h-[460px]' - ref={wrapperRef} - onLoad={handleOnLoad} - > - <div className='w-2/12'> - <HeroBannerSecondary /> - </div> - <div className='w-7/12 px-1' ref={bannerRef}> - <HeroBanner /> + <BasicLayout> + <Seo + title='Indoteknik.com: B2B Industrial Supply & Solution' + description='Temukan pilihan produk B2B Industri & Alat Teknik untuk Perusahaan, UMKM & Pemerintah dengan lengkap, mudah dan transparan.' + additionalMetaTags={[ + { + name: 'keywords', + content: + 'indoteknik, indoteknik.com, toko teknik, toko perkakas, jual genset, jual fogging, jual krisbow, harga krisbow, harga alat safety, harga pompa air', + }, + ]} + /> + + <PagePopupIformation /> + + <DesktopView> + <div className='container mx-auto'> + <div + className='flex min-h-[400px] h-[460px]' + ref={wrapperRef} + onLoad={handleOnLoad} + > + <div className='w-2/12'> + <HeroBannerSecondary /> + </div> + <div className='w-7/12 px-1' ref={bannerRef}> + <HeroBanner /> + </div> + <div className='w-3/12'> + <DelayRender renderAfter={200}> + <PopularProduct /> + </DelayRender> + </div> </div> - <div className='w-3/12'> - <DelayRender renderAfter={200}> - <PopularProduct /> - </DelayRender> - </div> - </div> - <div className='my-16 flex flex-col gap-y-8'> - <ServiceList /> - <div id='flashsale'> - <PreferredBrand /> + <div className='my-16 flex flex-col gap-y-8'> + <ServiceList /> + <div id='flashsale'> + <PreferredBrand /> + </div> + {!auth?.feature?.soApproval && ( + <> + <DelayRender renderAfter={200}> + <ProgramPromotion /> + </DelayRender> + <DelayRender renderAfter={200}> + <FlashSale /> + </DelayRender> + </> + )} + {/* <PromotinProgram /> */} + <CategoryPilihan /> + <CategoryDynamic /> + <CategoryHomeId /> + <BannerSection /> + <CustomerReviews /> </div> + </div> + </DesktopView> + <MobileView> + <DelayRender renderAfter={200}> + <HeroBanner /> + </DelayRender> + <div className='flex flex-col gap-y-4 my-6'> + <DelayRender renderAfter={400}> + <ServiceList /> + </DelayRender> + <DelayRender renderAfter={400}> + <div id='flashsale'> + <PreferredBrand /> + </div> + </DelayRender> {!auth?.feature?.soApproval && ( <> - <DelayRender renderAfter={200}> - <ProgramPromotion /> - </DelayRender> - <DelayRender renderAfter={200}> - <FlashSale /> - </DelayRender> + <DelayRender renderAfter={400}> + <ProgramPromotion /> + </DelayRender> + <DelayRender renderAfter={600}> + <FlashSale /> + </DelayRender> </> )} - {/* <PromotinProgram /> */} - {dataCategories &&( - <CategoryPilihan categories={dataCategories} /> - )} - <CategoryDynamic /> - <CategoryHomeId /> - <BannerSection /> - <CustomerReviews /> + <DelayRender renderAfter={600}> + {/* <PromotinProgram /> */} + </DelayRender> + <DelayRender renderAfter={600}> + <CategoryPilihan /> + <CategoryDynamicMobile /> + </DelayRender> + <DelayRender renderAfter={800}> + <PopularProduct /> + </DelayRender> + <DelayRender renderAfter={1000}> + <CategoryHomeId /> + <BannerSection /> + </DelayRender> + <DelayRender renderAfter={1200}> + <CustomerReviews /> + </DelayRender> </div> - </div> - </DesktopView> - <MobileView> - <DelayRender renderAfter={200}> - <HeroBanner /> - </DelayRender> - <div className='flex flex-col gap-y-4 my-6'> - <DelayRender renderAfter={400}> - <ServiceList /> - </DelayRender> - <DelayRender renderAfter={400}> - <div id='flashsale'> - <PreferredBrand /> - </div> - </DelayRender> - {!auth?.feature?.soApproval && ( - <> - <DelayRender renderAfter={400}> - <ProgramPromotion /> - </DelayRender> - <DelayRender renderAfter={600}> - <FlashSale /> - </DelayRender> - </> - )} - <DelayRender renderAfter={600}> - {/* <PromotinProgram /> */} - </DelayRender> - <DelayRender renderAfter={600}> - {dataCategories &&( - <CategoryPilihan categories={dataCategories} /> - )} - <CategoryDynamicMobile /> - </DelayRender> - <DelayRender renderAfter={800}> - <PopularProduct /> - </DelayRender> - <DelayRender renderAfter={1000}> - <CategoryHomeId /> - <BannerSection /> - </DelayRender> - <DelayRender renderAfter={1200}> - <CustomerReviews /> - </DelayRender> - </div> - </MobileView> - </BasicLayout> - </> + </MobileView> + </BasicLayout> + </> ); -}
\ No newline at end of file +} diff --git a/src/pages/shop/product/variant/[slug].jsx b/src/pages/shop/product/variant/[slug].jsx index 42f38774..2c0dd64b 100644 --- a/src/pages/shop/product/variant/[slug].jsx +++ b/src/pages/shop/product/variant/[slug].jsx @@ -32,16 +32,9 @@ export async function getServerSideProps(context) { tier ); let product = response.data; - // let product = await variantApi({ id: getIdFromSlug(slug), headers: { Token: authToken } }) if (product?.length == 1) { product = product[0]; - /* const regexHtmlTags = /(<([^>]+)>)/gi - const regexHtmlTagsExceptP = /<\/?(?!p\b)[^>]*>/g - product.description = product.description - .replace(regexHtmlTagsExceptP, ' ') - .replace(regexHtmlTags, ' ') - .trim()*/ } else { product = null; } diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js index f73e966a..ecd62be2 100644 --- a/src/utils/solrMapping.js +++ b/src/utils/solrMapping.js @@ -43,6 +43,7 @@ export const productMappingSolr = (products, pricelist) => { let productMapped = { id: product.product_id_i || '', image: product.image_s || '', + imageMobile: product.image_mobile_s || '', code: product.default_code_s || '', description: product.description_t || '', displayName: product.display_name_s || '', @@ -74,6 +75,7 @@ export const productMappingSolr = (products, pricelist) => { name: product.manufacture_name_s || '', imagePromotion1: product.image_promotion_1_s || '', imagePromotion2: product.image_promotion_2_s || '', + logo: product.x_logo_manufacture_s || '', }; } @@ -127,12 +129,14 @@ export const variantsMappingSolr = (parent, products, pricelist) => { manufacture: {}, parent: {}, qtySold: product?.qty_sold_f || 0, + is_in_bu: product?.is_in_bu_b || false, }; if (product.manufacture_id_i && product.manufacture_name_s) { productMapped.manufacture = { id: product.manufacture_id_i || '', name: product.manufacture_name_s || '', + logo: parent[0]?.x_logo_manufacture_s, }; } productMapped.parent = { |
