diff options
| author | trisusilo48 <tri.susilo@altama.co.id> | 2024-07-10 15:58:51 +0700 |
|---|---|---|
| committer | trisusilo48 <tri.susilo@altama.co.id> | 2024-07-10 15:58:51 +0700 |
| commit | 2e3c726bc8217f3960cfecec44b81303b03de72b (patch) | |
| tree | 1b85ced7f61f3e4c3f1f27b577b37aa161615065 /src/lib/product/components | |
| parent | 2b3bd9c0a454dbad69ce29cee877bfb1fca5dfa6 (diff) | |
| parent | a99bf6480eea556e53b85e6db45f3b8c2361e693 (diff) | |
Merge branch 'release' into development
# Conflicts:
# src/pages/shop/product/variant/[slug].jsx
Diffstat (limited to 'src/lib/product/components')
| -rw-r--r-- | src/lib/product/components/Product/ProductDesktop.jsx | 764 | ||||
| -rw-r--r-- | src/lib/product/components/Product/ProductDesktopVariant.jsx | 324 | ||||
| -rw-r--r-- | src/lib/product/components/Product/ProductMobile.jsx | 409 | ||||
| -rw-r--r-- | src/lib/product/components/Product/ProductMobileVariant.jsx | 240 | ||||
| -rw-r--r-- | src/lib/product/components/ProductCard.jsx | 197 | ||||
| -rw-r--r-- | src/lib/product/components/ProductFilterDesktop.jsx | 10 | ||||
| -rw-r--r-- | src/lib/product/components/ProductFilterDesktopPromotion.jsx | 132 | ||||
| -rw-r--r-- | src/lib/product/components/ProductSearch.jsx | 405 |
8 files changed, 1540 insertions, 941 deletions
diff --git a/src/lib/product/components/Product/ProductDesktop.jsx b/src/lib/product/components/Product/ProductDesktop.jsx index 5f034c09..444ddd8e 100644 --- a/src/lib/product/components/Product/ProductDesktop.jsx +++ b/src/lib/product/components/Product/ProductDesktop.jsx @@ -1,416 +1,442 @@ -import Image from '@/core/components/elements/Image/Image' -import Link from '@/core/components/elements/Link/Link' -import DesktopView from '@/core/components/views/DesktopView' -import currencyFormat from '@/core/utils/currencyFormat' -import { HeartIcon } from '@heroicons/react/24/outline' -import { useCallback, useEffect, useRef, useState } from 'react' -import LazyLoad from 'react-lazy-load' -import ProductSimilar from '../ProductSimilar' -import { toast } from 'react-hot-toast' -import { updateItemCart } from '@/core/utils/cart' -import { useRouter } from 'next/router' -import { createSlug } from '@/core/utils/slug' -import BottomPopup from '@/core/components/elements/Popup/BottomPopup' -import ProductCard from '../ProductCard' -import productSimilarApi from '../../api/productSimilarApi' -import whatsappUrl from '@/core/utils/whatsappUrl' -import odooApi from '@/core/api/odooApi' -import PromotionType from '@/lib/promotinProgram/components/PromotionType' -import useAuth from '@/core/hooks/useAuth' -import ImageNext from 'next/image' -import CountDown2 from '@/core/components/elements/CountDown/CountDown2' -import { LazyLoadComponent } from 'react-lazy-load-image-component' -import ColumnsSLA from './ColumnsSLA' -import { useProductCartContext } from '@/contexts/ProductCartContext' -import { Box, Skeleton, Tooltip } from '@chakra-ui/react' -import { Info } from 'lucide-react' -import Breadcrumb from './Breadcrumb' -import { sellingProductFormat } from '@/core/utils/formatValue' +import { useEffect, useRef, useState } from 'react'; +import ImageNext from 'next/image'; +import { LazyLoadComponent } from 'react-lazy-load-image-component'; +import { Box, Skeleton, Tooltip } from '@chakra-ui/react'; +import { HeartIcon } from '@heroicons/react/24/outline'; +import { Info } from 'lucide-react'; +import LazyLoad from 'react-lazy-load'; +import { toast } from 'react-hot-toast'; +import { useRouter } from 'next/router'; + +import Image from '@/core/components/elements/Image/Image'; +import Link from '@/core/components/elements/Link/Link'; +import DesktopView from '@/core/components/views/DesktopView'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import CountDown2 from '@/core/components/elements/CountDown/CountDown2'; + +import currencyFormat from '@/core/utils/currencyFormat'; +import { updateItemCart } from '@/core/utils/cart'; +import { createSlug } from '@/core/utils/slug'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import { sellingProductFormat } from '@/core/utils/formatValue'; + +import odooApi from '@/core/api/odooApi'; +import useAuth from '@/core/hooks/useAuth'; + +import { useProductCartContext } from '@/contexts/ProductCartContext'; + +import PromotionType from '@/lib/promotinProgram/components/PromotionType'; + +import ProductSimilar from '../ProductSimilar'; +import ProductCard from '../ProductCard'; +import productSimilarApi from '../../api/productSimilarApi'; +import ColumnsSLA from './ColumnsSLA'; +import Breadcrumb from './Breadcrumb'; + +import ProductPromoSection from '~/modules/product-promo/components/Section'; const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { - const router = useRouter() - const auth = useAuth() - const { slug } = router.query + const router = useRouter(); + const auth = useAuth(); + const { slug } = router.query; - const [quantityActive, setQuantity] = useState(null) - const [lowestPrice, setLowestPrice] = useState(null) - const [product, setProducts] = useState(products) + const [quantityActive, setQuantity] = useState(null); + const [lowestPrice, setLowestPrice] = useState(null); + const [product, setProducts] = useState(products); - const [addCartAlert, setAddCartAlert] = useState(false) - const [isLoadingSLA, setIsLoadingSLA] = useState(true) - const [promotionType, setPromotionType] = useState(false) - const [promotionActiveId, setPromotionActiveId] = useState(null) - const [selectVariantPromoActive, setSelectVariantPromoActive] = useState(null) - const [backgorundFlashSale, setBackgorundFlashSale] = useState(null) + const [addCartAlert, setAddCartAlert] = useState(false); + const [isLoadingSLA, setIsLoadingSLA] = useState(true); + const [promotionType, setPromotionType] = useState(false); + const [promotionActiveId, setPromotionActiveId] = useState(null); + const [selectVariantPromoActive, setSelectVariantPromoActive] = + useState(null); + const [backgorundFlashSale, setBackgorundFlashSale] = useState(null); - const { setRefreshCart, refreshCart } = useProductCartContext() + const { setRefreshCart, refreshCart } = useProductCartContext(); useEffect(() => { - setLowestPrice({ price: product?.lowestPrice }) - }, [product]) + setLowestPrice({ price: product?.lowestPrice }); + }, [product]); useEffect(() => { const getBackgound = async () => { - const get = await odooApi('GET', '/api/v1/banner?type=flash-sale-background-banner') - setBackgorundFlashSale(get[0].image) - } - getBackgound() - }, []) + const get = await odooApi( + 'GET', + '/api/v1/banner?type=flash-sale-background-banner' + ); + setBackgorundFlashSale(get[0].image); + }; + getBackgound(); + }, []); - const [informationTab, setInformationTab] = useState(informationTabOptions[0].value) + const [informationTab, setInformationTab] = useState( + informationTabOptions[0].value + ); - const variantQuantityRefs = useRef([]) + const variantQuantityRefs = useRef([]); const setVariantQuantityRef = (variantId) => (element) => { if (element) { - let variantIndex = product.variants.findIndex((varian) => varian.id == variantId) - product.variants[variantIndex].quantity = element?.value + let variantIndex = product.variants.findIndex( + (varian) => varian.id == variantId + ); + product.variants[variantIndex].quantity = element?.value; } - variantQuantityRefs.current[variantId] = element - } + variantQuantityRefs.current[variantId] = element; + }; const validQuantity = (quantity) => { - let isValid = true + let isValid = true; if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) { - toast.error('Jumlah barang minimal 1') - isValid = false + toast.error('Jumlah barang minimal 1'); + isValid = false; } - return isValid - } + return isValid; + }; const updateCart = (variantId, quantity, source) => { let dataUpdate = { productId: variantId, quantity, selected: true, - source: source === 'buy' ? 'buy' : null - } + source: source === 'buy' ? 'buy' : null, + }; if (product.variants.length > 1) { - let variantIndex = product.variants.findIndex((varian) => varian.id == variantId) - dataUpdate['programLineId'] = product.variants[variantIndex].programActive + let variantIndex = product.variants.findIndex( + (varian) => varian.id == variantId + ); + dataUpdate['programLineId'] = + product.variants[variantIndex].programActive; } else { - dataUpdate['programLineId'] = promotionActiveId + dataUpdate['programLineId'] = promotionActiveId; } - updateItemCart(dataUpdate) - } + updateItemCart(dataUpdate); + }; const redirectToLogin = (action, variantId, quantity) => { - const nextURL = `/shop/product/${slug}?action=${action}&variantId=${variantId}&qty=${quantity}` - router.push(`/login?next=${encodeURIComponent(nextURL)}`) - return true - } + const nextURL = `/shop/product/${slug}?action=${action}&variantId=${variantId}&qty=${quantity}`; + router.push(`/login?next=${encodeURIComponent(nextURL)}`); + return true; + }; const handleAddToCart = (variantId) => { - const quantity = variantQuantityRefs.current[variantId].value + const quantity = variantQuantityRefs.current[variantId].value; - if (!validQuantity(quantity)) return + if (!validQuantity(quantity)) return; if (!auth) { - return redirectToLogin('add_to_cart', variantId, quantity) + return redirectToLogin('add_to_cart', variantId, quantity); } - let source = 'cart' - updateCart(variantId, quantity, source) - setRefreshCart(true) - setAddCartAlert(true) - } + let source = 'cart'; + updateCart(variantId, quantity, source); + setRefreshCart(true); + setAddCartAlert(true); + }; const handleQuantityChange = (variantId) => (event) => { - const { value } = event.target - const variantIndex = product.variants.findIndex((variant) => variant.id === variantId) + const { value } = event.target; + const variantIndex = product.variants.findIndex( + (variant) => variant.id === variantId + ); if (variantIndex !== -1) { - product.variants[variantIndex].quantity = parseInt(value, 10) // Pastikan untuk mengubah ke tipe number jika diperlukan + product.variants[variantIndex].quantity = parseInt(value, 10); // Pastikan untuk mengubah ke tipe number jika diperlukan // Lakukan sesuatu jika nilai quantity diubah } - } + }; const handleBuy = (variant) => { - const quantity = variantQuantityRefs.current[variant].value - if (!validQuantity(quantity)) return + const quantity = variantQuantityRefs.current[variant].value; + if (!validQuantity(quantity)) return; if (!auth) { - return redirectToLogin('buy', variant, quantity) + return redirectToLogin('buy', variant, quantity); } - let source = 'buy' - updateCart(variant, quantity, source) - router.push(`/shop/checkout?source=buy`) - } + let source = 'buy'; + updateCart(variant, quantity, source); + router.push(`/shop/checkout?source=buy`); + }; - const variantSectionRef = useRef(null) + const variantSectionRef = useRef(null); const goToVariantSection = () => { if (variantSectionRef.current) { - const position = variantSectionRef.current.getBoundingClientRect() + const position = variantSectionRef.current.getBoundingClientRect(); window.scrollTo({ top: position.top - 120 + window.pageYOffset, - behavior: 'smooth' - }) + behavior: 'smooth', + }); } - } + }; const handlePromoClick = (variantId) => { - setSelectVariantPromoActive(variantId) - setPromotionType(true) - } + setSelectVariantPromoActive(variantId); + setPromotionType(true); + }; const productSimilarQuery = [ product?.name, `fq=-product_id_i:${product.id}`, - `fq=-manufacture_id_i:${product.manufacture?.id || 0}` - ].join('&') + `fq=-manufacture_id_i:${product.manufacture?.id || 0}`, + ].join('&'); - const [productSimilarInBrand, setProductSimilarInBrand] = useState(null) + const [productSimilarInBrand, setProductSimilarInBrand] = useState(null); useEffect(() => { const loadProductSimilarInBrand = async () => { - const productSimilarQuery = [product?.name, `fq=-product_id_i:${product.id}`].join('&') - const source = 'right' - const dataProductSimilar = await productSimilarApi({ query: productSimilarQuery, source }) - setProductSimilarInBrand(dataProductSimilar.products) - } - if (!productSimilarInBrand) loadProductSimilarInBrand() - }, [product, productSimilarInBrand]) + const productSimilarQuery = [ + product?.name, + `fq=-product_id_i:${product.id}`, + ].join('&'); + const source = 'right'; + const dataProductSimilar = await productSimilarApi({ + query: productSimilarQuery, + source, + }); + setProductSimilarInBrand(dataProductSimilar.products); + }; + if (!productSimilarInBrand) loadProductSimilarInBrand(); + }, [product, productSimilarInBrand]); useEffect(() => { const fetchData = async () => { const promises = product.variants.map(async (variant) => { - const dataSLA = await odooApi('GET', `/api/v1/product_variant/${variant.id}/stock`) + const dataSLA = await odooApi( + 'GET', + `/api/v1/product_variant/${variant.id}/stock` + ); return { ...variant, - sla: dataSLA - } - }) - const variantData = await Promise.all(promises) - product.variants = variantData + sla: dataSLA, + }; + }); + const variantData = await Promise.all(promises); + product.variants = variantData; - setIsLoadingSLA(false) - } - if (product.variantTotal == 1) fetchData() - }, [product]) + setIsLoadingSLA(false); + }; + if (product.variantTotal == 1) fetchData(); + }, [product]); return ( <DesktopView> <div className='container mx-auto pt-10'> <Breadcrumb productId={product.id} productName={product.name} /> - <div className='flex'> - <div className='w-full flex flex-wrap'> - <div className='w-5/12'> - <div className='relative mb-2'> - {product?.flashSale?.remainingTime > 0 && - lowestPrice?.price.discountPercentage > 0 && ( - <div className={`absolute bottom-0 w-full`}> - <div className='absolute bottom-0 w-full h-full'> - <ImageNext - src={backgorundFlashSale || '/images/GAMBAR-BG-FLASH-SALE.jpg'} - width={1000} - height={100} - /> - </div> - <div className='relative'> - <div className='flex gap-x-2 items-center p-2'> - <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '> - <span className='text-lg font-bold'> - {Math.floor(product.lowestPrice.discountPercentage)}% - </span> - </div> - <div - className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-4`} - > - <ImageNext - src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg' - width={17} - height={10} - /> - <span className='text-white text-lg font-semibold'> - {product?.flashSale?.tag != 'false' || product?.flashSale?.tag - ? product?.flashSale?.tag - : 'FLASH SALE'} - </span> - </div> - <div> - <CountDown2 initialTime={product.flashSale.remainingTime} /> - </div> + + <div className='w-full flex flex-wrap'> + <div className='w-3/12'> + <div className='relative mb-2'> + {product?.flashSale?.remainingTime > 0 && + lowestPrice?.price.discountPercentage > 0 && ( + <div className={`absolute bottom-0 w-full`}> + <div className='absolute bottom-0 w-full h-full'> + <ImageNext + src={ + backgorundFlashSale || + '/images/BG-FLASH-SALE.jpg' + } + width={1000} + height={100} + className='h-full' + /> + </div> + <div className='relative'> + <div className='flex gap-x-2 items-center p-2'> + <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '> + <span className='text-lg font-bold'> + {Math.floor(product.lowestPrice.discountPercentage)} + % + </span> + </div> + <div + className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-1`} + > + <ImageNext + src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg' + width={17} + height={10} + /> + <span className='text-white text-sm font-semibold'> + {product?.flashSale?.tag != 'false' || + product?.flashSale?.tag + ? product?.flashSale?.tag + : 'FLASH SALE'} + </span> + </div> + <div> + <CountDown2 + initialTime={product.flashSale.remainingTime} + /> </div> </div> </div> - )} - <Image - src={product.image} - alt={product.name} - className='h-[430px] object-contain object-center w-full border border-gray_r-4' - /> - </div> - <div> - <p className='text-justify text-xs leading-5'> - <span className='font-semibold '>Keterangan : </span>Gambar atau foto berperan - sebagai ilustrasi produk. Kadang tidak sesuai dengan kondisi terbaru dengan - berbagai perubahan dan perbaikan. Hubungi tim sales kami untuk informasi yang - lebih baik perihal gambar di 021-2933 8828. - </p> - </div> - </div> - - <div className='w-7/12 px-4'> - <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> - <div className='flex p-3'> - <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.variants.length > 1 && ( - <button - type='button' - onClick={goToVariantSection} - className={`flex gap-x-1 items-center p-2 rounded-lg w-auto btn-light`} - > - <span className='text-red-600 text-sm'>Lihat Selengkapnya</span> - </button> - )} + )} + <Image + src={product.image} + alt={product.name} + className='h-[430px] object-contain object-center w-full border border-gray_r-4' + /> + </div> + <div> + <p className='text-justify text-xs leading-5'> + <span className='font-semibold '>Keterangan : </span>Gambar atau + foto berperan sebagai ilustrasi produk. Kadang tidak sesuai + dengan kondisi terbaru dengan berbagai perubahan dan perbaikan. + Hubungi tim sales kami untuk informasi yang lebih baik perihal + gambar di 021-2933 8828. + </p> + </div> + </div> - {product.variants.length === 1 && ( - <> - {!product.variants[0]?.sla && <Skeleton width='20%' height='16px' />} - {product.variants[0]?.sla && ( - <Tooltip - placement='top' - label={`Masa Persiapan Barang ${product.variants[0]?.sla?.slaDate}`} - > - <Box className='w-fit flex items-center gap-x-2'> - {product.variants[0]?.sla?.slaDate} - <Info size={16} /> - </Box> - </Tooltip> - )} - </> - )} - </div> + <div className='w-6/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> + <div className='flex p-3'> + <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.variants.length > 1 && ( + <button + type='button' + onClick={goToVariantSection} + className={`flex gap-x-1 items-center p-2 rounded-lg w-auto btn-light`} + > + <span className='text-red-600 text-sm'> + Lihat Selengkapnya + </span> + </button> + )} - {product.variants.length === 1 && ( - <div className='flex p-3 '> - <div className='w-4/12 text-gray_r-12/70'>Stock</div> - <div className='w-8/12'> - {!product.variants[0]?.sla && <Skeleton width='10%' height='16px' />} - {product?.variants[0].sla?.qty > 0 && ( - <span>{product?.variants[0].sla?.qty}</span> + {product.variants.length === 1 && ( + <> + {!product.variants[0]?.sla && ( + <Skeleton width='20%' height='16px' /> )} - {product?.variants[0].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' + {product.variants[0]?.sla && ( + <Tooltip + placement='top' + label={`Masa Persiapan Barang ${product.variants[0]?.sla?.slaDate}`} > - Tanya Admin - </a> + <Box className='w-fit flex items-center gap-x-2'> + {product.variants[0]?.sla?.slaDate} + <Info size={16} /> + </Box> + </Tooltip> )} - </div> - </div> - )} + </> + )} + </div> + </div> - <div className={`flex p-3 ${product.variants.length > 1 ? '' : 'bg-gray_r-4'} `}> - <div className='w-4/12 text-gray_r-12/70'>Berat Barang</div> + {product.variants.length === 1 && ( + <div className='flex p-3 '> + <div className='w-4/12 text-gray_r-12/70'>Stock</div> <div className='w-8/12'> - {product?.weight > 0 && <span>{product?.weight} KG</span>} - {product?.weight == 0 && ( + {!product.variants[0]?.sla && ( + <Skeleton width='10%' height='16px' /> + )} + {product?.variants[0].sla?.qty > 0 && ( + <span>{product?.variants[0].sla?.qty}</span> + )} + {product?.variants[0].sla?.qty == 0 && ( <a - href={whatsappUrl('productWeight', { + href={whatsappUrl('product', { name: product.name, - url: createSlug('/shop/product/', product.name, product.id, true) + manufacture: product?.manufacture?.name, + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 font-medium' > - Tanya Berat + Tanya Admin </a> )} </div> </div> - {product.variants.length <= 1 && ( - <div className='pt-3'> - <div className='flex mt-1'> - <PromotionType - variantId={product.variants[0].id} - setPromotionActiveId={setPromotionActiveId} - promotionActiveId={promotionActiveId} - quantity={quantityActive} - product={product} - ></PromotionType> - </div> - </div> - )} - </div> - </div> + )} - <div className='w-full'> - <div className='mt-12'> - <div className='text-h-lg font-semibold'>Informasi Produk</div> - <div className='flex gap-x-4 mt-6 mb-4'> - {informationTabOptions.map((option) => ( - <TabButton - value={option.value} - key={option.value} - active={informationTab == option.value} - onClick={() => setInformationTab(option.value)} + <div + className={`flex p-3 ${ + product.variants.length > 1 ? '' : '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>} + {product?.weight == 0 && ( + <a + href={whatsappUrl('productWeight', { + name: product.name, + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), + })} + className='text-danger-500 font-medium' > - {option.label} - </TabButton> - ))} + Tanya Berat + </a> + )} </div> - <div className='flex'> - <div className='w-3/4 leading-8 product__description'> - <TabContent active={informationTab == 'description'}> - <span - dangerouslySetInnerHTML={{ - __html: - product.description != '' - ? product.description - : 'Belum ada deskripsi produk.' - }} - /> - </TabContent> - - <TabContent active={informationTab == 'information'}> - Belum ada informasi. - </TabContent> + </div> + {product.variants.length <= 1 && ( + <div className='pt-3'> + <div className='flex mt-1'> + <PromotionType + variantId={product.variants[0].id} + setPromotionActiveId={setPromotionActiveId} + promotionActiveId={promotionActiveId} + quantity={quantityActive} + product={product} + ></PromotionType> + <ProductPromoSection productId={product.variants[0].id} /> </div> </div> - </div> + )} </div> </div> - <div className='w-[30%]'> - {product.variants.length > 1 && product.lowestPrice.priceDiscount > 0 && ( - <div className='text-gray_r-12/80'>Harga mulai dari: </div> - )} + <div className='w-3/12'> + {product.variants.length > 1 && + product.lowestPrice.priceDiscount > 0 && ( + <div className='text-gray_r-12/80'>Harga mulai dari: </div> + )} {/* {lowestPrice?.discountPercentage > 0 && ( <div className='flex gap-x-1 items-center mt-2'> @@ -441,7 +467,8 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { {sellingProductFormat(product?.qtySold) + ' Terjual'} </div> )} - {product?.flashSale?.id && lowestPrice?.price.discountPercentage > 0 ? ( + {product?.flashSale?.id && + lowestPrice?.price.discountPercentage > 0 ? ( <> <div className='flex gap-x-1 items-center mt-2'> <div className='badge-solid-red text-caption-1'> @@ -456,7 +483,10 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { </div> <div className='text-gray_r-9 text-base font-normal mt-1'> Termasuk PPN:{' '} - {currencyFormat(lowestPrice?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + lowestPrice?.price.priceDiscount * + process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( @@ -466,7 +496,9 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { {currencyFormat(lowestPrice?.price.price)} <div className='text-gray_r-9 text-base font-normal mt-1'> Termasuk PPN:{' '} - {currencyFormat(lowestPrice?.price.price * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + lowestPrice?.price.price * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( @@ -476,7 +508,12 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { href={whatsappUrl('product', { name: product.name, manufacture: product.manufacture?.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 underline' rel='noopener noreferrer' @@ -524,7 +561,10 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { )} <div className='flex mt-4'> - <button className='flex items-center gap-x-1' onClick={toggleWishlist}> + <button + className='flex items-center gap-x-1' + onClick={toggleWishlist} + > {wishlist.data?.productTotal > 0 ? ( <HeartIcon className='w-6 fill-danger-500 text-danger-500' /> ) : ( @@ -538,7 +578,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { <div className='font-medium text-center p-4 bg-gray_r-1 border-b border-gray_r-6 sticky top-0 z-10'> Produk Serupa </div> - <div className='h-full divide-y divide-gray_r-6 max-h-96'> + <div className='h-full divide-y divide-gray_r-6 max-h-[550px]'> {productSimilarInBrand && productSimilarInBrand?.map((product) => ( <div className='py-2' key={product.id}> @@ -550,6 +590,42 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { </div> </div> + <div className='w-full'> + <div className='mt-12'> + <div className='text-h-lg font-semibold'>Informasi Produk</div> + <div className='flex gap-x-4 mt-6 mb-4'> + {informationTabOptions.map((option) => ( + <TabButton + value={option.value} + key={option.value} + active={informationTab == option.value} + onClick={() => setInformationTab(option.value)} + > + {option.label} + </TabButton> + ))} + </div> + <div className='flex'> + <div className='w-3/4 leading-8 product__description'> + <TabContent active={informationTab == 'description'}> + <span + dangerouslySetInnerHTML={{ + __html: + product.description != '' + ? product.description + : 'Belum ada deskripsi produk.', + }} + /> + </TabContent> + + <TabContent active={informationTab == 'information'}> + Belum ada informasi. + </TabContent> + </div> + </div> + </div> + </div> + {product.variants.length > 1 && ( <div className='mt-12' ref={variantSectionRef}> <div className='text-h-lg font-semibold mb-6'>Varian Produk</div> @@ -571,7 +647,9 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { <tr key={variant.id}> <td className='flex items-center justify-center gap-x-1'> {variant.isFlashsale && ( - <span className='blink-color-flash-sale'>🗲</span> + <span className='blink-color-flash-sale'> + 🗲 + </span> )} {variant.code} </td> @@ -580,11 +658,13 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { <ColumnsSLA variant={variant} product={product} /> </LazyLoadComponent> <td> - {variant.isFlashsale && variant?.price?.discountPercentage > 0 ? ( + {variant.isFlashsale && + variant?.price?.discountPercentage > 0 ? ( <> <div className='flex items-center gap-x-1 justify-center'> <div className='badge-solid-red text-caption-1'> - {Math.floor(variant?.price?.discountPercentage)}% + {Math.floor(variant?.price?.discountPercentage)} + % </div> <div className='line-through text-caption-1 text-gray_r-11'> {currencyFormat(variant?.price?.price)} @@ -596,7 +676,8 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { <div className=' text-caption-1 text-gray_r-11 mb-1'> Inc. PPN:{' '} {currencyFormat( - variant.price.priceDiscount * process.env.NEXT_PUBLIC_PPN + variant.price.priceDiscount * + process.env.NEXT_PUBLIC_PPN )} </div> </> @@ -610,7 +691,8 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { <div className=' text-caption-1 text-gray_r-11 mb-1'> Inc. PPN:{' '} {currencyFormat( - variant?.price?.price * process.env.NEXT_PUBLIC_PPN + variant?.price?.price * + process.env.NEXT_PUBLIC_PPN )} </div> </> @@ -619,7 +701,12 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { href={whatsappUrl('product', { name: variant.name, manufacture: product.manufacture?.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-red_r-11' rel='noopener noreferrer' @@ -705,11 +792,14 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { )} <div className='my-12'> - <div className='text-h-lg font-semibold mb-6'>Kamu Mungkin Juga Suka</div> + <div className='text-h-lg font-semibold mb-6'> + Kamu Mungkin Juga Suka + </div> <LazyLoad> <ProductSimilar query={productSimilarQuery} /> </LazyLoad> </div> + <BottomPopup className=' !h-[75%]' title='Pakai Promo' @@ -728,6 +818,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { ></PromotionType> </div> </BottomPopup> + <BottomPopup className='!container' title='Berhasil Ditambahkan' @@ -742,16 +833,23 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { className='h-32 object-contain object-center w-full border border-gray_r-4' /> </div> - <div className='ml-3 flex flex-1 items-center font-normal'>{product.name}</div> + <div className='ml-3 flex flex-1 items-center font-normal'> + {product.name} + </div> <div className='ml-3 flex items-center font-normal'> - <Link href='/shop/cart' className='flex-1 py-2 text-gray_r-12 btn-yellow'> + <Link + href='/shop/cart' + className='flex-1 py-2 text-gray_r-12 btn-yellow' + > Lihat Keranjang </Link> </div> </div> <div className='mt-8 mb-4'> - <div className='text-h-sm font-semibold mb-6'>Kamu Mungkin Juga Suka</div> + <div className='text-h-sm font-semibold mb-6'> + Kamu Mungkin Juga Suka + </div> <LazyLoad> <ProductSimilar query={productSimilarQuery} /> </LazyLoad> @@ -759,29 +857,33 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { </BottomPopup> </div> </DesktopView> - ) -} + ); +}; const informationTabOptions = [ { value: 'description', label: 'Deskripsi' }, - { value: 'information', label: 'Info Penting' } -] + { value: 'information', label: 'Info Penting' }, +]; const TabButton = ({ children, active, ...props }) => { const activeClassName = active ? 'text-danger-500 underline underline-offset-4' - : 'text-gray_r-12/80' + : 'text-gray_r-12/80'; return ( - <button {...props} type='button' className={`font-medium ${activeClassName}`}> + <button + {...props} + type='button' + className={`font-medium ${activeClassName}`} + > {children} </button> - ) -} + ); +}; const TabContent = ({ children, active, className = '', ...props }) => ( <div {...props} className={`${active ? 'block' : 'hidden'} ${className}`}> {children} </div> -) +); -export default ProductDesktop +export default ProductDesktop; diff --git a/src/lib/product/components/Product/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx index ef61bafd..09b30a44 100644 --- a/src/lib/product/components/Product/ProductDesktopVariant.jsx +++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx @@ -1,137 +1,155 @@ -import Image from '@/core/components/elements/Image/Image' -import Link from '@/core/components/elements/Link/Link' -import DesktopView from '@/core/components/views/DesktopView' -import currencyFormat from '@/core/utils/currencyFormat' -import { HeartIcon } from '@heroicons/react/24/outline' -import { useCallback, useEffect, useRef, useState } from 'react' -import LazyLoad from 'react-lazy-load' -import ProductSimilar from '../ProductSimilar' -import { toast } from 'react-hot-toast' -import { updateItemCart } from '@/core/utils/cart' -import { useRouter } from 'next/router' -import { createSlug } from '@/core/utils/slug' -import BottomPopup from '@/core/components/elements/Popup/BottomPopup' -import ProductCard from '../ProductCard' -import productSimilarApi from '../../api/productSimilarApi' -import whatsappUrl from '@/core/utils/whatsappUrl' -import useAuth from '@/core/hooks/useAuth' -import odooApi from '@/core/api/odooApi' -import { useProductCartContext } from '@/contexts/ProductCartContext' -import { Box, Skeleton, Tooltip } from '@chakra-ui/react' -import { Info } from 'lucide-react' - -const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) => { - const router = useRouter() - const auth = useAuth() - const { slug } = router.query - - const [lowestPrice, setLowestPrice] = useState(null) - - const [addCartAlert, setAddCartAlert] = useState(false) - const [isLoadingSLA, setIsLoadingSLA] = useState(true) - - const { setRefreshCart } = useProductCartContext() + +import { Box, Skeleton, Tooltip } from '@chakra-ui/react'; +import { HeartIcon } from '@heroicons/react/24/outline'; +import { Info } from 'lucide-react'; +import { useRouter } from 'next/router'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { toast } from 'react-hot-toast'; +import LazyLoad from 'react-lazy-load'; + +import { useProductCartContext } from '@/contexts/ProductCartContext'; +import odooApi from '@/core/api/odooApi'; +import Image from '@/core/components/elements/Image/Image'; +import Link from '@/core/components/elements/Link/Link'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import DesktopView from '@/core/components/views/DesktopView'; +import useAuth from '@/core/hooks/useAuth'; +import { updateItemCart } from '@/core/utils/cart'; +import currencyFormat from '@/core/utils/currencyFormat'; +import { createSlug } from '@/core/utils/slug'; +import whatsappUrl from '@/core/utils/whatsappUrl'; + +import productSimilarApi from '../../api/productSimilarApi'; +import ProductCard from '../ProductCard'; +import ProductSimilar from '../ProductSimilar'; + +const ProductDesktopVariant = ({ + product, + wishlist, + toggleWishlist, + isVariant, +}) => { + const router = useRouter(); + const auth = useAuth(); + const { slug } = router.query; + + const [lowestPrice, setLowestPrice] = useState(null); + + const [addCartAlert, setAddCartAlert] = useState(false); + const [isLoadingSLA, setIsLoadingSLA] = useState(true); + + const { setRefreshCart } = useProductCartContext(); const getLowestPrice = useCallback(() => { - const lowest = product.price + const lowest = product.price; /* const lowest = prices.reduce((lowest, price) => { return price.priceDiscount < lowest.priceDiscount ? price : lowest }, prices[0])*/ - return lowest - }, [product]) + return lowest; + }, [product]); useEffect(() => { - const lowest = getLowestPrice() - setLowestPrice(lowest) - }, [getLowestPrice]) + const lowest = getLowestPrice(); + setLowestPrice(lowest); + }, [getLowestPrice]); - const [informationTab, setInformationTab] = useState(informationTabOptions[0].value) + const [informationTab, setInformationTab] = useState( + informationTabOptions[0].value + ); - const variantQuantityRefs = useRef([]) + const variantQuantityRefs = useRef([]); const setVariantQuantityRef = (variantId) => (element) => { - variantQuantityRefs.current[variantId] = element - } + variantQuantityRefs.current[variantId] = element; + }; const validQuantity = (quantity) => { - let isValid = true + let isValid = true; if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) { - toast.error('Jumlah barang minimal 1') - isValid = false + toast.error('Jumlah barang minimal 1'); + isValid = false; } - return isValid - } + return isValid; + }; const handleAddToCart = (variant) => { if (!auth) { - router.push(`/login?next=/shop/product/${slug}`) - return + router.push(`/login?next=/shop/product/${slug}`); + return; } - const quantity = variantQuantityRefs.current[product.id].value - if (!validQuantity(quantity)) return + const quantity = variantQuantityRefs.current[product.id].value; + if (!validQuantity(quantity)) return; updateItemCart({ productId: product.id, quantity, programLineId: null, selected: true, - source: null + source: null, }).then(() => { - setRefreshCart(true) - }) - setAddCartAlert(true) - } + setRefreshCart(true); + }); + setAddCartAlert(true); + }; const handleBuy = (variant) => { - const quantity = variantQuantityRefs.current[product.id].value - if (!validQuantity(quantity)) return + const quantity = variantQuantityRefs.current[product.id].value; + if (!validQuantity(quantity)) return; updateItemCart({ productId: variant, quantity, programLineId: null, selected: true, - source: 'buy' - }) - router.push(`/shop/checkout?source=buy`) - } + source: 'buy', + }); + router.push(`/shop/checkout?source=buy`); + }; - const variantSectionRef = useRef(null) + const variantSectionRef = useRef(null); const goToVariantSection = () => { if (variantSectionRef.current) { - const position = variantSectionRef.current.getBoundingClientRect() + const position = variantSectionRef.current.getBoundingClientRect(); window.scrollTo({ top: position.top - 120 + window.pageYOffset, - behavior: 'smooth' - }) + behavior: 'smooth', + }); } - } + }; const productSimilarQuery = [ product?.name, `fq=-product_id_i:${product.id}`, - `fq=-manufacture_id_i:${product.manufacture?.id || 0}` - ].join('&') + `fq=-manufacture_id_i:${product.manufacture?.id || 0}`, + ].join('&'); - const [productSimilarInBrand, setProductSimilarInBrand] = useState(null) + const [productSimilarInBrand, setProductSimilarInBrand] = useState(null); useEffect(() => { const loadProductSimilarInBrand = async () => { - const productSimilarQuery = [product?.name, `fq=-product_id_i:${product.id}`].join('&') - const dataProductSimilar = await productSimilarApi({ query: productSimilarQuery }) - setProductSimilarInBrand(dataProductSimilar.products) - } - if (!productSimilarInBrand) loadProductSimilarInBrand() - }, [product, productSimilarInBrand]) + const productSimilarQuery = [ + product?.name, + `fq=-product_id_i:${product.id}`, + ].join('&'); + const dataProductSimilar = await productSimilarApi({ + query: productSimilarQuery, + }); + setProductSimilarInBrand(dataProductSimilar.products); + }; + if (!productSimilarInBrand) loadProductSimilarInBrand(); + }, [product, productSimilarInBrand]); useEffect(() => { const fetchData = async () => { - const dataSLA = await odooApi('GET', `/api/v1/product_variant/${product.id}/stock`) - product.sla = dataSLA + const dataSLA = await odooApi( + 'GET', + `/api/v1/product_variant/${product.id}/stock` + ); + product.sla = dataSLA; - setIsLoadingSLA(false) - } - fetchData() - }, [product]) + setIsLoadingSLA(false); + }; + fetchData(); + }, [product]); return ( <DesktopView> @@ -140,14 +158,16 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) <div className='w-full flex flex-wrap'> <div className='w-5/12'> <Image - src={product.image} + src={product.image + '?variant=True'} alt={product.name} className='h-[430px] object-contain object-center w-full border border-gray_r-4' /> </div> <div className='w-7/12 px-4'> - <h1 className='text-title-md leading-10 font-medium'>{product?.name}</h1> + <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> @@ -177,7 +197,9 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) </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-4/12 text-gray_r-12/70'> + Persiapan Barang + </div> <div className='w-8/12'> {!product?.sla && <Skeleton width='20%' height='16px' />} {product?.sla && ( @@ -203,8 +225,13 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) <a href={whatsappUrl('product', { name: product.name, - manufacture: product?.manufacture?.name, - url: createSlug('/shop/product/', product.name, product.id, true) + manufacture: product?.manufacture?.name, + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 font-medium' > @@ -221,7 +248,12 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) <a href={whatsappUrl('productWeight', { name: product.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 font-medium' > @@ -233,44 +265,23 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) </div> </div> - {/* <div className='w-full'> - <div className='mt-12'> - <div className='text-h-lg font-semibold'>Informasi Produk</div> - <div className='flex gap-x-4 mt-6 mb-4'> - {informationTabOptions.map((option) => ( - <TabButton - value={option.value} - key={option.value} - active={informationTab == option.value} - onClick={() => setInformationTab(option.value)} - > - {option.label} - </TabButton> - ))} - </div> - <div className='flex'> - <div className='w-3/4 leading-7 product__description'> - <TabContent active={informationTab == 'description'}> - <span - dangerouslySetInnerHTML={{ - __html: - product.description != '' - ? product.description - : 'Belum ada deskripsi produk.' - }} - /> - </TabContent> - - <TabContent active={informationTab == 'information'}> - Belum ada informasi. - </TabContent> - </div> - </div> - </div> - </div> */} + <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 className='w-[25%]'> - {product?.isFlashsale > 0 && product?.price?.discountPercentage > 0? ( + {product?.isFlashsale > 0 && + product?.price?.discountPercentage > 0 ? ( <> <div className='flex gap-x-1 items-center mt-2'> <div className='badge-solid-red text-caption-1'> @@ -285,7 +296,9 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) </div> <div className='text-gray_r-9 text-base font-normal mt-1'> Termasuk PPN:{' '} - {currencyFormat(product?.price?.priceDiscount * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + product?.price?.priceDiscount * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( @@ -295,7 +308,9 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) {currencyFormat(product?.price?.price)} <div className='text-gray_r-9 text-base font-normal mt-1'> Termasuk PPN:{' '} - {currencyFormat(product?.price?.price * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + product?.price?.price * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( @@ -305,7 +320,12 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) href={whatsappUrl('product', { name: product.name, manufacture: product.manufacture?.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 underline' rel='noopener noreferrer' @@ -340,7 +360,10 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) </button> </div> <div className='flex mt-4'> - <button className='flex items-center gap-x-1' onClick={toggleWishlist}> + <button + className='flex items-center gap-x-1' + onClick={toggleWishlist} + > {wishlist.data?.productTotal > 0 ? ( <HeartIcon className='w-6 fill-danger-500 text-danger-500' /> ) : ( @@ -366,7 +389,9 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) </div> <div className='my-12'> - <div className='text-h-lg font-semibold mb-6'>Kamu Mungkin Juga Suka</div> + <div className='text-h-lg font-semibold mb-6'> + Kamu Mungkin Juga Suka + </div> <LazyLoad> <ProductSimilar query={productSimilarQuery} /> </LazyLoad> @@ -381,21 +406,28 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) <div className='flex mt-4'> <div className='w-[10%]'> <Image - src={product.image} + src={product.image + '?variant=True'} alt={product.name} className='h-32 object-contain object-center w-full border border-gray_r-4' /> </div> - <div className='ml-3 flex flex-1 items-center font-normal'>{product.name}</div> + <div className='ml-3 flex flex-1 items-center font-normal'> + {product.name} + </div> <div className='ml-3 flex items-center font-normal'> - <Link href='/shop/cart' className='flex-1 py-2 text-gray_r-12 btn-yellow'> + <Link + href='/shop/cart' + className='flex-1 py-2 text-gray_r-12 btn-yellow' + > Lihat Keranjang </Link> </div> </div> <div className='mt-8 mb-4'> - <div className='text-h-sm font-semibold mb-6'>Kamu Mungkin Juga Suka</div> + <div className='text-h-sm font-semibold mb-6'> + Kamu Mungkin Juga Suka + </div> <LazyLoad> <ProductSimilar query={productSimilarQuery} /> </LazyLoad> @@ -403,29 +435,33 @@ const ProductDesktopVariant = ({ product, wishlist, toggleWishlist, isVariant }) </BottomPopup> </div> </DesktopView> - ) -} + ); +}; const informationTabOptions = [ { value: 'description', label: 'Deskripsi' }, - { value: 'information', label: 'Info Penting' } -] + { value: 'information', label: 'Info Penting' }, +]; const TabButton = ({ children, active, ...props }) => { const activeClassName = active ? 'text-danger-500 underline underline-offset-4' - : 'text-gray_r-12/80' + : 'text-gray_r-12/80'; return ( - <button {...props} type='button' className={`font-medium ${activeClassName}`}> + <button + {...props} + type='button' + className={`font-medium ${activeClassName}`} + > {children} </button> - ) -} + ); +}; const TabContent = ({ children, active, className = '', ...props }) => ( <div {...props} className={`${active ? 'block' : 'hidden'} ${className}`}> {children} </div> -) +); -export default ProductDesktopVariant +export default ProductDesktopVariant; diff --git a/src/lib/product/components/Product/ProductMobile.jsx b/src/lib/product/components/Product/ProductMobile.jsx index e23e2fb9..113a1e42 100644 --- a/src/lib/product/components/Product/ProductMobile.jsx +++ b/src/lib/product/components/Product/ProductMobile.jsx @@ -1,60 +1,66 @@ -import Divider from '@/core/components/elements/Divider/Divider' -import Image from '@/core/components/elements/Image/Image' -import Link from '@/core/components/elements/Link/Link' -import currencyFormat from '@/core/utils/currencyFormat' -import { useEffect, useState } from 'react' -import Select from 'react-select' -import ProductSimilar from '../ProductSimilar' -import LazyLoad from 'react-lazy-load' -import { updateItemCart } from '@/core/utils/cart' -import { HeartIcon } from '@heroicons/react/24/outline' -import { useRouter } from 'next/router' -import MobileView from '@/core/components/views/MobileView' -import { toast } from 'react-hot-toast' -import { createSlug } from '@/core/utils/slug' -import BottomPopup from '@/core/components/elements/Popup/BottomPopup' -import whatsappUrl from '@/core/utils/whatsappUrl' -import PromotionType from '@/lib/promotinProgram/components/PromotionType' -import { gtagAddToCart } from '@/core/utils/googleTag' -import odooApi from '@/core/api/odooApi' -import ImageNext from 'next/image' -import CountDown2 from '@/core/components/elements/CountDown/CountDown2' -import Breadcrumb from './Breadcrumb' -import useAuth from '@/core/hooks/useAuth' -import { sellingProductFormat } from '@/core/utils/formatValue' +import Divider from '@/core/components/elements/Divider/Divider'; +import Image from '@/core/components/elements/Image/Image'; +import Link from '@/core/components/elements/Link/Link'; +import currencyFormat from '@/core/utils/currencyFormat'; +import { useEffect, useState } from 'react'; +import Select from 'react-select'; +import ProductSimilar from '../ProductSimilar'; +import LazyLoad from 'react-lazy-load'; +import { updateItemCart } from '@/core/utils/cart'; +import { HeartIcon } from '@heroicons/react/24/outline'; +import { useRouter } from 'next/router'; +import MobileView from '@/core/components/views/MobileView'; +import { toast } from 'react-hot-toast'; +import { createSlug } from '@/core/utils/slug'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import PromotionType from '@/lib/promotinProgram/components/PromotionType'; +import { gtagAddToCart } from '@/core/utils/googleTag'; +import odooApi from '@/core/api/odooApi'; +import ImageNext from 'next/image'; +import CountDown2 from '@/core/components/elements/CountDown/CountDown2'; +import Breadcrumb from './Breadcrumb'; +import useAuth from '@/core/hooks/useAuth'; +import { sellingProductFormat } from '@/core/utils/formatValue'; +import ProductPromoSection from '~/modules/product-promo/components/Section'; const ProductMobile = ({ product, wishlist, toggleWishlist }) => { - const router = useRouter() - const auth = useAuth() - const { slug } = router.query - - const [quantity, setQuantity] = useState('1') - const [selectedVariant, setSelectedVariant] = useState(null) - const [informationTab, setInformationTab] = useState(informationTabOptions[0].value) - const [addCartAlert, setAddCartAlert] = useState(false) - - const [isLoadingSLA, setIsLoadingSLA] = useState(true) - const [promotionType, setPromotionType] = useState(false) - const [promotionActiveId, setPromotionActiveId] = useState(null) - const [backgorundFlashSale, setBackgorundFlashSale] = useState(null) + const router = useRouter(); + const auth = useAuth(); + const { slug } = router.query; + + const [quantity, setQuantity] = useState('1'); + const [selectedVariant, setSelectedVariant] = useState(null); + const [informationTab, setInformationTab] = useState( + informationTabOptions[0].value + ); + const [addCartAlert, setAddCartAlert] = useState(false); + + const [isLoadingSLA, setIsLoadingSLA] = useState(true); + const [promotionType, setPromotionType] = useState(false); + const [promotionActiveId, setPromotionActiveId] = useState(null); + const [backgorundFlashSale, setBackgorundFlashSale] = useState(null); const getLowestPrice = () => { - const prices = product.variants.map((variant) => variant.price) + const prices = product.variants.map((variant) => variant.price); const lowest = prices.reduce((lowest, price) => { - return price.priceDiscount < lowest.priceDiscount ? price : lowest - }, prices[0]) - return lowest - } + return price.priceDiscount < lowest.priceDiscount ? price : lowest; + }, prices[0]); + return lowest; + }; useEffect(() => { const getBackgound = async () => { - const get = await odooApi('GET', '/api/v1/banner?type=flash-sale-background-banner') + const get = await odooApi( + 'GET', + '/api/v1/banner?type=flash-sale-background-banner' + ); if (get.length > 0) { - setBackgorundFlashSale(get[0].image) + setBackgorundFlashSale(get[0].image); } - } - getBackgound() - }, []) + }; + getBackgound(); + }, []); const [activeVariant, setActiveVariant] = useState({ id: null, @@ -64,40 +70,44 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { stock: product.stockTotal, weight: product.weight, hasProgram: false, - qtySold: product.qtySold - }) + qtySold: product.qtySold, + }); const variantOptions = product.variants?.map((variant) => { - let label = [] + let label = []; if (variant.isFlashsale) { - label.push("<span class='blink-color-flash-sale'>🗲</span>") + label.push("<span class='blink-color-flash-sale'>🗲</span>"); } if (variant.code) { - label.push(`[${variant.code}]`) + label.push(`[${variant.code}]`); } if (variant.attributes.length > 0) { - label.push(variant.attributes.join(', ')) + label.push(variant.attributes.join(', ')); } else { - label.push(product.name) + label.push(product.name); } return { value: variant.id, - label: label.join(' ') - } - }) + label: label.join(' '), + }; + }); useEffect(() => { if (!selectedVariant && variantOptions.length == 1) { - setSelectedVariant(variantOptions[0]) + setSelectedVariant(variantOptions[0]); } - }, [selectedVariant, variantOptions]) + }, [selectedVariant, variantOptions]); useEffect(() => { if (selectedVariant) { - const variant = product.variants.find((variant) => variant.id == selectedVariant.value) + const variant = product.variants.find( + (variant) => variant.id == selectedVariant.value + ); const variantAttributes = - variant.attributes.length > 0 ? ' - ' + variant.attributes.join(', ') : '' + variant.attributes.length > 0 + ? ' - ' + variant.attributes.join(', ') + : ''; const newActiveVariant = { id: variant.id, @@ -108,60 +118,63 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { weight: variant.weight, hasProgram: variant.hasProgram, isFlashsale: variant.isFlashsale, - qtySold: variant.qtySold - } + qtySold: variant.qtySold, + }; - setActiveVariant(newActiveVariant) + setActiveVariant(newActiveVariant); const fetchSLA = async () => { - const dataSLA = await odooApi('GET', `/api/v1/product_variant/${variant.id}/stock`) - setActiveVariant({ ...newActiveVariant, sla: dataSLA }) - } - fetchSLA() + const dataSLA = await odooApi( + 'GET', + `/api/v1/product_variant/${variant.id}/stock` + ); + setActiveVariant({ ...newActiveVariant, sla: dataSLA }); + }; + fetchSLA(); } - }, [selectedVariant, product]) + }, [selectedVariant, product]); const validAction = () => { - let isValid = true + let isValid = true; if (!selectedVariant) { - toast.error('Pilih varian terlebih dahulu') - isValid = false + toast.error('Pilih varian terlebih dahulu'); + isValid = false; } if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) { - toast.error('Jumlah barang minimal 1') - isValid = false + toast.error('Jumlah barang minimal 1'); + isValid = false; } - return isValid - } + return isValid; + }; const redirectToLogin = (action) => { - const nextURL = `/shop/product/${slug}?action=${action}&variantId=${activeVariant.id}&qty=${quantity}` - router.push(`/login?next=${encodeURIComponent(nextURL)}`) - return true - } + const nextURL = `/shop/product/${slug}?action=${action}&variantId=${activeVariant.id}&qty=${quantity}`; + router.push(`/login?next=${encodeURIComponent(nextURL)}`); + return true; + }; const handleClickCart = () => { - if (!validAction()) return - gtagAddToCart(activeVariant, quantity) + if (!validAction()) return; + gtagAddToCart(activeVariant, quantity); if (!auth) { - return redirectToLogin('add_to_cart') + return redirectToLogin('add_to_cart'); } updateItemCart({ productId: activeVariant.id, quantity, programLineId: promotionActiveId, - selected: true - }) - setAddCartAlert(true) - } + selected: true, + }); + setAddCartAlert(true); + }; const handleClickBuy = () => { - if (!validAction()) return + if (!validAction()) return; if (!auth) { - return redirectToLogin('buy') + return redirectToLogin('buy'); } updateItemCart({ @@ -169,58 +182,60 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { quantity, programLineId: promotionActiveId, selected: true, - source: 'buy' - }) - router.push(`/shop/checkout?source=buy`) - } + source: 'buy', + }); + router.push(`/shop/checkout?source=buy`); + }; const productSimilarQuery = [ product?.name, `fq=-product_id_i:${product.id}`, - `fq=-manufacture_id_i:${product.manufacture?.id || 0}` - ].join('&') + `fq=-manufacture_id_i:${product.manufacture?.id || 0}`, + ].join('&'); return ( <MobileView> <Breadcrumb productId={product.id} productName={product.name} /> <div className='relative'> - {product?.flashSale?.remainingTime > 0 && activeVariant?.price.discountPercentage > 0 && ( - <div className={`absolute bottom-0 w-full`}> - <div className='absolute bottom-0 w-full'> - <ImageNext - src={backgorundFlashSale || '/images/GAMBAR-BG-FLASH-SALE.jpg'} - width={1000} - height={100} - /> - </div> - <div className='relative'> - <div className='flex gap-x-2 items-center p-2'> - <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '> - <span className='text-lg font-bold'> - {Math.floor(product.lowestPrice.discountPercentage)}% - </span> - </div> - <div - className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-4`} - > - <ImageNext - src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg' - width={17} - height={10} - /> - <span className='text-white text-lg font-semibold'> - {product?.flashSale?.tag != 'false' || product?.flashSale?.tag - ? product?.flashSale?.tag - : 'FLASH SALE'} - </span> - </div> - <div> - <CountDown2 initialTime={product.flashSale.remainingTime} /> + {product?.flashSale?.remainingTime > 0 && + activeVariant?.price.discountPercentage > 0 && ( + <div className={`absolute bottom-0 w-full`}> + <div className='absolute bottom-0 w-full'> + <ImageNext + src={backgorundFlashSale || '/images/BG-FLASH-SALE.jpg'} + width={1000} + height={100} + /> + </div> + <div className='relative'> + <div className='flex gap-x-2 items-center p-2'> + <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '> + <span className='text-lg font-bold'> + {Math.floor(product.lowestPrice.discountPercentage)}% + </span> + </div> + <div + className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-4`} + > + <ImageNext + src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg' + width={17} + height={10} + /> + <span className='text-white text-lg font-semibold'> + {product?.flashSale?.tag != 'false' || + product?.flashSale?.tag + ? product?.flashSale?.tag + : 'FLASH SALE'} + </span> + </div> + <div> + <CountDown2 initialTime={product.flashSale.remainingTime} /> + </div> </div> </div> </div> - </div> - )} + )} <Image src={product.image} alt={product.name} @@ -232,7 +247,11 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { <div className='flex items-end mb-2'> {product.manufacture?.name ? ( <Link - href={createSlug('/shop/brands/', product.manufacture?.name, product.manufacture?.id)} + href={createSlug( + '/shop/brands/', + product.manufacture?.name, + product.manufacture?.id + )} > {product.manufacture?.name} </Link> @@ -249,18 +268,25 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { </div> <h1 className='leading-6 font-medium mb-3'>{activeVariant?.name}</h1> {product?.qtySold > 0 && ( - <div className='text-gray_r-9'>{sellingProductFormat(activeVariant?.qtySold) + ' Terjual'}</div> + <div className='text-gray_r-9'> + {sellingProductFormat(activeVariant?.qtySold) + ' Terjual'} + </div> )} {product.variants.length > 1 && activeVariant.price.priceDiscount > 0 && !selectedVariant && ( - <div className='text-gray_r-12/80 text-caption-2 mt-2 mb-1'>Harga mulai dari: </div> + <div className='text-gray_r-12/80 text-caption-2 mt-2 mb-1'> + Harga mulai dari:{' '} + </div> )} - {activeVariant.isFlashsale && activeVariant?.price?.discountPercentage > 0 ? ( + {activeVariant.isFlashsale && + activeVariant?.price?.discountPercentage > 0 ? ( <> <div className='flex gap-x-1 items-center'> - <div className='badge-solid-red'>{Math.floor(activeVariant?.price?.discountPercentage)}%</div> + <div className='badge-solid-red'> + {Math.floor(activeVariant?.price?.discountPercentage)}% + </div> <div className='text-gray_r-11 line-through text-caption-1'> {currencyFormat(activeVariant?.price?.price)} </div> @@ -270,7 +296,9 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { </div> <div className='text-gray_r-9 text-base font-normal mt-1'> Termasuk PPN:{' '} - {currencyFormat(activeVariant?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + activeVariant?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( @@ -280,7 +308,9 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { {currencyFormat(activeVariant?.price?.price)} <div className='text-gray_r-9 text-base font-normal mt-1'> Termasuk PPN:{' '} - {currencyFormat(activeVariant?.price.price * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + activeVariant?.price.price * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( @@ -289,7 +319,12 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { <a href={whatsappUrl('product', { name: product.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 underline' > @@ -307,13 +342,17 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { <div> <label className='flex justify-between'> Pilih Varian: - <span className='text-gray_r-11'>{product?.variantTotal} Varian</span> + <span className='text-gray_r-11'> + {product?.variantTotal} Varian + </span> </label> <Select name='variant' classNamePrefix='form-select' options={variantOptions} - formatOptionLabel={({ label }) => <div dangerouslySetInnerHTML={{ __html: label }} />} + formatOptionLabel={({ label }) => ( + <div dangerouslySetInnerHTML={{ __html: label }} /> + )} className='mt-2' value={selectedVariant} onChange={(option) => setSelectedVariant(option)} @@ -342,15 +381,27 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { onChange={(e) => setQuantity(e.target.value)} /> </div> - <button type='button' className='btn-yellow flex-1' onClick={handleClickCart}> + <button + type='button' + className='btn-yellow flex-1' + onClick={handleClickCart} + > Keranjang </button> - <button type='button' className='btn-solid-red flex-1' onClick={handleClickBuy}> + <button + type='button' + className='btn-solid-red flex-1' + onClick={handleClickBuy} + > Beli </button> </div> + + <div className='h-4' /> </div> + <ProductPromoSection productId={activeVariant.id} /> + <Divider /> <div className='p-4'> @@ -380,12 +431,16 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { type='button' title={`Masa Persiapan Barang ${activeVariant?.sla?.slaDate}`} className={`flex gap-x-1 items-center p-2 h-8 rounded-lg w-full ${ - activeVariant?.sla?.slaDate === 'indent' ? 'bg-indigo-900' : 'btn-light' + activeVariant?.sla?.slaDate === 'indent' + ? 'bg-indigo-900' + : 'btn-light' }`} > <div className={`flex-1 text-sm ${ - activeVariant?.sla?.slaDate === 'indent' ? 'text-white' : '' + activeVariant?.sla?.slaDate === 'indent' + ? 'text-white' + : '' }`} > {activeVariant?.sla?.slaDate} @@ -397,7 +452,9 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { stroke='currentColor' stroke-width='1.5' className={`w-7 h-7 text-sm ${ - activeVariant?.sla?.slaDate === 'indent' ? 'text-white' : '' + activeVariant?.sla?.slaDate === 'indent' + ? 'text-white' + : '' }`} > <path @@ -436,7 +493,12 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { <a href={whatsappUrl('product', { name: product.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 font-medium' > @@ -445,12 +507,19 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { )} </SpecificationContent> <SpecificationContent label='Berat Barang'> - {activeVariant?.weight > 0 && <span>{activeVariant?.weight} KG</span>} + {activeVariant?.weight > 0 && ( + <span>{activeVariant?.weight} KG</span> + )} {activeVariant?.weight == 0 && ( <a href={whatsappUrl('productWeight', { name: product.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 font-medium' > @@ -464,7 +533,10 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { active={informationTab == 'description'} className='leading-6 text-gray_r-11' dangerouslySetInnerHTML={{ - __html: product.description != '' ? product.description : 'Belum ada deskripsi produk.' + __html: + product.description != '' + ? product.description + : 'Belum ada deskripsi produk.', }} /> </div> @@ -491,50 +563,63 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { className='h-20 object-contain object-center w-full border border-gray_r-4' /> </div> - <div className='ml-3 flex flex-1 items-center text-sm font-normal'>{product.name}</div> + <div className='ml-3 flex flex-1 items-center text-sm font-normal'> + {product.name} + </div> <div className='ml-3 flex items-center text-sm font-normal'> - <Link href='/shop/cart' className='flex-1 py-2 text-gray_r-12 btn-yellow'> + <Link + href='/shop/cart' + className='flex-1 py-2 text-gray_r-12 btn-yellow' + > Lihat Keranjang </Link> </div> </div> <div className='mt-8 mb-4'> - <div className='text-h-sm font-semibold mb-6'>Kamu Mungkin Juga Suka</div> + <div className='text-h-sm font-semibold mb-6'> + Kamu Mungkin Juga Suka + </div> <LazyLoad> <ProductSimilar query={productSimilarQuery} /> </LazyLoad> </div> </BottomPopup> </MobileView> - ) -} + ); +}; const informationTabOptions = [ { value: 'specification', label: 'Spesifikasi' }, { value: 'description', label: 'Deskripsi' }, - { value: 'information', label: 'Info Penting' } -] + { value: 'information', label: 'Info Penting' }, +]; const TabButton = ({ children, active, ...props }) => { - const activeClassName = active ? 'text-danger-500 underline underline-offset-4' : 'text-gray_r-11' + const activeClassName = active + ? 'text-danger-500 underline underline-offset-4' + : 'text-gray_r-11'; return ( - <button {...props} type='button' className={`font-medium pb-1 ${activeClassName}`}> + <button + {...props} + type='button' + className={`font-medium pb-1 ${activeClassName}`} + > {children} </button> - ) -} + ); +}; const TabContent = ({ children, active, className, ...props }) => ( <div {...props} className={`${active ? 'block' : 'hidden'} ${className}`}> {children} </div> -) +); const SpecificationContent = ({ children, label }) => ( <div className='flex justify-between p-3 items-center'> <span className='text-gray_r-11'>{label}</span> {children} </div> -) +); -export default ProductMobile +export default ProductMobile; diff --git a/src/lib/product/components/Product/ProductMobileVariant.jsx b/src/lib/product/components/Product/ProductMobileVariant.jsx index 9888e482..af9e52bb 100644 --- a/src/lib/product/components/Product/ProductMobileVariant.jsx +++ b/src/lib/product/components/Product/ProductMobileVariant.jsx @@ -1,37 +1,40 @@ -import Divider from '@/core/components/elements/Divider/Divider' -import Image from '@/core/components/elements/Image/Image' -import Link from '@/core/components/elements/Link/Link' -import currencyFormat from '@/core/utils/currencyFormat' -import { useEffect, useState } from 'react' -import Select from 'react-select' -import ProductSimilar from '../ProductSimilar' -import LazyLoad from 'react-lazy-load' -import { updateItemCart } from '@/core/utils/cart' -import { HeartIcon } from '@heroicons/react/24/outline' -import { useRouter } from 'next/router' -import MobileView from '@/core/components/views/MobileView' -import { toast } from 'react-hot-toast' -import { createSlug } from '@/core/utils/slug' -import BottomPopup from '@/core/components/elements/Popup/BottomPopup' -import whatsappUrl from '@/core/utils/whatsappUrl' -import { gtagAddToCart } from '@/core/utils/googleTag' -import odooApi from '@/core/api/odooApi' -import { Skeleton } from '@chakra-ui/react' +import { 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 odooApi from '@/core/api/odooApi'; +import Divider from '@/core/components/elements/Divider/Divider'; +import Image from '@/core/components/elements/Image/Image'; +import Link from '@/core/components/elements/Link/Link'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import MobileView from '@/core/components/views/MobileView'; +import { updateItemCart } from '@/core/utils/cart'; +import currencyFormat from '@/core/utils/currencyFormat'; +import { gtagAddToCart } from '@/core/utils/googleTag'; +import { createSlug } from '@/core/utils/slug'; +import whatsappUrl from '@/core/utils/whatsappUrl'; + +import ProductSimilar from '../ProductSimilar'; const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { - const router = useRouter() + const router = useRouter(); - const [quantity, setQuantity] = useState('1') - const [selectedVariant, setSelectedVariant] = useState(product.id) - const [informationTab, setInformationTab] = useState(informationTabOptions[0].value) - const [addCartAlert, setAddCartAlert] = useState(false) + const [quantity, setQuantity] = useState('1'); + const [selectedVariant, setSelectedVariant] = useState(product.id); + const [informationTab, setInformationTab] = useState( + informationTabOptions[0].value + ); + const [addCartAlert, setAddCartAlert] = useState(false); - const [isLoadingSLA, setIsLoadingSLA] = useState(true) + const [isLoadingSLA, setIsLoadingSLA] = useState(true); const getLowestPrice = () => { - const lowest = product.lowestPrice - return lowest - } + const lowest = product.lowestPrice; + return lowest; + }; const [activeVariant, setActiveVariant] = useState({ id: null, @@ -40,8 +43,8 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { price: getLowestPrice(), stock: product.stockTotal, weight: product.weight, - isFlashSale: product.isFlashSale - }) + isFlashSale: product.isFlashSale, + }); useEffect(() => { if (selectedVariant) { @@ -52,70 +55,73 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { price: product.price, stock: product.stockTotal, weight: product.weight, - isFlashSale: product.isFlashSale - }) + isFlashSale: product.isFlashSale, + }); } - }, [selectedVariant, product]) + }, [selectedVariant, product]); const validAction = () => { - let isValid = true + let isValid = true; if (!selectedVariant) { - toast.error('Pilih varian terlebih dahulu') - isValid = false + toast.error('Pilih varian terlebih dahulu'); + isValid = false; } if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) { - toast.error('Jumlah barang minimal 1') - isValid = false + toast.error('Jumlah barang minimal 1'); + isValid = false; } - return isValid - } + return isValid; + }; const handleClickCart = () => { - if (!validAction()) return - gtagAddToCart(activeVariant, quantity) + if (!validAction()) return; + gtagAddToCart(activeVariant, quantity); updateItemCart({ productId: variant, quantity, programLineId: null, selected: true, - source: null - }) - setAddCartAlert(true) - } + source: null, + }); + setAddCartAlert(true); + }; const handleClickBuy = () => { - if (!validAction()) return + if (!validAction()) return; updateItemCart({ productId: product.id, quantity, programLineId: null, selected: true, - source: 'buy' - }) - router.push(`/shop/checkout?source=buy`) - } + source: 'buy', + }); + router.push(`/shop/checkout?source=buy`); + }; const productSimilarQuery = [ product?.name, `fq=-product_id_i:${product.id}`, - `fq=-manufacture_id_i:${product.manufacture?.id || 0}` - ].join('&') + `fq=-manufacture_id_i:${product.manufacture?.id || 0}`, + ].join('&'); useEffect(() => { const fetchData = async () => { - const dataSLA = await odooApi('GET', `/api/v1/product_variant/${product.id}/stock`) - product.sla = dataSLA + const dataSLA = await odooApi( + 'GET', + `/api/v1/product_variant/${product.id}/stock` + ); + product.sla = dataSLA; - setIsLoadingSLA(false) - } - fetchData() - }, [product]) + setIsLoadingSLA(false); + }; + fetchData(); + }, [product]); return ( <MobileView> <Image - src={product.image} + src={product.image + '?variant=True'} alt={product.name} className='h-72 object-contain object-center w-full border-b border-gray_r-4' /> @@ -124,7 +130,11 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { <div className='flex items-end mb-2'> {product.manufacture?.name ? ( <Link - href={createSlug('/shop/brands/', product.manufacture?.name, product.manufacture?.id)} + href={createSlug( + '/shop/brands/', + product.manufacture?.name, + product.manufacture?.id + )} > {product.manufacture?.name} </Link> @@ -141,10 +151,13 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { </div> <h1 className='leading-6 font-medium mb-3'>{activeVariant?.name}</h1> - {activeVariant.isFlashSale && activeVariant?.price?.discountPercentage > 0 ? ( + {activeVariant.isFlashSale && + activeVariant?.price?.discountPercentage > 0 ? ( <> <div className='flex gap-x-1 items-center'> - <div className='badge-solid-red'>{activeVariant?.price?.discountPercentage}%</div> + <div className='badge-solid-red'> + {activeVariant?.price?.discountPercentage}% + </div> <div className='text-gray_r-11 line-through text-caption-1'> {currencyFormat(activeVariant?.price?.price)} </div> @@ -154,7 +167,9 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { </div> <div className='text-gray_r-9 text-base font-normal mt-1'> Termasuk PPN:{' '} - {currencyFormat(activeVariant?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + activeVariant?.price.priceDiscount * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( @@ -164,7 +179,9 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { {currencyFormat(activeVariant?.price?.price)} <div className='text-gray_r-9 text-base font-normal mt-1'> Termasuk PPN:{' '} - {currencyFormat(activeVariant?.price.price * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + activeVariant?.price.price * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( @@ -173,7 +190,12 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { <a href={whatsappUrl('product', { name: product.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 underline' > @@ -199,10 +221,18 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { onChange={(e) => setQuantity(e.target.value)} /> </div> - <button type='button' className='btn-yellow flex-1' onClick={handleClickCart}> + <button + type='button' + className='btn-yellow flex-1' + onClick={handleClickCart} + > Keranjang </button> - <button type='button' className='btn-solid-red flex-1' onClick={handleClickBuy}> + <button + type='button' + className='btn-solid-red flex-1' + onClick={handleClickBuy} + > Beli </button> </div> @@ -238,7 +268,9 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { type='button' title={`Masa Persiapan Barang ${product?.sla?.slaDate}`} className={`flex gap-x-1 items-center p-2 h-8 rounded-lg w-full ${ - product?.sla?.slaDate === 'indent' ? 'bg-indigo-900' : 'btn-light' + product?.sla?.slaDate === 'indent' + ? 'bg-indigo-900' + : 'btn-light' }`} > <div @@ -281,14 +313,21 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { {activeVariant?.stock > 0 && ( <span className='flex gap-x-1.5'> <div className='badge-solid-red'>Ready Stock</div> - <div className='badge-gray'>{activeVariant?.stock > 5 ? '> 5' : '< 5'}</div> + <div className='badge-gray'> + {activeVariant?.stock > 5 ? '> 5' : '< 5'} + </div> </span> )} {activeVariant?.stock == 0 && ( <a href={whatsappUrl('product', { name: product.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 font-medium' > @@ -297,12 +336,19 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { )} </SpecificationContent> <SpecificationContent label='Berat Barang'> - {activeVariant?.weight > 0 && <span>{activeVariant?.weight} KG</span>} + {activeVariant?.weight > 0 && ( + <span>{activeVariant?.weight} KG</span> + )} {activeVariant?.weight == 0 && ( <a href={whatsappUrl('productWeight', { name: product.name, - url: createSlug('/shop/product/', product.name, product.id, true) + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ), })} className='text-danger-500 font-medium' > @@ -316,7 +362,10 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { active={informationTab == 'description'} className='leading-6 text-gray_r-11' dangerouslySetInnerHTML={{ - __html: product.description != '' ? product.description : 'Belum ada deskripsi produk.' + __html: + product.description != '' + ? product.description + : 'Belum ada deskripsi produk.', }} /> </div> @@ -338,55 +387,68 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { <div className='flex mt-4'> <div className='w-[15%]'> <Image - src={product.image} + src={product.image + '?variant=True'} alt={product.name} className='h-20 object-contain object-center w-full border border-gray_r-4' /> </div> - <div className='ml-3 flex flex-1 items-center text-sm font-normal'>{product.name}</div> + <div className='ml-3 flex flex-1 items-center text-sm font-normal'> + {product.name} + </div> <div className='ml-3 flex items-center text-sm font-normal'> - <Link href='/shop/cart' className='flex-1 py-2 text-gray_r-12 btn-yellow'> + <Link + href='/shop/cart' + className='flex-1 py-2 text-gray_r-12 btn-yellow' + > Lihat Keranjang </Link> </div> </div> <div className='mt-8 mb-4'> - <div className='text-h-sm font-semibold mb-6'>Kamu Mungkin Juga Suka</div> + <div className='text-h-sm font-semibold mb-6'> + Kamu Mungkin Juga Suka + </div> <LazyLoad> <ProductSimilar query={productSimilarQuery} /> </LazyLoad> </div> </BottomPopup> </MobileView> - ) -} + ); +}; const informationTabOptions = [ - { value: 'specification', label: 'Spesifikasi' } + { value: 'specification', label: 'Spesifikasi' }, // { value: 'description', label: 'Deskripsi' }, // { value: 'information', label: 'Info Penting' } -] +]; const TabButton = ({ children, active, ...props }) => { - const activeClassName = active ? 'text-danger-500 underline underline-offset-4' : 'text-gray_r-11' + const activeClassName = active + ? 'text-danger-500 underline underline-offset-4' + : 'text-gray_r-11'; return ( - <button {...props} type='button' className={`font-medium pb-1 ${activeClassName}`}> + <button + {...props} + type='button' + className={`font-medium pb-1 ${activeClassName}`} + > {children} </button> - ) -} + ); +}; const TabContent = ({ children, active, className, ...props }) => ( <div {...props} className={`${active ? 'block' : 'hidden'} ${className}`}> {children} </div> -) +); const SpecificationContent = ({ children, label }) => ( <div className='flex justify-between p-3'> <span className='text-gray_r-11'>{label}</span> {children} </div> -) +); -export default ProductMobileVariant +export default ProductMobileVariant; diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index fa555bcf..98732407 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -1,38 +1,85 @@ -import Image from '@/core/components/elements/Image/Image' -import Link from '@/core/components/elements/Link/Link' -import currencyFormat from '@/core/utils/currencyFormat' -import { sellingProductFormat } from '@/core/utils/formatValue' -import { createSlug } from '@/core/utils/slug' -import whatsappUrl from '@/core/utils/whatsappUrl' -import ImageNext from 'next/image' -import { useRouter } from 'next/router' +import clsx from 'clsx'; +import ImageNext from 'next/image'; +import { useRouter } from 'next/router'; +import { useMemo, useEffect, useState } from 'react'; + +import Image from '@/core/components/elements/Image/Image'; +import Link from '@/core/components/elements/Link/Link'; +import currencyFormat from '@/core/utils/currencyFormat'; +import { sellingProductFormat } from '@/core/utils/formatValue'; +import { createSlug } from '@/core/utils/slug'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import useUtmSource from '~/hooks/useUtmSource'; const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { - const router = useRouter() + const router = useRouter(); + const utmSource = useUtmSource(); + const callForPriceWhatsapp = whatsappUrl('product', { name: product.name, manufacture: product.manufacture?.name, - url: createSlug('/shop/product/', product.name, product.id, true) - }) + url: createSlug('/shop/product/', product.name, product.id, true), + }); + + const image = useMemo(() => { + if (product.image) return product.image + '?ratio=square'; + return '/images/noimage.jpeg'; + }, [product.image]); + + const URL = { + product: + createSlug('/shop/product/', product?.name, product?.id) + + `?utm_source=${utmSource}`, + manufacture: createSlug( + '/shop/brands/', + product?.manufacture?.name, + product?.manufacture.id + ), + }; if (variant == 'vertical') { return ( <div className='rounded shadow-sm border border-gray_r-4 bg-white h-[300px] md:h-[350px]'> - <Link - href={createSlug('/shop/product/', product?.name, product?.id)} - className='border-b border-gray_r-4 relative' - > + <Link href={URL.product} className='border-b border-gray_r-4 relative'> + <div className="relative"> <Image - src={product?.image} + src={image} alt={product?.name} - className='w-full object-contain object-center h-36 sm:h-48' + className="gambarA w-full object-contain object-center h-36 sm:h-48" /> + <div className="absolute top-0 right-0 flex mt-3"> + <div className="gambarB "> + {product?.isSni && ( + <ImageNext + src="/images/sni-logo.png" + alt="SNI Logo" + className="w-4 h-5 object-contain object-top sm:h-6" + width={50} + height={50} + /> + )} + </div> + <div className="gambarC "> + {product?.isTkdn && ( + <ImageNext + src="/images/TKDN.png" + alt="TKDN" + className="w-11 h-6 object-contain object-top ml-1 mr-1 sm:h-6" + width={50} + height={50} + /> + )} + </div> + </div> + </div> + + {router.pathname != '/' && product?.flashSale?.id > 0 && ( <div className='absolute bottom-0 w-full grid'> <div className='absolute bottom-0 w-full h-full'> <ImageNext - src='/images/GAMBAR-BG-FLASH-SALE.jpg' + src='/images/BG-FLASH-SALE.jpg' className='h-full' width={1000} height={100} @@ -52,7 +99,8 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { height={5} /> <span className='text-white text-[9px] md:text-[10px] font-semibold'> - {product?.flashSale?.tag != 'false' || product?.flashSale?.tag + {product?.flashSale?.tag != 'false' || + product?.flashSale?.tag ? product?.flashSale?.tag : 'FLASH SALE'} </span> @@ -69,27 +117,21 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { </Link> <div className='p-2 sm:p-3 pb-3 text-caption-2 sm:text-body-2 leading-5'> {product?.manufacture?.name ? ( - <Link - href={createSlug( - '/shop/brands/', - product?.manufacture?.name, - product?.manufacture.id - )} - className='mb-1' - > + <Link href={URL.manufacture} className='mb-1'> {product.manufacture.name} </Link> ) : ( <div>-</div> )} <Link - href={createSlug('/shop/product/', product?.name, product?.id)} + href={URL.product} className={`mb-2 !text-gray_r-12 leading-6 block line-clamp-3 h-[64px]`} title={product?.name} > {product?.name} </Link> - {product?.flashSale?.id > 0 && product?.lowestPrice.discountPercentage > 0 ? ( + {product?.flashSale?.id > 0 && + product?.lowestPrice.discountPercentage > 0 ? ( <> <div className='flex gap-x-1 mb-1 items-center'> <div className='text-gray_r-11 line-through text-[11px] sm:text-caption-2'> @@ -103,7 +145,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { {product?.lowestPrice.priceDiscount > 0 ? ( currencyFormat(product?.lowestPrice.priceDiscount) ) : ( - <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}> + <a + rel='noopener noreferrer' + target='_blank' + href={callForPriceWhatsapp} + > Call for Inquiry </a> )} @@ -116,11 +162,17 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { {currencyFormat(product?.lowestPrice.price)} <div className='text-gray_r-9 text-[10px] font-normal mt-2'> Inc. PPN:{' '} - {currencyFormat(product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( - <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}> + <a + rel='noopener noreferrer' + target='_blank' + href={callForPriceWhatsapp} + > Call for Inquiry </a> )} @@ -128,7 +180,9 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { )} <div className='flex w-full items-center gap-x-1 '> - {product?.stockTotal > 0 && <div className='badge-solid-red'>Ready Stock</div>} + {product?.stockTotal > 0 && ( + <div className='badge-solid-red'>Ready Stock</div> + )} {/* <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div> */} {product?.qtySold > 0 && ( <div className='text-gray_r-9 text-[11px]'> @@ -138,22 +192,45 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { </div> </div> </div> - ) + ); } if (variant == 'horizontal') { return ( <div className='flex bg-white'> <div className='w-4/12'> - <Link - href={createSlug('/shop/product/', product?.name, product?.id)} - className='relative' - > + <Link href={URL.product} className='relative'> + <div className="relative"> <Image - src={product?.image} + src={image} alt={product?.name} - className='w-full object-contain object-center h-36' + className="gambarA w-full object-contain object-center h-36 sm:h-48" /> + <div className="absolute top-0 right-0 flex mt-3"> + <div className="gambarB "> + {product?.isSni && ( + <ImageNext + src="/images/sni-logo.png" + alt="SNI Logo" + className="w-4 h-5 object-contain object-top sm:h-6" + width={50} + height={50} + /> + )} + </div> + <div className="gambarC "> + {product?.isTkdn && ( + <ImageNext + src="/images/TKDN.png" + alt="TKDN" + className="w-11 h-6 object-contain object-top ml-1 sm:h-6" + width={50} + height={50} + /> + )} + </div> + </div> + </div> {product.variantTotal > 1 && ( <div className='absolute badge-gray bottom-1.5 left-1.5'> {product.variantTotal} Varian @@ -178,26 +255,20 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { </div> )} {product?.manufacture?.name ? ( - <Link - href={createSlug( - '/shop/brands/', - product?.manufacture?.name, - product?.manufacture.id - )} - className='mb-1' - > + <Link href={URL.manufacture} className='mb-1'> {product.manufacture.name} </Link> ) : ( <div>-</div> )} <Link - href={createSlug('/shop/product/', product?.name, product?.id)} + href={URL.product} className={`mb-3 !text-gray_r-12 leading-6 line-clamp-3`} > {product?.name} </Link> - {product?.flashSale?.id > 0 && product?.lowestPrice?.discountPercentage > 0 ? ( + {product?.flashSale?.id > 0 && + product?.lowestPrice?.discountPercentage > 0 ? ( <> {product?.lowestPrice.discountPercentage > 0 && ( <div className='flex gap-x-1 mb-1 items-center'> @@ -214,7 +285,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { {product?.lowestPrice?.priceDiscount > 0 ? ( currencyFormat(product?.lowestPrice?.priceDiscount) ) : ( - <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}> + <a + rel='noopener noreferrer' + target='_blank' + href={callForPriceWhatsapp} + > Call for Inquiry </a> )} @@ -227,11 +302,17 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { {currencyFormat(product?.lowestPrice.price)} <div className='text-gray_r-9 text-[11px] sm:text-caption-2 font-normal mt-2'> Inc. PPN:{' '} - {currencyFormat(product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN)} + {currencyFormat( + product.lowestPrice.price * process.env.NEXT_PUBLIC_PPN + )} </div> </> ) : ( - <a rel='noopener noreferrer' target='_blank' href={callForPriceWhatsapp}> + <a + rel='noopener noreferrer' + target='_blank' + href={callForPriceWhatsapp} + > Call for Inquiry </a> )} @@ -239,7 +320,9 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { )} <div className='flex w-full items-center gap-x-1 '> - {product?.stockTotal > 0 && <div className='badge-solid-red'>Ready Stock</div>} + {product?.stockTotal > 0 && ( + <div className='badge-solid-red'>Ready Stock</div> + )} {/* <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div> */} {product?.qtySold > 0 && ( <div className='text-gray_r-9 text-[11px]'> @@ -249,8 +332,8 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { </div> </div> </div> - ) + ); } -} +}; -export default ProductCard +export default ProductCard; diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx index e4a62abb..a8073036 100644 --- a/src/lib/product/components/ProductFilterDesktop.jsx +++ b/src/lib/product/components/ProductFilterDesktop.jsx @@ -21,6 +21,7 @@ import Image from '@/core/components/elements/Image/Image' import { formatCurrency } from '@/core/utils/formatValue' const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = null }) => { + const router = useRouter() const { query } = router const [order, setOrder] = useState(query?.orderBy) @@ -102,7 +103,14 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu } params = _.pickBy(params, _.identity) params = toQuery(params) - router.push(`${prefixUrl}?${params}`) + + const slug = Array.isArray(router.query.slug) ? router.query.slug[0] : router.query.slug; + + if (slug) { + router.push(`${prefixUrl}/${slug}?${params}`) + } else { + router.push(`${prefixUrl}?${params}`) + } } diff --git a/src/lib/product/components/ProductFilterDesktopPromotion.jsx b/src/lib/product/components/ProductFilterDesktopPromotion.jsx new file mode 100644 index 00000000..0815b881 --- /dev/null +++ b/src/lib/product/components/ProductFilterDesktopPromotion.jsx @@ -0,0 +1,132 @@ +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import _ from 'lodash'; +import { toQuery } from 'lodash-contrib'; +import { Button } from '@chakra-ui/react'; +import { MultiSelect } from 'react-multi-select-component'; + +const ProductFilterDesktop = ({ brands, categories, prefixUrl }) => { + const router = useRouter(); + const { query } = router; + const [order, setOrder] = useState(query?.orderBy); + const [brandValues, setBrand] = useState([]); + const [categoryValues, setCategory] = useState([]); + const [priceFrom, setPriceFrom] = useState(query?.priceFrom); + const [priceTo, setPriceTo] = useState(query?.priceTo); + const [stock, setStock] = useState(query?.stock); + const [activeRange, setActiveRange] = useState(null); + const [isBrandDropdownClicked, setIsBrandDropdownClicked] = useState(false); + const [isCategoryDropdownClicked, setIsCategoryDropdownClicked] = useState(false); + + // Effect to set brandValues from query parameter 'brand' + useEffect(() => { + const brandParam = query?.brand; + if (brandParam) { + const brandsArray = brandParam.split(',').map((b) => ({ + label: b, + value: b, + })); + setBrand(brandsArray); + } + + }, [query.brand]); // Trigger effect whenever query.brand changes + + useEffect(() => { + const categoryParam = query?.category; + if (categoryParam) { + const categoriesArray = categoryParam.split(',').map((c) => ({ + label: c, + value: c, + })); + setCategory(categoriesArray); + } + }, [query.category]); // Trigger effect whenever query.category changes + + const handleSubmit = () => { + let params = { + q: router.query.q, + orderBy: order, + brand: brandValues.map((b) => b.value).join(','), + category: categoryValues.map((c) => c.value).join(','), + priceFrom, + priceTo, + stock: stock, + }; + params = _.pickBy(params, _.identity); + params = toQuery(params); + + const slug = Array.isArray(router.query.slug) + ? router.query.slug[0] + : router.query.slug; + + if (slug) { + router.push(`${prefixUrl}/${slug}?${params}`); + } else { + router.push(`${prefixUrl}?${params}`); + } + }; + + + const brandOptions = brands.map((brand) => ({ + label: `${brand.brand} (${brand.qty})`, + value: brand.brand, + })); + + const categoryOptions = categories.map((category) => ({ + label: `${category.name} (${category.qty})`, + value: category.name, + })); + + return ( + <> + <div className='flex h-full w-[100%] justify-end '> + {/* Brand MultiSelect */} + <div className='mb-[20px] mr-4 w-64 h-full flex justify-start '> + <div className='relative'> + <label>Brand</label> + <div className='h-auto z-50 w-64 '> + <MultiSelect + options={brandOptions} + value={brandValues} + onChange={setBrand} + labelledBy='Select Brand' + onMenuToggle={(isOpen) => setIsBrandDropdownClicked(isOpen)} + hasSelectAll={false} + /> + </div> + </div> + </div> + + {/* Category MultiSelect */} + <div className='mb-[20px] mr-4 w-64 h-full flex justify-start '> + <div className='relative'> + <label>Kategori</label> + <div className=' h-auto w-64'> + <MultiSelect + options={categoryOptions} + value={categoryValues} + onChange={setCategory} + labelledBy='Select Kategori' + onMenuToggle={() => + setIsCategoryDropdownClicked(!isCategoryDropdownClicked) + } + hasSelectAll={false} + /> + </div> + </div> + </div> + + {/* Apply Button */} + <div className='TOMBOL mb-1 h-24 flex justify-center items-center w-24'> + <div className=' bottom-1 pb-1 left-0 right-0 flex justify-center rounded' > + <Button colorScheme='red' width={"full"} onClick={handleSubmit}> + Terapkan + </Button> + </div> + </div> + </div> + </> + ); +}; + +export default ProductFilterDesktop; diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index 29bb987e..b1a5d409 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -1,171 +1,204 @@ -import { useEffect, useMemo, useState } from 'react' -import useProductSearch from '../hooks/useProductSearch' -import ProductCard from './ProductCard' -import Pagination from '@/core/components/elements/Pagination/Pagination' -import { toQuery } from 'lodash-contrib' -import _ from 'lodash' -import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton' -import ProductFilter from './ProductFilter' -import useActive from '@/core/hooks/useActive' -import MobileView from '@/core/components/views/MobileView' -import DesktopView from '@/core/components/views/DesktopView' -import NextImage from 'next/image' -import ProductFilterDesktop from './ProductFilterDesktop' -import { useRouter } from 'next/router' -import searchSpellApi from '@/core/api/searchSpellApi' -import Link from '@/core/components/elements/Link/Link' -import whatsappUrl from '@/core/utils/whatsappUrl' -import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react' -import odooApi from '@/core/api/odooApi' -import { formatCurrency } from '@/core/utils/formatValue' -import axios from 'axios' -import Skeleton from 'react-loading-skeleton' -import { createSlug } from '@/core/utils/slug' - -const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) => { - const router = useRouter() - const { page = 1 } = query - const [q, setQ] = useState(query?.q || '*') - const [search, setSearch] = useState(query?.q || '*') - const [limit, setLimit] = useState(query?.limit || 30) - const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular') - if (defaultBrand) query.brand = defaultBrand.toLowerCase() +import NextImage from 'next/image'; +import { useRouter } from 'next/router'; +import { useEffect, useMemo, useState } from 'react'; + +import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react'; +import axios from 'axios'; +import _ from 'lodash'; +import { toQuery } from 'lodash-contrib'; + +import odooApi from '@/core/api/odooApi'; +import searchSpellApi from '@/core/api/searchSpellApi'; +import Link from '@/core/components/elements/Link/Link'; +import Pagination from '@/core/components/elements/Pagination/Pagination'; +import DesktopView from '@/core/components/views/DesktopView'; +import MobileView from '@/core/components/views/MobileView'; +import useActive from '@/core/hooks/useActive'; +import { formatCurrency } from '@/core/utils/formatValue'; +import { createSlug } from '@/core/utils/slug'; +import whatsappUrl from '@/core/utils/whatsappUrl'; + +import useProductSearch from '../hooks/useProductSearch'; +import ProductCard from './ProductCard'; +import ProductFilter from './ProductFilter'; +import ProductFilterDesktop from './ProductFilterDesktop'; +import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton'; + +import SideBanner from '~/modules/side-banner'; +import FooterBanner from '~/modules/footer-banner'; + +const ProductSearch = ({ + query, + prefixUrl, + defaultBrand = null, + brand = null, +}) => { + const router = useRouter(); + const { page = 1 } = query; + const [q, setQ] = useState(query?.q || '*'); + const [search, setSearch] = useState(query?.q || '*'); + const [limit, setLimit] = useState(query?.limit || 30); + const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular'); + if (defaultBrand) query.brand = defaultBrand.toLowerCase(); const { productSearch } = useProductSearch({ query: { ...query, q, limit, orderBy }, - operation: 'AND' - }) - const [products, setProducts] = useState(null) - const [spellings, setSpellings] = useState(null) - const [bannerPromotionHeader, setBannerPromotionHeader] = useState(null) - const [bannerPromotionFooter, setBannerPromotionFooter] = useState(null) - const [isBrand, setIsBrand] = useState(null) - const popup = useActive() - const numRows = [30, 50, 80, 100] + operation: 'AND', + }); + const [products, setProducts] = useState(null); + const [spellings, setSpellings] = useState(null); + const [bannerPromotionHeader, setBannerPromotionHeader] = useState(null); + const [bannerPromotionFooter, setBannerPromotionFooter] = useState(null); + const [isBrand, setIsBrand] = useState(null); + const popup = useActive(); + const numRows = [30, 50, 80, 100]; const [brandValues, setBrand] = useState( - !router.pathname.includes('brands') ? (query.brand ? query.brand.split(',') : []) : [] - ) - const [categoryValues, setCategory] = useState(query?.category?.split(',') || []) - const [priceFrom, setPriceFrom] = useState(query?.priceFrom || null) - const [priceTo, setPriceTo] = useState(query?.priceTo || null) - - const pageCount = Math.ceil(productSearch.data?.response.numFound / limit) - const productStart = productSearch.data?.responseHeader.params.start - const productRows = limit - const productFound = productSearch.data?.response.numFound + !router.pathname.includes('brands') + ? query.brand + ? query.brand.split(',') + : [] + : [] + ); + const [categoryValues, setCategory] = useState( + query?.category?.split(',') || [] + ); + const [priceFrom, setPriceFrom] = useState(query?.priceFrom || null); + const [priceTo, setPriceTo] = useState(query?.priceTo || null); + + const pageCount = Math.ceil(productSearch.data?.response.numFound / limit); + const productStart = productSearch.data?.responseHeader.params.start; + const productRows = limit; + const productFound = productSearch.data?.response.numFound; useEffect(() => { if (productFound == 0 && query.q && !spellings) { searchSpellApi({ query: query.q }).then((response) => { const oddIndexSuggestions = response.data.spellcheck.suggestions.filter( (_, index) => index % 2 === 1 - ) + ); const oddIndexCollations = response.data.spellcheck.collations.filter( (_, index) => index % 2 === 1 - ) + ); const dataSpellings = oddIndexSuggestions.reduce((acc, curr) => { oddIndexCollations.forEach((collation) => { - acc.push(collation.collationQuery) - }) + acc.push(collation.collationQuery); + }); curr.suggestion.forEach((s) => { - if (!acc.includes(s.word)) acc.push(s.word) - }) - return acc - }, []) + if (!acc.includes(s.word)) acc.push(s.word); + }); + return acc; + }, []); if (dataSpellings.length > 0) { - setQ(dataSpellings[0]) + setQ(dataSpellings[0]); } - setSpellings(dataSpellings) - }) + setSpellings(dataSpellings); + }); } - }, [productFound, query, spellings]) + }, [productFound, query, spellings]); useEffect(() => { const checkIfBrand = async () => { const brand = await axios( `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?params=search&q=${search}` - ) - console.log('ini brand', brand) + ); + if (brand.data.length > 0) { - setIsBrand(brand?.data[0]) + setIsBrand(brand?.data[0]); } else { - setIsBrand(null) + setIsBrand(null); } + }; + if (router.pathname.includes('search') && q !== '*') { + checkIfBrand(); } - if (router.pathname.includes('search')) { - checkIfBrand() - } - }, [q]) + }, [q]); - const brands = [] + const brands = []; for ( let i = 0; i < productSearch.data?.facetCounts?.facetFields?.manufactureNameS.length; i += 2 ) { - const brand = productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i] - const qty = productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i + 1] + const brand = + productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i]; + const qty = + productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i + 1]; if (qty > 0) { - brands.push({ brand, qty }) + brands.push({ brand, qty }); } } + - const categories = [] - for (let i = 0; i < productSearch.data?.facetCounts?.facetFields?.categoryName.length; i += 2) { - const name = productSearch.data?.facetCounts?.facetFields?.categoryName[i] - const qty = productSearch.data?.facetCounts?.facetFields?.categoryName[i + 1] + const categories = []; + for ( + let i = 0; + i < productSearch.data?.facetCounts?.facetFields?.categoryName.length; + i += 2 + ) { + const name = productSearch.data?.facetCounts?.facetFields?.categoryName[i]; + const qty = + productSearch.data?.facetCounts?.facetFields?.categoryName[i + 1]; if (qty > 0) { - categories.push({ name, qty }) + categories.push({ name, qty }); } } + const orderOptions = [ { value: 'price-asc', label: 'Harga Terendah' }, { value: 'price-desc', label: 'Harga Tertinggi' }, { value: 'popular', label: 'Populer' }, - { value: 'stock', label: 'Ready Stock' } - ] + { value: 'stock', label: 'Ready Stock' }, + ]; const handleOrderBy = (e) => { let params = { ...router.query, - orderBy: e.target.value - } - params = _.pickBy(params, _.identity) - params = toQuery(params) - router.push(`${prefixUrl}?${params}`) - } + orderBy: e.target.value, + }; + params = _.pickBy(params, _.identity); + params = toQuery(params); + router.push(`${prefixUrl}?${params}`); + }; const handleLimit = (e) => { let params = { ...router.query, - limit: e.target.value - } - params = _.pickBy(params, _.identity) - params = toQuery(params) - router.push(`${prefixUrl}?${params}`) - } + limit: e.target.value, + }; + params = _.pickBy(params, _.identity); + params = toQuery(params); + router.push(`${prefixUrl}?${params}`); + }; const getBanner = async () => { if (router.pathname.includes('search')) { - const getBannerHeader = await odooApi('GET', '/api/v1/banner?type=promotion-header') - const getBannerFooter = await odooApi('GET', '/api/v1/banner?type=promotion-footer') - var randomIndex = Math.floor(Math.random() * getBannerHeader.length) - var randomIndexFooter = Math.floor(Math.random() * getBannerFooter.length) - setBannerPromotionHeader(getBannerHeader[randomIndex]) - setBannerPromotionFooter(getBannerFooter[randomIndexFooter]) + const getBannerHeader = await odooApi( + 'GET', + '/api/v1/banner?type=promotion-header' + ); + const getBannerFooter = await odooApi( + 'GET', + '/api/v1/banner?type=promotion-footer' + ); + var randomIndex = Math.floor(Math.random() * getBannerHeader.length); + var randomIndexFooter = Math.floor( + Math.random() * getBannerFooter.length + ); + setBannerPromotionHeader(getBannerHeader[randomIndex]); + setBannerPromotionFooter(getBannerFooter[randomIndexFooter]); } - } + }; useEffect(() => { - getBanner() - }, []) + getBanner(); + }, []); useEffect(() => { - setProducts(productSearch.data?.response?.products) - }, [productSearch]) + setProducts(productSearch.data?.response?.products); + }, [productSearch]); const SpellingComponent = useMemo(() => { return ( @@ -182,8 +215,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) </Link> ))} </> - ) - }, [spellings]) + ); + }, [spellings]); const handleDeleteFilter = async (source, value) => { let params = { @@ -192,51 +225,64 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) brand: brandValues.join(','), category: categoryValues.join(','), priceFrom, - priceTo - } + priceTo, + }; - let brands = brandValues - let catagories = categoryValues + let brands = brandValues; + let catagories = categoryValues; switch (source) { case 'brands': - brands = brandValues.filter((item) => item !== value) - params.brand = brands.join(',') - await setBrand(brands) - break + brands = brandValues.filter((item) => item !== value); + params.brand = brands.join(','); + await setBrand(brands); + break; case 'category': - catagories = categoryValues.filter((item) => item !== value) - params.category = catagories.join(',') - await setCategory(catagories) - break + catagories = categoryValues.filter((item) => item !== value); + params.category = catagories.join(','); + await setCategory(catagories); + break; case 'price': - params.priceFrom = null - params.priceTo = null - break + params.priceFrom = null; + params.priceTo = null; + break; case 'delete': params = { q: router.query.q, - orderBy: orderBy - } - break + orderBy: orderBy, + }; + break; } - handleSubmitFilter(params) - } + handleSubmitFilter(params); + }; const handleSubmitFilter = (params) => { - params = _.pickBy(params, _.identity) - params = toQuery(params) - router.push(`${prefixUrl}?${params}`) - } + params = _.pickBy(params, _.identity); + params = toQuery(params); + router.push(`${prefixUrl}?${params}`); + }; + + const isNotReadyStockPage = router.asPath !== '/shop/search?orderBy=stock'; return ( <> <MobileView> {productSearch.isLoading && <ProductSearchSkeleton />} <div className='p-4 pt-0'> - {isBrand && isBrand.logo && ( + {isNotReadyStockPage && isBrand && isBrand.logo && ( <div className='mb-3'> - <h1 className='mb-2 font-semibold text-h-sm'>Brand Pencarian {q}</h1> - <Image src={isBrand?.logo} alt='' className='object-cover object-center h-[60px]' /> + <h1 className='mb-2 font-semibold text-h-sm'> + Brand Pencarian {q} + </h1> + <Link + href={createSlug('/shop/brands/', isBrand.name, isBrand.id)} + className='inline' + > + <Image + src={isBrand?.logo} + alt='' + className='object-cover object-center h-[60px]' + /> + </Link> </div> )} <h1 className='mb-2 font-semibold text-h-sm'>Produk</h1> @@ -255,7 +301,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) {pageCount > 1 ? ( <> {productStart + 1}- - {parseInt(productStart) + parseInt(productRows) > productFound + {parseInt(productStart) + parseInt(productRows) > + productFound ? productFound : parseInt(productStart) + parseInt(productRows)} dari @@ -267,7 +314,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) produk{' '} {query.q && ( <> - untuk pencarian <span className='font-semibold'>{query.q}</span> + untuk pencarian{' '} + <span className='font-semibold'>{query.q}</span> </> )} </> @@ -279,7 +327,10 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) {productFound > 0 && ( <div className='flex items-center gap-x-2 mb-5 justify-between'> <div> - <button className='btn-light py-2 px-5 h-[40px]' onClick={popup.activate}> + <button + className='btn-light py-2 px-5 h-[40px]' + onClick={popup.activate} + > Filter </button> </div> @@ -303,7 +354,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) <div className='grid grid-cols-2 gap-3'> {products && - products.map((product) => <ProductCard product={product} key={product.id} />)} + products.map((product) => ( + <ProductCard product={product} key={product.id} /> + ))} </div> <Pagination @@ -329,7 +382,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) <div className='w-3/12'> {brand && ( <div className='p-4'> - <div className='text-caption-1 text-gray_r-11 mb-2'>Produk dari brand:</div> + <div className='text-caption-1 text-gray_r-11 mb-2'> + Produk dari brand: + </div> {brand?.data?.logo && ( <Image src={brand?.data?.logo} @@ -351,6 +406,10 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) prefixUrl={prefixUrl} defaultBrand={defaultBrand} /> + + <div className='h-6' /> + + <SideBanner /> </div> <div className='w-9/12 pl-6'> {bannerPromotionHeader && bannerPromotionHeader?.image && ( @@ -363,14 +422,20 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) </div> )} - {isBrand && isBrand.logo && ( + {isNotReadyStockPage && isBrand && isBrand.logo && ( <div className='mb-3'> - <h1 className='text-2xl mb-2 font-semibold'>Brand Pencarian {q}</h1> + <h1 className='text-2xl mb-2 font-semibold'> + Brand Pencarian {q} + </h1> <Link href={createSlug('/shop/brands/', isBrand.name, isBrand.id)} className='inline' > - <Image src={isBrand?.logo} alt='' className='object-cover object-center h-24' /> + <Image + src={isBrand?.logo} + alt='' + className='object-cover object-center h-24' + /> </Link> </div> )} @@ -391,7 +456,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) {pageCount > 1 ? ( <> {productStart + 1}- - {parseInt(productStart) + parseInt(productRows) > productFound + {parseInt(productStart) + parseInt(productRows) > + productFound ? productFound : parseInt(productStart) + parseInt(productRows)} dari @@ -403,7 +469,8 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) produk{' '} {query.q && ( <> - untuk pencarian <span className='font-semibold'>{query.q}</span> + untuk pencarian{' '} + <span className='font-semibold'>{query.q}</span> </> )} </> @@ -447,7 +514,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) {productSearch.isLoading && <ProductSearchSkeleton />} <div className='grid grid-cols-5 gap-x-3 gap-y-6'> {products && - products.map((product) => <ProductCard product={product} key={product.id} />)} + products.map((product) => ( + <ProductCard product={product} key={product.id} /> + ))} </div> <div className='flex justify-between items-center mt-6 mb-2'> <div className='pt-2 pb-6 flex items-center gap-x-3'> @@ -464,7 +533,7 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) href={ query?.q ? whatsappUrl('productSearch', { - name: query.q + name: query.q, }) : whatsappUrl() } @@ -492,44 +561,66 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null, brand = null }) /> </div> )} + <FooterBanner /> </div> </div> </DesktopView> </> - ) -} + ); +}; -export default ProductSearch +export default ProductSearch; const FilterChoicesComponent = ({ brandValues, categoryValues, priceFrom, priceTo, - handleDeleteFilter + handleDeleteFilter, }) => ( <div className='flex items-center'> <HStack spacing={2} className='flex-wrap'> {brandValues?.map((value, index) => ( - <Tag size='lg' key={index} borderRadius='lg' variant='outline' colorScheme='gray'> + <Tag + size='lg' + key={index} + borderRadius='lg' + variant='outline' + colorScheme='gray' + > <TagLabel>{value}</TagLabel> <TagCloseButton onClick={() => handleDeleteFilter('brands', value)} /> </Tag> ))} {categoryValues?.map((value, index) => ( - <Tag size='lg' key={index} borderRadius='lg' variant='outline' colorScheme='gray'> + <Tag + size='lg' + key={index} + borderRadius='lg' + variant='outline' + colorScheme='gray' + > <TagLabel>{value}</TagLabel> - <TagCloseButton onClick={() => handleDeleteFilter('category', value)} /> + <TagCloseButton + onClick={() => handleDeleteFilter('category', value)} + /> </Tag> ))} {priceFrom && priceTo && ( <Tag size='lg' borderRadius='lg' variant='outline' colorScheme='gray'> - <TagLabel>{formatCurrency(priceFrom) + '-' + formatCurrency(priceTo)}</TagLabel> - <TagCloseButton onClick={() => handleDeleteFilter('price', priceFrom)} /> + <TagLabel> + {formatCurrency(priceFrom) + '-' + formatCurrency(priceTo)} + </TagLabel> + <TagCloseButton + onClick={() => handleDeleteFilter('price', priceFrom)} + /> </Tag> )} - {brandValues?.length > 0 || categoryValues?.length > 0 || priceFrom || priceTo ? ( + {brandValues?.length > 0 || + categoryValues?.length > 0 || + priceFrom || + priceTo ? ( <span> <button className='btn-transparent py-2 px-5 h-[40px] text-red-700' @@ -543,4 +634,4 @@ const FilterChoicesComponent = ({ )} </HStack> </div> -) +); |
