diff options
Diffstat (limited to 'src/lib')
19 files changed, 992 insertions, 420 deletions
diff --git a/src/lib/auth/components/IsAuth.jsx b/src/lib/auth/components/IsAuth.jsx index a32e648c..1948ae0c 100644 --- a/src/lib/auth/components/IsAuth.jsx +++ b/src/lib/auth/components/IsAuth.jsx @@ -8,7 +8,7 @@ const IsAuth = ({ children }) => { useEffect(() => { if (!getAuth() && router.pathname != '/login') { - router.replace(`/login?next=${router.asPath}`) + router.replace(`/login?next=${encodeURIComponent(router.asPath)}`) } else { setResponse(children) } diff --git a/src/lib/auth/hooks/useLogin.js b/src/lib/auth/hooks/useLogin.js index 34605614..17731a86 100644 --- a/src/lib/auth/hooks/useLogin.js +++ b/src/lib/auth/hooks/useLogin.js @@ -71,7 +71,7 @@ const useLogin = () => { if (data.isAuth) { session.odooUser = data.user setCookie('auth', JSON.stringify(session?.odooUser)) - router.push(router?.query?.next ?? '/') + router.push(decodeURIComponent(router?.query?.next) ?? '/') return } } diff --git a/src/lib/brand/components/Brand.jsx b/src/lib/brand/components/Brand.jsx index 3c411969..4afbcb3e 100644 --- a/src/lib/brand/components/Brand.jsx +++ b/src/lib/brand/components/Brand.jsx @@ -6,10 +6,10 @@ import { Pagination, Autoplay } from 'swiper' import 'swiper/css' import 'swiper/css/pagination' import 'swiper/css/autoplay' -import Divider from '@/core/components/elements/Divider/Divider' -import ImageSkeleton from '@/core/components/elements/Skeleton/ImageSkeleton' import MobileView from '@/core/components/views/MobileView' import DesktopView from '@/core/components/views/DesktopView' +import { Skeleton } from '@chakra-ui/react' +import classNames from 'classnames' const swiperBanner = { pagination: { dynamicBullets: true }, @@ -28,65 +28,77 @@ const Brand = ({ id }) => { <MobileView> <> <div> - {brand.isLoading && <ImageSkeleton />} - {brand.data?.banners?.length == 0 && ( - <Image - src='/images/default-banner-brand.jpg' - alt='Brand - Indoteknik' - width={1024} - height={512} - className='w-full h-auto' - /> - )} - {brand.data && ( - <> - <Swiper - slidesPerView={1} - pagination={swiperBanner.pagination} - modules={swiperBanner.modules} - autoplay={swiperBanner.autoplay} - className='border-b border-gray_r-6' - > - {brand.data?.banners?.map((banner, index) => ( - <SwiperSlide key={index}> + <Skeleton + isLoaded={!brand.isLoading} + aspectRatio='4/2' + className={classNames({ + 'mb-6': brand.isLoading + })} + > + {brand.data?.banners?.length == 0 && ( + <Image + src='/images/default-banner-brand.jpg' + alt='Brand - Indoteknik' + width={1024} + height={512} + className='w-full h-auto' + /> + )} + + {brand.data && ( + <> + <Swiper + slidesPerView={1} + pagination={swiperBanner.pagination} + modules={swiperBanner.modules} + autoplay={swiperBanner.autoplay} + className='border-b border-gray_r-6' + > + {brand.data?.banners?.map((banner, index) => ( + <SwiperSlide key={index}> + <Image + src={banner} + alt={`Brand ${brand.data?.name} - Indoteknik`} + width={1024} + height={512} + className='w-full h-auto' + /> + </SwiperSlide> + ))} + </Swiper> + <div className='p-4'> + <div className='text-caption-1 text-gray_r-11 mb-2'>Produk dari brand:</div> + {brand?.data?.logo && ( <Image - src={banner} - alt={`Brand ${brand.data?.name} - Indoteknik`} - width={1024} - height={512} - className='w-full h-auto' + src={brand?.data?.logo} + alt={brand?.data?.name} + className='w-32 p-2 border borde-gray_r-6 rounded' + width={256} + height={128} /> - </SwiperSlide> - ))} - </Swiper> - <div className='p-4'> - <div className='text-caption-1 text-gray_r-11 mb-2'>Produk dari brand:</div> - {brand?.data?.logo && ( - <Image - src={brand?.data?.logo} - alt={brand?.data?.name} - className='w-32 p-2 border borde-gray_r-6 rounded' - width={256} - height={128} - /> - )} - {!brand?.data?.logo && ( - <div className='bg-danger-500 text-white text-center text-caption-1 py-2 px-4 rounded w-fit'> - {brand?.data?.name} - </div> - )} - </div> - </> - )} + )} + {!brand?.data?.logo && ( + <div className='bg-danger-500 text-white text-center text-caption-1 py-2 px-4 rounded w-fit'> + {brand?.data?.name} + </div> + )} + </div> + </> + )} + </Skeleton> </div> - <Divider /> </> </MobileView> <DesktopView> - <div className='container mx-auto mt-10 mb-3'> - <div className='min-h-[150px]'> - {brand.isLoading && <ImageSkeleton />} + <div className='container mx-auto'> + <Skeleton + isLoaded={!brand.isLoading} + aspectRatio='4/1' + className={classNames({ + 'mb-6': brand.isLoading + })} + > {brand.data?.banners?.length == 0 && ( <Image src='/images/default-banner-brand.jpg' @@ -96,6 +108,7 @@ const Brand = ({ id }) => { className='w-full h-auto' /> )} + {brand.data && ( <> <Swiper @@ -117,6 +130,7 @@ const Brand = ({ id }) => { </SwiperSlide> ))} </Swiper> + <div className='p-4'> <div className='text-caption-1 text-gray_r-11 mb-2'>Produk dari brand:</div> {brand?.data?.logo && ( @@ -136,8 +150,7 @@ const Brand = ({ id }) => { </div> </> )} - </div> - <Divider /> + </Skeleton> </div> </DesktopView> </> diff --git a/src/lib/brand/components/Breadcrumb.jsx b/src/lib/brand/components/Breadcrumb.jsx new file mode 100644 index 00000000..0fec2dad --- /dev/null +++ b/src/lib/brand/components/Breadcrumb.jsx @@ -0,0 +1,40 @@ +import { Breadcrumb as ChakraBreadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react' +import Link from 'next/link' +import React from 'react' + +/** + * Renders a breadcrumb component with links to navigate through different pages. + * + * @param {Object} props - The props object containing the brand name. + * @param {string} props.brandName - The name of the brand to display in the breadcrumb. + * @return {JSX.Element} The rendered breadcrumb component. + */ +const Breadcrumb = ({ brandName }) => { + return ( + <div className='container mx-auto py-4 md:py-6'> + <ChakraBreadcrumb> + <BreadcrumbItem> + <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'> + Home + </BreadcrumbLink> + </BreadcrumbItem> + + <BreadcrumbItem> + <BreadcrumbLink + as={Link} + href='/shop/brands' + className='!text-danger-500 whitespace-nowrap' + > + Brands + </BreadcrumbLink> + </BreadcrumbItem> + + <BreadcrumbItem isCurrentPage> + <BreadcrumbLink className='whitespace-nowrap'>{brandName}</BreadcrumbLink> + </BreadcrumbItem> + </ChakraBreadcrumb> + </div> + ) +} + +export default Breadcrumb diff --git a/src/lib/cart/components/Cartheader.jsx b/src/lib/cart/components/Cartheader.jsx index b3cf849b..580dfc8c 100644 --- a/src/lib/cart/components/Cartheader.jsx +++ b/src/lib/cart/components/Cartheader.jsx @@ -8,6 +8,7 @@ import { useRouter } from 'next/router' import odooApi from '@/core/api/odooApi' import { useProductCartContext } from '@/contexts/ProductCartContext' import whatsappUrl from '@/core/utils/whatsappUrl' +import { AnimatePresence, motion } from 'framer-motion' const { ShoppingCartIcon, PhotoIcon } = require('@heroicons/react/24/outline') const { default: Link } = require('next/link') @@ -113,164 +114,182 @@ const Cardheader = (cartCount) => { </span> </Link> </div> - <div - className={` ${ - isHovered ? 'block' : 'hidden' - } fixed top-[155px] left-0 w-full h-full bg-black opacity-50 z-10`} - ></div> - <div - className='hidden group-hover:block absolute z-10 left-0 w-96' - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} - > - <div className='w-full max-w-md p-2 bg-white border border-gray-200 rounded-lg shadow'> - <div className='p-2 flex justify-between items-center'> - <h5 class='text-base font-semibold leading-none'>Keranjang Belanja</h5> - <Link href='/shop/cart' class='text-sm font-medium text-red-600 underline'> - Lihat Semua - </Link> - </div> - <hr className='mt-3 mb-3 border border-gray-100' /> - <div className='flow-root max-h-[250px] overflow-y-auto'> - {!auth && ( - <div className='justify-center p-4'> - <p className='text-gray-500 text-center '> - Silahkan{' '} - <Link href='/login' className='text-red-600 underline leading-6'> - Login - </Link>{' '} - Untuk Melihat Daftar Keranjang Belanja Anda - </p> - </div> - )} - {isLoading && - itemLoading.map((item) => ( - <div key={item} role='status' class='max-w-sm animate-pulse'> - <div class='flex items-center space-x-4 mb- 2'> - <div class='flex-shrink-0'> - <PhotoIcon class='h-16 w-16 text-gray-500' /> + + <AnimatePresence> + {isHovered && ( + <> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + exit={{ opacity: 0 }} + transition={{ duration: 0.15 }} + className='fixed top-[155px] left-0 w-full h-full bg-black/50 z-10' + /> + + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1, transition: { duration: 0.2 } }} + exit={{ opacity: 0, transition: { duration: 0.3 } }} + className='absolute z-10 left-0 w-96' + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + <motion.div + initial={{ height: 0 }} + animate={{ height: 'auto' }} + exit={{ height: 0 }} + className='w-full max-w-md p-2 bg-white border border-gray-200 rounded-lg shadow overflow-hidden' + > + <div className='p-2 flex justify-between items-center'> + <h5 className='text-base font-semibold leading-none'>Keranjang Belanja</h5> + <Link href='/shop/cart' class='text-sm font-medium text-red-600 underline'> + Lihat Semua + </Link> + </div> + <hr className='mt-3 mb-3 border border-gray-100' /> + <div className='flow-root max-h-[250px] overflow-y-auto'> + {!auth && ( + <div className='justify-center p-4'> + <p className='text-gray-500 text-center '> + Silahkan{' '} + <Link href='/login' className='text-red-600 underline leading-6'> + Login + </Link>{' '} + Untuk Melihat Daftar Keranjang Belanja Anda + </p> </div> - <div class='flex-1 min-w-0'> - <div class='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4'></div> - <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5'></div> - <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> + )} + {isLoading && + itemLoading.map((item) => ( + <div key={item} role='status' className='max-w-sm animate-pulse'> + <div className='flex items-center space-x-4 mb- 2'> + <div className='flex-shrink-0'> + <PhotoIcon className='h-16 w-16 text-gray-500' /> + </div> + <div className='flex-1 min-w-0'> + <div className='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4'></div> + <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5'></div> + <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div> + </div> + </div> + </div> + ))} + {auth && products.length === 0 && !isLoading && ( + <div className='justify-center p-4'> + <p className='text-gray-500 text-center '> + Tidak Ada Produk di Keranjang Belanja Anda + </p> </div> - </div> - </div> - ))} - {auth && products.length === 0 && !isLoading && ( - <div className='justify-center p-4'> - <p className='text-gray-500 text-center '> - Tidak Ada Produk di Keranjang Belanja Anda - </p> - </div> - )} - {auth && products.length > 0 && !isLoading && ( - <> - <ul role='list' class='divide-y divide-gray-200 dark:divide-gray-700'> - {products && - products?.map((product, index) => ( - <> - <li class='py-1 sm:py-2'> - <div class='flex items-center space-x-4'> - <div class='flex-shrink-0'> - <Link - href={createSlug( - '/shop/product/', - product?.parent.name, - product?.parent.id - )} - className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' - > - <Image - src={product?.parent?.image} - alt={product?.name} - className='object-contain object-center border border-gray_r-6 h-16 w-16 rounded-md' - /> - </Link> - </div> - <div class='flex-1 min-w-0'> - <Link - href={createSlug( - '/shop/product/', - product?.parent.name, - product?.parent.id - )} - className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' - > - {' '} - <p class='text-caption-2 font-medium text-gray-900 truncate dark:text-white'> - {product.parent.name} - </p> - </Link> - - {product?.hasFlashsale && ( - <div className='flex gap-x-1 items-center mb-2 mt-1'> - <div className='badge-solid-red'> - {product?.price?.discountPercentage}% + )} + {auth && products.length > 0 && !isLoading && ( + <> + <ul role='list' className='divide-y divide-gray-200 dark:divide-gray-700'> + {products && + products?.map((product, index) => ( + <> + <li className='py-1 sm:py-2'> + <div className='flex items-center space-x-4'> + <div className='flex-shrink-0'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + <Image + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-16 w-16 rounded-md' + /> + </Link> </div> - <div className='text-gray_r-11 line-through text-caption-2'> - {currencyFormat(product?.price?.price)} + <div className='flex-1 min-w-0'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + {' '} + <p className='text-caption-2 font-medium text-gray-900 truncate dark:text-white'> + {product.parent.name} + </p> + </Link> + + {product?.hasFlashsale && ( + <div className='flex gap-x-1 items-center mb-2 mt-1'> + <div className='badge-solid-red'> + {product?.price?.discountPercentage}% + </div> + <div className='text-gray_r-11 line-through text-caption-2'> + {currencyFormat(product?.price?.price)} + </div> + </div> + )} + <div className='flex justify-between items-center'> + <div className='font-semibold text-sm text-red-600'> + {product?.price?.priceDiscount > 0 ? ( + currencyFormat(product?.price?.priceDiscount) + ) : ( + <span className='text-gray_r-12/90 font-normal text-caption-1'> + <a + href={whatsappUrl('product', { + name: product.name, + manufacture: product.manufacture?.name, + url: createSlug( + '/shop/product/', + product.name, + product.id, + true + ) + })} + className='text-danger-500 underline' + rel='noopener noreferrer' + target='_blank' + > + Call For Price + </a> + </span> + )} + </div> + </div> </div> </div> - )} - <div className='flex justify-between items-center'> - <div className='font-semibold text-sm text-red-600'> - {product?.price?.priceDiscount > 0 ? ( - currencyFormat(product?.price?.priceDiscount) - ) : ( - <span className='text-gray_r-12/90 font-normal text-caption-1'> - <a - href={whatsappUrl('product', { - name: product.name, - manufacture: product.manufacture?.name, - url: createSlug( - '/shop/product/', - product.name, - product.id, - true - ) - })} - className='text-danger-500 underline' - rel='noopener noreferrer' - target='_blank' - > - Call for Inquiry - </a> - </span> - )} - </div> - </div> - </div> - </div> - </li> - </> - ))} - </ul> - <hr /> - </> - )} - </div> - {auth && products.length > 0 && !isLoading && ( - <> - <div className='mt-3'> - <span className='text-gray-400 text-caption-2'>Subtotal Sebelum PPN : </span> - <span className='font-semibold text-red-600'>{currencyFormat(subTotal)}</span> - </div> - <div className='mt-5 mb-2'> - <button - type='button' - className='btn-solid-red rounded-lg w-full' - onClick={handleCheckout} - disabled={buttonLoading} - > - {buttonLoading ? 'Loading...' : 'Lanjutkan Ke Pembayaran'} - </button> - </div> - </> - )} - </div> - </div> + </li> + </> + ))} + </ul> + <hr /> + </> + )} + </div> + {auth && products.length > 0 && !isLoading && ( + <> + <div className='mt-3'> + <span className='text-gray-400 text-caption-2'>Subtotal Sebelum PPN : </span> + <span className='font-semibold text-red-600'>{currencyFormat(subTotal)}</span> + </div> + <div className='mt-5 mb-2'> + <button + type='button' + className='btn-solid-red rounded-lg w-full' + onClick={handleCheckout} + disabled={buttonLoading} + > + {buttonLoading ? 'Loading...' : 'Lanjutkan Ke Pembayaran'} + </button> + </div> + </> + )} + </motion.div> + </motion.div> + </> + )} + </AnimatePresence> </div> ) } diff --git a/src/lib/category/components/Breadcrumb.jsx b/src/lib/category/components/Breadcrumb.jsx new file mode 100644 index 00000000..127904ee --- /dev/null +++ b/src/lib/category/components/Breadcrumb.jsx @@ -0,0 +1,56 @@ +import odooApi from '@/core/api/odooApi' +import { createSlug } from '@/core/utils/slug' +import { + Breadcrumb as ChakraBreadcrumb, + BreadcrumbItem, + BreadcrumbLink, + Skeleton +} from '@chakra-ui/react' +import Link from 'next/link' +import React from 'react' +import { useQuery } from 'react-query' + +/** + * Render a breadcrumb component. + * + * @param {object} categoryId - The ID of the category. + * @return {JSX.Element} The breadcrumb component. + */ +const Breadcrumb = ({ categoryId }) => { + const breadcrumbs = useQuery( + `category-breadcrumbs/${categoryId}`, + async () => await odooApi('GET', `/api/v1/category/${categoryId}/category-breadcrumb`) + ) + + return ( + <div className='container mx-auto py-4 md:py-6'> + <Skeleton isLoaded={!breadcrumbs.isLoading} className='w-2/3'> + <ChakraBreadcrumb> + <BreadcrumbItem> + <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'> + Home + </BreadcrumbLink> + </BreadcrumbItem> + + {breadcrumbs.data?.map((category, index) => ( + <BreadcrumbItem key={index} isCurrentPage={index === breadcrumbs.data.length - 1}> + {index === breadcrumbs.data.length - 1 ? ( + <BreadcrumbLink className='whitespace-nowrap'>{category.name}</BreadcrumbLink> + ) : ( + <BreadcrumbLink + as={Link} + href={createSlug('/shop/category/', category.name, category.id)} + className='!text-danger-500 whitespace-nowrap' + > + {category.name} + </BreadcrumbLink> + )} + </BreadcrumbItem> + ))} + </ChakraBreadcrumb> + </Skeleton> + </div> + ) +} + +export default Breadcrumb diff --git a/src/lib/category/components/Category.jsx b/src/lib/category/components/Category.jsx index af696d42..e6ea5acf 100644 --- a/src/lib/category/components/Category.jsx +++ b/src/lib/category/components/Category.jsx @@ -30,7 +30,7 @@ const Category = () => { return ( <DesktopView> <div className='category-mega-box'> - {categories.map((category) => ( + {categories?.map((category) => ( <div key={category.id}> <Link href={createSlug('/shop/category/', category.name, category.id)} diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 85221e5d..9a799010 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -5,7 +5,6 @@ import useAuth from '@/core/hooks/useAuth' import { getItemAddress } from '@/core/utils/address' import addressesApi from '@/lib/address/api/addressesApi' import { - ArrowLongLeftIcon, BanknotesIcon, ChevronLeftIcon, ClockIcon, @@ -17,22 +16,21 @@ import { deleteItemCart, getCartApi } from '@/core/utils/cart' import currencyFormat from '@/core/utils/currencyFormat' import { toast } from 'react-hot-toast' import getFileBase64 from '@/core/utils/getFileBase64' -// import checkoutApi from '../api/checkoutApi' import { useRouter } from 'next/router' import VariantGroupCard from '@/lib/variant/components/VariantGroupCard' import axios from 'axios' import Image from '@/core/components/elements/Image/Image' -import imageNext from 'next/image' import MobileView from '@/core/components/views/MobileView' import DesktopView from '@/core/components/views/DesktopView' import ExpedisiList from '../api/ExpedisiList' import whatsappUrl from '@/core/utils/whatsappUrl' -import { createSlug } from '@/core/utils/slug' import BottomPopup from '@/core/components/elements/Popup/BottomPopup' import { useQuery } from 'react-query' import { gtagPurchase } from '@/core/utils/googleTag' import { findVoucher, getVoucher } from '../api/getVoucher' import CardProdcuctsList from '@/core/components/elements/Product/cartProductsList' +import { Spinner } from '@chakra-ui/react' +import { AnimatePresence, motion } from 'framer-motion' const SELF_PICKUP_ID = 32 @@ -110,6 +108,7 @@ const Checkout = () => { const [buttonTerapkan, SetButtonTerapkan] = useState(false) const [checkoutValidation, setCheckoutValidation] = useState(false) const [loadingVoucher, setLoadingVoucher] = useState(true) + const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false) const expedisiValidation = useRef(null) @@ -169,6 +168,16 @@ const Checkout = () => { setExpedisi(dataExpedisi) } loadExpedisi() + + const handlePopState = () => { + router.push('/shop/cart') + } + + window.onpopstate = handlePopState + + return () => { + window.onpopstate = null + } // voucher() }, []) @@ -211,6 +220,7 @@ const Checkout = () => { useEffect(() => { setCheckoutValidation(false) const loadServiceRajaOngkir = async () => { + setLoadingRajaOngkir(true) const body = { origin: 2127, destination: selectedAddress.shipping.rajaongkirCityId, @@ -221,6 +231,7 @@ const Checkout = () => { } setBiayaKirim(0) const dataService = await axios('/api/rajaongkir-service?body=' + JSON.stringify(body)) + setLoadingRajaOngkir(false) setListServiceExpedisi(dataService.data[0].costs) if (dataService.data[0].costs[0]) { setBiayaKirim(dataService.data[0].costs[0]?.cost[0].value) @@ -259,9 +270,8 @@ const Checkout = () => { useEffect(() => { if (selectedExpedisi) { let serviceType = selectedExpedisi.split(',') - if (serviceType[0] === 0) { - setSelectedExpedisi(0) - } + if (serviceType[0] === 0) return + setselectedCarrier(serviceType[0]) setselectedCarrierId(serviceType[1]) setListServiceExpedisi([]) @@ -723,6 +733,7 @@ const Checkout = () => { checkWeigth={checkWeigth} checkoutValidation={checkoutValidation} expedisiValidation={expedisiValidation} + loadingRajaOngkir={loadingRajaOngkir} /> <Divider /> <SectionListService @@ -980,6 +991,7 @@ const Checkout = () => { checkWeigth={checkWeigth} checkoutValidation={checkoutValidation} expedisiValidation={expedisiValidation} + loadingRajaOngkir={loadingRajaOngkir} /> <Divider /> <SectionListService @@ -990,7 +1002,7 @@ const Checkout = () => { <div className='p-4'> <div className='font-medium'>Detail Pesanan</div> <CardProdcuctsList isLoading={isLoading} products={products} /> - + {/* <table className='table-checkout'> <thead> <tr> @@ -1158,7 +1170,9 @@ const Checkout = () => { <div className='sticky top-48 border border-gray_r-6 bg-white rounded p-4'> <div className='flex justify-between items-center'> <div className='font-medium'>Ringkasan Pesanan</div> - <div className='text-gray_r-11 text-caption-1'>{products?.length} Barang</div> + <div className='text-gray_r-11 text-caption-1'> + {products?.length} Barang - {cartCheckout?.totalWeight.kg} Kg + </div> </div> <hr className='my-4 border-gray_r-6' /> @@ -1403,32 +1417,51 @@ const SectionExpedisi = ({ setSelectedExpedisi, checkWeigth, checkoutValidation, - expedisiValidation + expedisiValidation, + loadingRajaOngkir }) => address?.rajaongkirCityId > 0 && ( <div className='p-4' ref={expedisiValidation}> <div className='flex justify-between items-center'> - <div className='font-medium'>Pilih Expedisi : </div> + <div className='font-medium'>Pilih Ekspedisi: </div> <div className='w-[250px]'> - <select - className={`form-input ${checkoutValidation ? 'border-red-500 shake' : ''}`} - onChange={(e) => setSelectedExpedisi(e.target.value)} - required - > - <option value='0,0'>Pilih Pengiriman</option> - <option value='1,32'>SELF PICKUP</option> - {checkWeigth != true && - listExpedisi.map((expedisi) => ( - <option - disabled={checkWeigth} - value={expedisi.label + ',' + expedisi.carrierId} - key={expedisi.value} + <div className='flex items-center gap-x-4'> + <select + className={`form-input ${checkoutValidation ? 'border-red-500 shake' : ''}`} + onChange={(e) => setSelectedExpedisi(e.target.value)} + required + > + <option value='0,0'>Pilih Pengiriman</option> + <option value='1,32'>SELF PICKUP</option> + {checkWeigth != true && + listExpedisi.map((expedisi) => ( + <option + disabled={checkWeigth} + value={expedisi.label + ',' + expedisi.carrierId} + key={expedisi.value} + > + {' '} + {expedisi.label.toUpperCase()}{' '} + </option> + ))} + </select> + + <AnimatePresence> + {loadingRajaOngkir && ( + <motion.div + initial={{ opacity: 0, width: 0 }} + animate={{ opacity: 1, width: '28px' }} + exit={{ opacity: 0, width: 0 }} + transition={{ + duration: 0.25 + }} + className='overflow-hidden' > - {' '} - {expedisi.label.toUpperCase()}{' '} - </option> - ))} - </select> + <Spinner thickness='3px' speed='0.5s' color='red.500' /> + </motion.div> + )} + </AnimatePresence> + </div> {checkoutValidation && ( <span className='text-sm text-red-500'>*silahkan pilih expedisi</span> )} @@ -1459,7 +1492,7 @@ const SectionListService = ({ listserviceExpedisi, setSelectedServiceType }) => <> <div className='p-4'> <div className='flex justify-between items-center'> - <div className='font-medium'>Service Type Expedisi : </div> + <div className='font-medium'>Tipe Layanan Ekspedisi: </div> <div> <select className='form-input' onChange={(e) => setSelectedServiceType(e.target.value)}> {listserviceExpedisi.map((service) => ( diff --git a/src/lib/flashSale/components/FlashSale.jsx b/src/lib/flashSale/components/FlashSale.jsx index 87545d8d..3d5c4e0e 100644 --- a/src/lib/flashSale/components/FlashSale.jsx +++ b/src/lib/flashSale/components/FlashSale.jsx @@ -63,7 +63,7 @@ const FlashSaleProduct = ({ flashSaleId }) => { useEffect(() => { const loadProducts = async () => { const dataProducts = await productSearchApi({ - query: `fq=flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500`, + query: `fq=flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500&orderBy=flashsale-price-asc`, operation: 'AND' }) setProducts(dataProducts.response) diff --git a/src/lib/product/components/Product/Breadcrumb.jsx b/src/lib/product/components/Product/Breadcrumb.jsx new file mode 100644 index 00000000..0554dba5 --- /dev/null +++ b/src/lib/product/components/Product/Breadcrumb.jsx @@ -0,0 +1,69 @@ +import odooApi from '@/core/api/odooApi' +import { createSlug } from '@/core/utils/slug' +import { + Breadcrumb as ChakraBreadcrumb, + BreadcrumbItem, + BreadcrumbLink, + Skeleton +} from '@chakra-ui/react' +import classNames from 'classnames' +import Link from 'next/link' +import { useQuery } from 'react-query' + +/** + * Renders a breadcrumb component based on the provided `productId`. + * + * @param {Object} props - The properties passed to the component. + * @param {number} props.productId - The ID of the product. + * @param {string} props.productName - The ID of the product. + * @return {ReactElement} The rendered breadcrumb component. + */ +const Breadcrumb = ({ productId, productName }) => { + const categories = useQuery( + `detail/categories/${productId}`, + async () => await odooApi('GET', `/api/v1/product/${productId}/category-breadcrumb`), + { + enabled: !!productId + } + ) + + return ( + <Skeleton + isLoaded={!categories.isLoading} + className={classNames({ + 'w-2/3': categories.isLoading, + 'w-full': !categories.isLoading + })} + > + <ChakraBreadcrumb + mb={10} + overflowX={'auto'} + className='text-caption-2 md:text-body-2 p-4 md:p-0' + > + <BreadcrumbItem> + <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'> + Home + </BreadcrumbLink> + </BreadcrumbItem> + + {categories.data?.map((category) => ( + <BreadcrumbItem key={category.id}> + <BreadcrumbLink + as={Link} + href={createSlug('/shop/category/', category.name, category.id)} + className='!text-danger-500 whitespace-nowrap' + > + {category.name} + </BreadcrumbLink> + </BreadcrumbItem> + ))} + + <BreadcrumbItem isCurrentPage> + <BreadcrumbLink className='whitespace-nowrap'>{productName}</BreadcrumbLink> + </BreadcrumbItem> + </ChakraBreadcrumb> + </Skeleton> + ) +} + +export default Breadcrumb diff --git a/src/lib/product/components/Product/Product.jsx b/src/lib/product/components/Product/Product.jsx index 54490c26..6e983c2e 100644 --- a/src/lib/product/components/Product/Product.jsx +++ b/src/lib/product/components/Product/Product.jsx @@ -36,29 +36,21 @@ const Product = ({ product, isVariant = false }) => { } }, [product, isVariant]) - if (isVariant == true) { - return ( - <> - <ProductDesktopVariant - product={product} - wishlist={wishlist} - toggleWishlist={toggleWishlist} - /> - <ProductMobileVariant - product={product} - wishlist={wishlist} - toggleWishlist={toggleWishlist} - /> - </> - ) - } else { - return ( - <> - <ProductMobile product={product} wishlist={wishlist} toggleWishlist={toggleWishlist} /> - <ProductDesktop products={product} wishlist={wishlist} toggleWishlist={toggleWishlist} /> - </> - ) - } + return isVariant == true ? ( + <> + <ProductDesktopVariant + product={product} + wishlist={wishlist} + toggleWishlist={toggleWishlist} + /> + <ProductMobileVariant product={product} wishlist={wishlist} toggleWishlist={toggleWishlist} /> + </> + ) : ( + <> + <ProductMobile product={product} wishlist={wishlist} toggleWishlist={toggleWishlist} /> + <ProductDesktop products={product} wishlist={wishlist} toggleWishlist={toggleWishlist} /> + </> + ) } export default Product diff --git a/src/lib/product/components/Product/ProductDesktop.jsx b/src/lib/product/components/Product/ProductDesktop.jsx index aa771eab..11c34009 100644 --- a/src/lib/product/components/Product/ProductDesktop.jsx +++ b/src/lib/product/components/Product/ProductDesktop.jsx @@ -24,6 +24,8 @@ 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' const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { const router = useRouter() @@ -89,38 +91,38 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { } const updateCart = (variantId, quantity, source) => { - let dataUpdate + let dataUpdate = { + productId: variantId, + quantity, + selected: true, + source: source === 'buy' ? 'buy' : null + } + if (product.variants.length > 1) { let variantIndex = product.variants.findIndex((varian) => varian.id == variantId) - dataUpdate = { - productId: variantId, - quantity, - programLineId: product.variants[variantIndex].programActive, - selected: true, - source: source === 'buy' ? 'buy' : null - } + dataUpdate['programLineId'] = product.variants[variantIndex].programActive } else { - dataUpdate = { - productId: variantId, - quantity, - programLineId: promotionActiveId, - selected: true, - source: source === 'buy' ? 'buy' : null - } + dataUpdate['programLineId'] = promotionActiveId } + updateItemCart(dataUpdate) } - const handleAddToCart = (variantId) => { - if (!auth) { - router.push(`/login?next=/shop/product/${slug}`) - return - } + 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 handleAddToCart = (variantId) => { const quantity = variantQuantityRefs.current[variantId].value if (!validQuantity(quantity)) return + if (!auth) { + return redirectToLogin('add_to_cart', variantId, quantity) + } + let source = 'cart' updateCart(variantId, quantity, source) setRefreshCart(true) @@ -140,6 +142,10 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { const quantity = variantQuantityRefs.current[variant].value if (!validQuantity(quantity)) return + if (!auth) { + return redirectToLogin('buy', variant, quantity) + } + let source = 'buy' updateCart(variant, quantity, source) router.push(`/shop/checkout?source=buy`) @@ -199,47 +205,49 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { 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} /> + {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> </div> </div> - </div> - )} + )} <Image src={product.image} alt={product.name} @@ -390,7 +398,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { ))} </div> <div className='flex'> - <div className='w-3/4 leading-7 product__description'> + <div className='w-3/4 leading-8 product__description'> <TabContent active={informationTab == 'description'}> <span dangerouslySetInnerHTML={{ @@ -411,7 +419,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { </div> </div> - <div className='w-[25%]'> + <div className='w-[30%]'> {product.variants.length > 1 && product.lowestPrice.priceDiscount > 0 && ( <div className='text-gray_r-12/80'>Harga mulai dari: </div> )} @@ -440,7 +448,11 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => { )} </div> )} */} - + {product?.qtySold > 0 && ( + <div className='text-gray_r-9'> + {sellingProductFormat(product?.qtySold) + ' Terjual'} + </div> + )} {lowestPrice?.isFlashsale && lowestPrice?.price.discountPercentage > 0 ? ( <> <div className='flex gap-x-1 items-center mt-2'> diff --git a/src/lib/product/components/Product/ProductMobile.jsx b/src/lib/product/components/Product/ProductMobile.jsx index ffa75f72..ef2c0002 100644 --- a/src/lib/product/components/Product/ProductMobile.jsx +++ b/src/lib/product/components/Product/ProductMobile.jsx @@ -19,9 +19,14 @@ 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' 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) @@ -58,7 +63,8 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { price: getLowestPrice(), stock: product.stockTotal, weight: product.weight, - hasProgram: false + hasProgram: false, + qtySold: product.qtySold }) const variantOptions = product.variants?.map((variant) => { @@ -101,7 +107,8 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { stock: variant.stock, weight: variant.weight, hasProgram: variant.hasProgram, - isFlashsale: variant.isFlashsale + isFlashsale: variant.isFlashsale, + qtySold: variant.qtySold } setActiveVariant(newActiveVariant) @@ -127,9 +134,20 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { 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 handleClickCart = () => { if (!validAction()) return gtagAddToCart(activeVariant, quantity) + + if (!auth) { + return redirectToLogin('add_to_cart') + } + updateItemCart({ productId: activeVariant.id, quantity, @@ -142,6 +160,10 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { const handleClickBuy = () => { if (!validAction()) return + if (!auth) { + return redirectToLogin('buy') + } + updateItemCart({ productId: activeVariant.id, quantity, @@ -160,6 +182,7 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { 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`}> @@ -225,7 +248,9 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { </button> </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> + )} {product.variants.length > 1 && activeVariant.price.priceDiscount > 0 && !selectedVariant && ( diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index bc2174e4..9500a3fd 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -1,6 +1,7 @@ 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' @@ -82,9 +83,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { )} <Link href={createSlug('/shop/product/', product?.name, product?.id)} - className={`mb-3 !text-gray_r-12 leading-6 block ${ - simpleTitle ? 'line-clamp-2 h-12' : 'line-clamp-3 h-[64px]' - }`} + className={`mb-2 !text-gray_r-12 leading-6 block line-clamp-3 h-[64px]`} title={product?.name} > {product?.name} @@ -123,12 +122,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { </div> )} - {product?.stockTotal > 0 && ( - <div className='flex gap-x-1'> - <div className='badge-solid-red'>Ready Stock</div> - <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div> - </div> - )} + <div className='flex w-full items-center gap-x-1 '> + {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]'>{sellingProductFormat(product?.qtySold) + ' Terjual'}</div>} + </div> </div> </div> ) @@ -186,9 +184,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { )} <Link href={createSlug('/shop/product/', product?.name, product?.id)} - className={`mb-3 !text-gray_r-12 leading-6 ${ - simpleTitle ? 'line-clamp-2' : 'line-clamp-3' - }`} + className={`mb-3 !text-gray_r-12 leading-6 line-clamp-3`} > {product?.name} </Link> @@ -229,12 +225,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { </div> )} - {product?.stockTotal > 0 && ( - <div className='flex gap-x-1'> - <div className='badge-solid-red'>Ready Stock</div> - <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div> - </div> - )} + <div className='flex w-full items-center gap-x-1 '> + {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]'>{sellingProductFormat(product?.qtySold) + ' Terjual'}</div>} + </div> </div> </div> ) diff --git a/src/lib/product/components/ProductFilter.jsx b/src/lib/product/components/ProductFilter.jsx index 34357526..d52fcb90 100644 --- a/src/lib/product/components/ProductFilter.jsx +++ b/src/lib/product/components/ProductFilter.jsx @@ -3,6 +3,7 @@ import { useRouter } from 'next/router' import { useState } from 'react' import _ from 'lodash' import { toQuery } from 'lodash-contrib' +import { Checkbox } from '@chakra-ui/react' const orderOptions = [ { value: 'price-asc', label: 'Harga Terendah' }, @@ -14,12 +15,51 @@ const orderOptions = [ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBrand = null }) => { const router = useRouter() const { query } = router - const [order, setOrder] = useState(query?.orderBy) + const [order, setOrder] = useState(query?.orderBy || 'popular') const [brand, setBrand] = useState(query?.brand) const [category, setCategory] = useState(query?.category) const [priceFrom, setPriceFrom] = useState(query?.priceFrom) const [priceTo, setPriceTo] = useState(query?.priceTo) + const [stock, setStock] = useState(query?.stock) + + const [activeRange, setActiveRange] = useState(null) + + const priceRange = [ + { + priceFrom: 100000, + priceTo: 200000 + }, + { + priceFrom: 200000, + priceTo: 300000 + }, + { + priceFrom: 300000, + priceTo: 400000 + }, + { + priceFrom: 400000, + priceTo: 500000 + } + ] + + const handlePriceFromChange = async (priceFromr, priceTor, index) => { + await setPriceFrom(priceFromr) + await setPriceTo(priceTor) + setActiveRange(index) + } + + const handleReadyStockChange = (event) => { + const value = event.target.value + const isChecked = event.target.checked + if (isChecked) { + setStock(value) + } else { + setStock(null) + } + } + const handleSubmit = () => { let params = { q: router.query.q, @@ -27,17 +67,27 @@ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBr brand, category, priceFrom, - priceTo + priceTo, + stock: stock } params = _.pickBy(params, _.identity) params = toQuery(params) router.push(`${prefixUrl}?${params}`) } + const formatCurrency = (value) => { + if (value >= 1000) { + const thousands = Math.floor(value / 1000) // Menghitung ribuan + return `Rp${thousands}k` + } else { + return `Rp${value}` + } + } + return ( <BottomPopup active={active} close={close} title='Filter Produk'> <div className='flex flex-col gap-y-4'> - {!defaultBrand && ( + {!router.pathname.includes('brands') && !defaultBrand && ( <div> <label>Brand</label> <select @@ -46,15 +96,22 @@ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBr value={brand} onChange={(e) => setBrand(e.target.value)} > - <option value=''>Pilih Brand...</option> - {brands.map((brand, index) => ( - <option value={brand} key={index}> - {brand} - </option> - ))} + {brands.length > 0 ? ( + <> + <option value=''>Pilih Brand...</option> + {brands.map((brand, index) => ( + <option value={brand.brand} key={index}> + {brand.brand} <span className='text-sm text-gray-200'>({brand.qty})</span> + </option> + ))} + </> + ) : ( + <option value=''>Brands tidak tersedia</option> + )} </select> </div> )} + <div> <label>Kategori</label> <select @@ -63,12 +120,18 @@ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBr value={category} onChange={(e) => setCategory(e.target.value)} > - <option value=''>Pilih Kategori...</option> - {categories.map((category, index) => ( - <option value={category} key={index}> - {category} - </option> - ))} + {categories.length > 0 ? ( + <> + <option value=''>Pilih Kategori...</option> + {categories.map((category, index) => ( + <option value={category.name} key={index}> + {category.name} <span className='text-sm text-gray-200'>({category.qty})</span> + </option> + ))} + </> + ) : ( + <option value=''>Kategori tidak tersedia</option> + )} </select> </div> <div> @@ -106,7 +169,34 @@ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBr onChange={(e) => setPriceTo(e.target.value)} /> </div> + <div className='grid grid-cols-2 gap-x-3 gap-y-2 mt-2'> + {priceRange.map((price, i) => ( + <button + key={i} + onClick={() => handlePriceFromChange(price.priceFrom, price.priceTo, i)} + className={`w-full border ${ + i === activeRange ? 'border-red-600' : 'border-gray-400' + } + py-2 p-3 rounded-full text-sm whitespace-nowrap`} + > + {formatCurrency(price.priceFrom)} - {formatCurrency(price.priceTo)} + </button> + ))} + </div> </div> + {/* <div> + <label>Ketersedian Stok</label> + <div className='mt-2'> + <Checkbox + isChecked={stock === 'ready stock'} + onChange={handleReadyStockChange} + value={'ready stock'} + size='md' + > + Ketersedian Stok + </Checkbox> + </div> + </div> */} <button type='button' className='btn-solid-red w-full mt-2' onClick={handleSubmit}> Terapkan Filter </button> diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx index b64349c7..6118ed6b 100644 --- a/src/lib/product/components/ProductFilterDesktop.jsx +++ b/src/lib/product/components/ProductFilterDesktop.jsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router' -import { useState } from 'react' +import { useEffect, useState } from 'react' import _ from 'lodash' import { toQuery } from 'lodash-contrib' import { @@ -17,6 +17,7 @@ import { Stack, VStack } from '@chakra-ui/react' +import Image from '@/core/components/elements/Image/Image' const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = null }) => { const router = useRouter() @@ -26,6 +27,32 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu const [categoryValues, setCategory] = useState(query?.category?.split(',') || []) const [priceFrom, setPriceFrom] = useState(query?.priceFrom) const [priceTo, setPriceTo] = useState(query?.priceTo) + const [stock, setStock] = useState(query?.stock) + const [activeRange, setActiveRange] = useState(null) + const [activeIndeces, setActiveIndeces] = useState([]) + + const priceRange = [ + { + priceFrom: 100000, + priceTo: 200000 + }, + { + priceFrom: 200000, + priceTo: 300000 + }, + { + priceFrom: 300000, + priceTo: 400000 + }, + { + priceFrom: 400000, + priceTo: 500000 + } + ] + + const indexRange = priceRange.findIndex((range) => { + return range.priceFrom === parseInt(priceFrom) && range.priceTo == parseInt(priceTo) + }) const handleCategoriesChange = (event) => { const value = event.target.value @@ -46,6 +73,22 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu } } + const handleReadyStockChange = (event) => { + const value = event.target.value + const isChecked = event.target.checked + if (isChecked) { + setStock(value) + } else { + setStock(null) + } + } + + const handlePriceFromChange = async (priceFromr, priceTor, index) => { + await setPriceFrom(priceFromr) + await setPriceTo(priceTor) + setActiveRange(index) + } + const handleSubmit = () => { let params = { q: router.query.q, @@ -53,41 +96,80 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu brand: brandValues.join(','), category: categoryValues.join(','), priceFrom, - priceTo + priceTo, + stock: stock } params = _.pickBy(params, _.identity) params = toQuery(params) router.push(`${prefixUrl}?${params}`) } + const formatCurrency = (value) => { + if (value >= 1000) { + const thousands = Math.floor(value / 1000) // Menghitung ribuan + return `Rp${thousands}k` + } else { + return `Rp${value}` + } + } + + /*const handleIndexAccordion = async () => { + if (brandValues) { + await setActiveIndeces([...activeIndeces, 0]) + } + if (categoryValues) { + await setActiveIndeces([...activeIndeces, !router.pathname.includes('brands') ? 1 : 0]) + } + if (priceRange) { + await setActiveIndeces([...activeIndeces, !router.pathname.includes('brands') ? 2 : 1]) + } + if (stock) { + await setActiveIndeces([...activeIndeces, !router.pathname.includes('brands') ? 3 : 2]) + } + }*/ + + useEffect(() => { + setActiveRange(indexRange) + }, []) + + return ( <> <Accordion defaultIndex={[0]} allowMultiple> - <AccordionItem> - <AccordionButton padding={[2, 4]}> - <Box as='span' flex='1' textAlign='left' fontWeight='semibold'> - Brand - </Box> - <AccordionIcon /> - </AccordionButton> + {!router.pathname.includes('brands') && ( + <AccordionItem> + <AccordionButton padding={[2, 4]}> + <Box as='span' flex='1' textAlign='left' fontWeight='semibold'> + Brand + </Box> + <AccordionIcon /> + </AccordionButton> - <AccordionPanel> - <Stack gap={3} direction='column' maxH={'240px'} overflow='auto'> - {brands.map((brand, index) => ( - <div className='flex items-center gap-2' key={index}> - <Checkbox - isChecked={brandValues.includes(brand)} - onChange={handleBrandsChange} - value={brand} - size='md' - > - {brand} - </Checkbox> - </div> - ))} - </Stack> - </AccordionPanel> - </AccordionItem> + <AccordionPanel> + <Stack gap={3} direction='column' maxH={'240px'} overflow='auto'> + {brands && brands.length > 0 ? ( + brands.map((brand, index) => ( + <div className='flex items-center gap-2 ' key={index}> + <Checkbox + isChecked={brandValues.includes(brand.brand)} + onChange={handleBrandsChange} + value={brand.brand} + size='md' + > + <div className='flex items-center gap-2'> + <span>{brand.brand} </span> + <span className='text-sm text-gray-600'>({brand.qty})</span> + </div> + </Checkbox> + </div> + )) + ) : ( + <div className='flex items-center gap-2'>Brands tidak tersedia</div> + )} + </Stack> + </AccordionPanel> + </AccordionItem> + )} <AccordionItem> <AccordionButton padding={[2, 4]}> @@ -99,18 +181,25 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu <AccordionPanel> <Stack gap={3} direction='column' maxH={'240px'} overflow='auto'> - {categories.map((category, index) => ( - <div className='flex items-center gap-2' key={index}> - <Checkbox - isChecked={categoryValues.includes(category)} - onChange={handleCategoriesChange} - value={category} - size='md' - > - {category} - </Checkbox> - </div> - ))} + {categories && categories.length > 0 ? ( + categories.map((category, index) => ( + <div className='flex items-center gap-2' key={index}> + <Checkbox + isChecked={categoryValues.includes(category.name)} + onChange={handleCategoriesChange} + value={category.name} + size='md' + > + <div className='flex items-center gap-2'> + <span>{category.name} </span> + <span className='text-sm text-gray-600'>({category.qty})</span> + </div> + </Checkbox> + </div> + )) + ) : ( + <div className='flex items-center gap-2'>Kategori tidak tersedia</div> + )} </Stack> </AccordionPanel> </AccordionItem> @@ -143,9 +232,43 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu onChange={(e) => setPriceTo(e.target.value)} /> </InputGroup> + <div className='grid grid-cols-2 gap-x-3 gap-y-2'> + {priceRange.map((price, i) => ( + <button + key={i} + onClick={() => handlePriceFromChange(price.priceFrom, price.priceTo, i)} + className={`w-full border ${ + i === activeRange ? 'border-red-600' : 'border-gray-400' + } + py-2 p-3 rounded-full text-sm whitespace-nowrap`} + > + {formatCurrency(price.priceFrom)} - {formatCurrency(price.priceTo)} + </button> + ))} + </div> </VStack> </AccordionPanel> </AccordionItem> + + {/* <AccordionItem> + <AccordionButton padding={[2, 4]}> + <Box as='span' flex='1' textAlign='left' fontWeight='semibold'> + Ketersedian Stok + </Box> + <AccordionIcon /> + </AccordionButton> + + <AccordionPanel paddingY={4}> + <Checkbox + isChecked={stock === 'ready stock'} + onChange={handleReadyStockChange} + value={'ready stock'} + size='md' + > + Ketersedian Stock + </Checkbox> + </AccordionPanel> + </AccordionItem> */} </Accordion> <Button className='w-full mt-6' colorScheme='red' onClick={handleSubmit}> diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index df9aa91b..9d59b305 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -15,22 +15,27 @@ 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 { Image } from '@chakra-ui/react' +import odooApi from '@/core/api/odooApi' const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { const router = useRouter() const { page = 1 } = query const [q, setQ] = 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 } }) + const { productSearch } = useProductSearch({ query: { ...query, q, limit, orderBy } }) const [products, setProducts] = useState(null) const [spellings, setSpellings] = useState(null) + const [bannerPromotionHeader, setBannerPromotionHeader] = useState(null) + const [bannerPromotionFooter, setBannerPromotionFooter] = useState(null) const popup = useActive() + const numRows = [30, 50, 80, 100] - const pageCount = Math.ceil( - productSearch.data?.response.numFound / productSearch.data?.responseHeader.params.rows - ) + const pageCount = Math.ceil(productSearch.data?.response.numFound / limit) const productStart = productSearch.data?.responseHeader.params.start - const productRows = productSearch.data?.responseHeader.params.rows + const productRows = limit const productFound = productSearch.data?.response.numFound useEffect(() => { @@ -63,20 +68,44 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { } }, [productFound, query, spellings]) - const brands = productSearch.data?.facetCounts?.facetFields?.manufactureName?.filter( + 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] + if (qty > 0) { + brands.push({ brand, qty }) + } + } + /*const brandsList = productSearch.data?.facetCounts?.facetFields?.manufactureName?.filter( (value, index) => { if (index % 2 === 0) { - return true + const brand = value + const qty = index + 1 + brands.push({ brand, qty }) } } - ) - const categories = productSearch.data?.facetCounts?.facetFields?.categoryName?.filter( + )*/ + + 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 }) + } + } + + /*const categories = productSearch.data?.facetCounts?.facetFields?.categoryName?.filter( (value, index) => { if (index % 2 === 0) { return true } } - ) + )*/ const orderOptions = [ { value: 'price-asc', label: 'Harga Terendah' }, @@ -95,6 +124,30 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { 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}`) + } + 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]) + } + } + + useEffect(() => { + getBanner() + }, []) + useEffect(() => { setProducts(productSearch.data?.response?.products) }, [productSearch]) @@ -117,7 +170,7 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { <> <MobileView> {productSearch.isLoading && <ProductSearchSkeleton />} - <div className='p-4'> + <div className='p-4 pt-0'> <h1 className='mb-2 font-semibold text-h-sm'>Produk</h1> <div className='mb-2 leading-6 text-gray_r-11'> @@ -127,9 +180,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { {pageCount > 1 ? ( <> {productStart + 1}- - {productStart + productRows > productFound + {parseInt(productStart) + parseInt(productRows) > productFound ? productFound - : productStart + productRows} + : parseInt(productStart) + parseInt(productRows)} dari </> ) : ( @@ -149,9 +202,28 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { </div> {productFound > 0 && ( - <button className='btn-light mb-6 py-2 px-5' onClick={popup.activate}> - Filter - </button> + <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}> + Filter + </button> + </div> + <div className=''> + <select + name='limit' + className='form-input w-24' + value={router.query?.limit || ''} + onChange={(e) => handleLimit(e)} + > + {numRows.map((option, index) => ( + <option key={index} value={option}> + {' '} + {option}{' '} + </option> + ))} + </select> + </div> + </div> )} <div className='grid grid-cols-2 gap-3'> @@ -178,7 +250,7 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { </MobileView> <DesktopView> - <div className='container mx-auto mt-10 flex mb-3'> + <div className='container mx-auto flex mb-3'> <div className='w-3/12'> <ProductFilterDesktop brands={brands || []} @@ -188,6 +260,16 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { /> </div> <div className='w-9/12 pl-6'> + {bannerPromotionHeader && bannerPromotionHeader?.image && ( + <div className='mb-3'> + <Image + src={bannerPromotionHeader?.image} + alt='' + className='object-cover object-center h-full mx-auto' + /> + </div> + )} + <h1 className='text-2xl mb-2 font-semibold'>Hasil Pencarian</h1> <div className='flex justify-between items-center mb-2'> <div className='mb-2 leading-6 text-gray_r-11'> @@ -197,9 +279,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { {pageCount > 1 ? ( <> {productStart + 1}- - {productStart + productRows > productFound + {parseInt(productStart) + parseInt(productRows) > productFound ? productFound - : productStart + productRows} + : parseInt(productStart) + parseInt(productRows)} dari </> ) : ( @@ -222,10 +304,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { <select name='urutan' className='form-input mt-2' - value={router.query?.orderBy || ''} + value={orderBy} onChange={(e) => handleOrderBy(e)} > - <option value=''>Urutkan</option> {orderOptions.map((option, index) => ( <option key={index} value={option.value}> {' '} @@ -234,6 +315,21 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { ))} </select> </div> + <div className='ml-3'> + <select + name='limit' + className='form-input mt-2' + value={router.query?.limit || ''} + onChange={(e) => handleLimit(e)} + > + {numRows.map((option, index) => ( + <option key={index} value={option}> + {' '} + {option}{' '} + </option> + ))} + </select> + </div> </div> </div> {productSearch.isLoading && <ProductSearchSkeleton />} @@ -275,6 +371,15 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { className='!justify-end' /> </div> + {bannerPromotionFooter && bannerPromotionFooter?.image && ( + <div className='mb-3'> + <Image + src={bannerPromotionFooter?.image} + alt='' + className='object-cover object-center h-full mx-auto' + /> + </div> + )} </div> </div> </DesktopView> diff --git a/src/lib/promotinProgram/components/PromotionType.jsx b/src/lib/promotinProgram/components/PromotionType.jsx index ad7185e3..51f2622a 100644 --- a/src/lib/promotinProgram/components/PromotionType.jsx +++ b/src/lib/promotinProgram/components/PromotionType.jsx @@ -24,7 +24,7 @@ const PromotionType = ({ const id = variantId const listProgram = async () => { const programs = await getPromotionProgram({ id }) - if (programs.length > 0) { + if (programs?.length > 0) { setPromotionList(programs) setActiveTitle(programs?.[0].type.value) } diff --git a/src/lib/variant/components/VariantCard.jsx b/src/lib/variant/components/VariantCard.jsx index 6b06bc31..9f1b5733 100644 --- a/src/lib/variant/components/VariantCard.jsx +++ b/src/lib/variant/components/VariantCard.jsx @@ -40,7 +40,7 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { {product.attributes.length > 0 ? ` ・ ${product.attributes.join(', ')}` : ''} </p> <p className='text-caption-2 text-gray_r-11 mt-1'> - Berat Item : {product?.weight} Kg + Berat Item : {product?.weight} Kg x {product?.quantity} Barang </p> <div className='flex flex-wrap gap-x-1 items-center mt-auto'> {product.hasFlashsale && ( |
