diff options
Diffstat (limited to 'src')
53 files changed, 2893 insertions, 833 deletions
diff --git a/src/api/productApi.js b/src/api/productApi.js index 009d95ef..4a29b59d 100644 --- a/src/api/productApi.js +++ b/src/api/productApi.js @@ -2,8 +2,11 @@ import axios from 'axios' export const popularProductApi = () => { return async () => { + const today = new Date(); + const dayOfYear = Math.floor((today - new Date(today.getFullYear(), 0, 0)) / 86400000); + const page = (dayOfYear % 24) + 1; const dataPopularProducts = await axios( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?q=*&page=1&orderBy=popular-weekly&priceFrom=1` + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?q=*&page=${page}&orderBy=stock&priceFrom=1` ) return dataPopularProducts.data.response } diff --git a/src/components/ui/PopularProduct.jsx b/src/components/ui/PopularProduct.jsx index bbbd18bc..92b2a1b6 100644 --- a/src/components/ui/PopularProduct.jsx +++ b/src/components/ui/PopularProduct.jsx @@ -5,6 +5,7 @@ import { useQuery } from 'react-query' import { PopularProductSkeleton } from '../skeleton/PopularProductSkeleton' import DesktopView from '@/core/components/views/DesktopView' import ProductCard from '@/lib/product/components/ProductCard' +import Link from '@/core/components/elements/Link/Link' const PopularProduct = () => { const popularProduct = useQuery('popularProduct', popularProductApi()) @@ -16,15 +17,31 @@ const PopularProduct = () => { <> <MobileView> <div className='px-4'> - <div className='font-semibold mb-4'>Produk Banyak Dilihat</div> + <div className='font-semibold mb-4 flex justify-between items-center'><p> + Produk Ready Stock + </p> + <Link + href='/shop/search?orderBy=stock' + className='' + > + <p className='text-danger-500 font-semibold'>Lihat Semua</p> + </Link></div> <ProductSlider products={popularProduct.data} simpleTitle /> </div> </MobileView> <DesktopView> <div className='border border-gray_r-6 h-full overflow-auto'> - <div className='font-semibold text-center p-4 bg-gray_r-1 border-b border-gray_r-6 sticky top-0 z-10'> - Produk Banyak Dilihat + <div className='font-semibold text-center p-4 bg-gray_r-1 border-b border-gray_r-6 sticky top-0 z-10 flex justify-between items-center'> + <p> + Produk Ready Stock + </p> + <Link + href='/shop/search?orderBy=stock' + className='' + > + <p className='text-danger-500 font-semibold'>Lihat Semua</p> + </Link> </div> <div className='h-full divide-y divide-gray_r-6'> {popularProduct.data && diff --git a/src/contexts/ProductCartContext.js b/src/contexts/ProductCartContext.js index 06e97563..3a21d2e0 100644 --- a/src/contexts/ProductCartContext.js +++ b/src/contexts/ProductCartContext.js @@ -6,10 +6,11 @@ export const ProductCartProvider = ({ children }) => { const [productCart, setProductCart] = useState(null) const [refreshCart, setRefreshCart] = useState(false) const [isLoading, setIsloading] = useState(false) + const [productQuotation, setProductQuotation] = useState(null) return ( <ProductCartContext.Provider - value={{ productCart, setProductCart, refreshCart, setRefreshCart, isLoading, setIsloading }} + value={{ productCart, setProductCart, refreshCart, setRefreshCart, isLoading, setIsloading, productQuotation, setProductQuotation}} > {children} </ProductCartContext.Provider> diff --git a/src/core/components/elements/Footer/BasicFooter.jsx b/src/core/components/elements/Footer/BasicFooter.jsx index 4beea604..8f024d86 100644 --- a/src/core/components/elements/Footer/BasicFooter.jsx +++ b/src/core/components/elements/Footer/BasicFooter.jsx @@ -175,7 +175,7 @@ const CustomerGuide = () => ( <div> <div className={headerClassName}>Bantuan & Panduan</div> <ul className='flex flex-col gap-y-3'> - <li> + <li > <InternalItemLink href='/metode-pembayaran'> Metode Pembayaran </InternalItemLink> @@ -210,6 +210,11 @@ const CustomerGuide = () => ( Panduan Pick Up Service </InternalItemLink> </li> + <li> + <InternalItemLink href='/tracking-order'> + Tracking Order + </InternalItemLink> + </li> </ul> </div> ); diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index 2ddf5efe..ebbcf857 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -5,7 +5,9 @@ import { createSlug } from '@/core/utils/slug'; import whatsappUrl from '@/core/utils/whatsappUrl'; import IndoteknikLogo from '@/images/logo.png'; import Cardheader from '@/lib/cart/components/Cartheader'; +import Quotationheader from "../../../../../src/lib/quotation/components/Quotationheader.jsx" import Category from '@/lib/category/components/Category'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; import { ChevronDownIcon, DocumentCheckIcon, @@ -29,6 +31,8 @@ import { useDisclosure, } from '@chakra-ui/react'; import style from "./style/NavbarDesktop.module.css"; +import useTransactions from '@/lib/transaction/hooks/useTransactions'; +import { useCartStore } from '~/modules/cart/stores/useCartStore'; const Search = dynamic(() => import('./Search'), { ssr: false }); const TopBanner = dynamic(() => import('./TopBanner'), { ssr: false }); @@ -38,15 +42,27 @@ const NavbarDesktop = () => { const auth = useAuth(); const [cartCount, setCartCount] = useState(0); - + const [quotationCount, setQuotationCount] = useState(0); + const [pendingTransactions, setPendingTransactions] = useState([]) const [templateWA, setTemplateWA] = useState(null); const [payloadWA, setPayloadWa] = useState(null); const [urlPath, setUrlPath] = useState(null); - + const { loadCart, cart, summary, updateCartItem } = useCartStore(); const router = useRouter(); const { product } = useProductContext(); const { isOpen, onOpen, onClose } = useDisclosure(); + + const query = { + context: 'quotation', + site: + (auth?.webRole === null && auth?.site ? auth.site : null), + }; + + const { transactions } = useTransactions({ query }); + const data = transactions?.data?.saleOrders.filter( + (transaction) => transaction.status === 'draft' + ); const [showPopup, setShowPopup] = useState(false); const [isTop, setIsTop] = useState(true); @@ -89,6 +105,11 @@ const NavbarDesktop = () => { }, []); useEffect(() => { + setPendingTransactions(data); + }, [transactions.data]); + + + useEffect(() => { if (router.pathname === '/shop/product/[slug]') { setPayloadWa({ name: product?.name, @@ -96,11 +117,11 @@ const NavbarDesktop = () => { url: createSlug('/shop/product/', product?.name, product?.id, true), }); setTemplateWA('product'); - + setUrlPath(router.asPath); } }, [product, router]); - + useEffect(() => { const handleCartChange = () => { const cart = async () => { @@ -109,15 +130,31 @@ const NavbarDesktop = () => { }; cart(); }; - handleCartChange(); - + handleCartChange(); + window.addEventListener('localStorageChange', handleCartChange); - + return () => { window.removeEventListener('localStorageChange', handleCartChange); }; - }, []); + }, [transactions.data, cart]); + useEffect(() => { + const handleQuotationChange = () => { + const quotation = async () => { + setQuotationCount(pendingTransactions?.length); + }; + quotation(); + }; + handleQuotationChange(); + + window.addEventListener('localStorageChange', handleQuotationChange); + + return () => { + window.removeEventListener('localStorageChange', handleQuotationChange); + }; + }, [pendingTransactions]); + return ( <DesktopView> <TopBanner onLoad={handleTopBannerLoad} /> @@ -180,17 +217,7 @@ const NavbarDesktop = () => { <Search /> </div> <div className='flex gap-x-4 items-center'> - <Link - href='/my/transactions' - target='_blank' - rel='noreferrer' - className='flex items-center gap-x-2 !text-gray_r-12/80' - > - <DocumentCheckIcon className='w-7' /> - Daftar - <br /> - Quotation - </Link> + <Quotationheader quotationCount={quotationCount} data={pendingTransactions} /> <Cardheader cartCount={cartCount} /> @@ -225,8 +252,7 @@ const NavbarDesktop = () => { <div className='container mx-auto mt-6'> <div className='flex'> - <button - type='button' + <div onClick={() => setIsOpenCategory((isOpen) => !isOpen)} onBlur={() => setIsOpenCategory(false)} className='w-3/12 p-4 font-semibold border border-gray_r-6 rounded-t-xl flex items-center relative' @@ -243,7 +269,7 @@ const NavbarDesktop = () => { > <Category /> </div> - </button> + </div> <div className='w-6/12 flex px-1 divide-x divide-gray_r-6'> <Link @@ -291,7 +317,7 @@ const NavbarDesktop = () => { <Link href='/shop/search?orderBy=stock' className={`${ - router.asPath === '/shop/search?orderBy=stock' && + router.asPath.includes('/shop/search?orderBy=stock') && 'bg-gray_r-3' } p-4 flex-1 flex justify-center items-center !text-gray_r-12/80 hover:bg-gray_r-3 idt-transition group`} target='_blank' diff --git a/src/core/components/elements/Navbar/NavbarMobile.jsx b/src/core/components/elements/Navbar/NavbarMobile.jsx index bcf45e0a..90314671 100644 --- a/src/core/components/elements/Navbar/NavbarMobile.jsx +++ b/src/core/components/elements/Navbar/NavbarMobile.jsx @@ -11,7 +11,7 @@ import Image from 'next/image'; import { useEffect, useState } from 'react'; import MobileView from '../../views/MobileView'; import Link from '../Link/Link'; -// import TopBanner from './TopBanner'; +import TopBanner from './TopBanner'; const Search = dynamic(() => import('./Search')); @@ -39,7 +39,7 @@ const NavbarMobile = () => { return ( <MobileView> - {/* <TopBanner /> */} + <TopBanner /> <nav className='px-4 py-2 pb-3 sticky top-0 z-50 bg-white shadow'> <div className='flex justify-between items-center mb-2'> <Link href='/'> diff --git a/src/core/components/elements/Navbar/TopBanner.jsx b/src/core/components/elements/Navbar/TopBanner.jsx index 7bc8fb6a..f438ae67 100644 --- a/src/core/components/elements/Navbar/TopBanner.jsx +++ b/src/core/components/elements/Navbar/TopBanner.jsx @@ -1,19 +1,20 @@ import Image from 'next/image'; -import { useQuery } from 'react-query'; - +import { useQuery } from 'react-query';import useDevice from '@/core/hooks/useDevice' import odooApi from '@/core/api/odooApi'; import SmoothRender from '~/components/ui/smooth-render'; import Link from '../Link/Link'; +import { background } from '@chakra-ui/react'; import { useEffect } from 'react'; -const TopBanner = ({ onLoad }) => { +const TopBanner = ({ onLoad = () => {} }) => { + const { isDesktop, isMobile } = useDevice() const topBanner = useQuery({ queryKey: 'topBanner', queryFn: async () => await odooApi('GET', '/api/v1/banner?type=top-banner'), refetchOnWindowFocus: false, }); - const backgroundColor = topBanner.data?.[0]?.backgroundColor || 'transparent'; + // const backgroundColor = topBanner.data?.[0]?.backgroundColor || 'transparent'; const hasData = topBanner.data?.length > 0; const data = topBanner.data?.[0] || null; @@ -26,21 +27,21 @@ const TopBanner = ({ onLoad }) => { return ( <SmoothRender isLoaded={hasData} - height='36px' + // height='36px' duration='700ms' delay='300ms' - style={{ backgroundColor }} - > - <Link href={data?.url}> - <Image - src={data?.image} - alt={data?.name} - width={1440} - height={40} - className='object-cover object-center h-full mx-auto' - /> - </Link> - </SmoothRender> + className='h-auto' + > + <Link + href={data?.url} + className="block bg-cover bg-center h-3 md:h-6 lg:h-[36px]" + style={{ + backgroundImage: `url('${data?.image}')`, + }} + > + </Link> + + </SmoothRender> ); }; diff --git a/src/core/components/elements/Sidebar/Sidebar.jsx b/src/core/components/elements/Sidebar/Sidebar.jsx index 55838890..ddae3e20 100644 --- a/src/core/components/elements/Sidebar/Sidebar.jsx +++ b/src/core/components/elements/Sidebar/Sidebar.jsx @@ -5,6 +5,8 @@ import { AnimatePresence, motion } from 'framer-motion' import { ChevronDownIcon, ChevronUpIcon, CogIcon, UserIcon } from '@heroicons/react/24/outline' import { Fragment, useEffect, useState } from 'react' import odooApi from '@/core/api/odooApi' +import { createSlug } from '@/core/utils/slug' +import Image from 'next/image' const Sidebar = ({ active, close }) => { const auth = useAuth() @@ -79,7 +81,7 @@ const Sidebar = ({ active, close }) => { exit={{ left: '-80%' }} transition={transition} > - <div className='divide-y divide-gray_r-6'> + <div className='divide-y divide-gray_r-6 h-full flex flex-col'> <div className='p-4 flex gap-x-3'> {!auth && ( <> @@ -112,104 +114,115 @@ const Sidebar = ({ active, close }) => { href='/my/menu' className='!text-gray_r-11 ml-auto my-auto' > - <UserIcon class='h-6 w-6 text-gray-500' /> + <UserIcon className='h-6 w-6 text-gray-500' /> </Link> </> )} </div> - <SidebarLink className={itemClassName} href='/shop/promo'> - Semua Promo - </SidebarLink> - <SidebarLink className={itemClassName} href='/shop/brands'> - Semua Brand - </SidebarLink> - <SidebarLink - className={itemClassName} - href='https://blog.indoteknik.com/' - target='_blank' - rel='noreferrer noopener' - > - Blog Indoteknik - </SidebarLink> - {/* <SidebarLink className={itemClassName} href='/video'> - Indoteknik TV - </SidebarLink> */} - <SidebarLink className={itemClassName} href='/tentang-kami'> - Tentang Indoteknik - </SidebarLink> - <SidebarLink className={itemClassName} href='/contact-us'> - Hubungi Kami - </SidebarLink> - <button - className={`${itemClassName} w-full text-left flex`} - onClick={() => setOpenCategory(!isOpenCategory)} - > - Kategori - <div className='ml-auto'> - {!isOpenCategory && <ChevronDownIcon className='text-gray_r-12 w-5' />} - {isOpenCategory && <ChevronUpIcon className='text-gray_r-12 w-5' />} - </div> - </button> - {isOpenCategory && - categories.map((category) => ( - <Fragment key={category.id}> - <div className='flex w-full text-gray_r-11 border-b border-gray_r-6 px-4 pl-8 items-center'> - <Link - href={`/shop/search?category=${category.name}`} - className='flex-1 font-normal !text-gray_r-11 py-4' - > - {category.name} - </Link> - <div - className='ml-4 h-full py-4' - onClick={() => toggleCategories(category.id)} - > - {!category.isOpen && <ChevronDownIcon className='text-gray_r-11 w-5' />} - {category.isOpen && <ChevronUpIcon className='text-gray_r-11 w-5' />} + <div className='overflow-y-auto flex-1'> + <SidebarLink className={itemClassName} href='/shop/promo'> + Semua Promo + </SidebarLink> + <SidebarLink className={itemClassName} href='/shop/brands'> + Semua Brand + </SidebarLink> + <SidebarLink + className={itemClassName} + href='https://blog.indoteknik.com/' + target='_blank' + rel='noreferrer noopener' + > + Blog Indoteknik + </SidebarLink> + {/* <SidebarLink className={itemClassName} href='/video'> + Indoteknik TV + </SidebarLink> */} + <SidebarLink className={itemClassName} href='/tentang-kami'> + Tentang Indoteknik + </SidebarLink> + <SidebarLink className={itemClassName} href='/contact-us'> + Hubungi Kami + </SidebarLink> + <button + className={`${itemClassName} w-full text-left flex`} + onClick={() => setOpenCategory(!isOpenCategory)} + > + Kategori + <div className='ml-auto'> + {!isOpenCategory && <ChevronDownIcon className='text-gray_r-12 w-5' />} + {isOpenCategory && <ChevronUpIcon className='text-gray_r-12 w-5' />} + </div> + </button> + {isOpenCategory && + categories.map((category) => ( + <Fragment key={category.id}> + <div className='flex w-full text-gray_r-11 border-b border-gray_r-6 px-4 pl-8 items-center'> + <Link + href={createSlug('/shop/category/', category.name, category.id)} + className='flex-1 font-normal !text-gray_r-11 py-4 flex items-center flex-row' + > + <div className='mr-2 flex justify-center items-center'> + <Image src={category.image} alt='' width={25} height={25} /> + </div> + {category.name} + </Link> + <div + className='ml-4 h-full py-4' + onClick={() => toggleCategories(category.id)} + > + {!category.isOpen && <ChevronDownIcon className='text-gray_r-11 w-5' />} + {category.isOpen && <ChevronUpIcon className='text-gray_r-11 w-5' />} + </div> </div> - </div> - {category.isOpen && - category.childs.map((child1Category) => ( - <Fragment key={child1Category.id}> - <div - className={`flex w-full !text-gray_r-11 border-b border-gray_r-6 p-4 pl-12 ${ - category.isOpen ? 'bg-gray_r-2' : '' - }`} - > - <Link - href={`/shop/search?category=${child1Category.name}`} - className='flex-1 font-normal !text-gray_r-11' + {category.isOpen && + category.childs.map((child1Category) => ( + <Fragment key={child1Category.id}> + <div + className={`flex w-full !text-gray_r-11 border-b border-gray_r-6 p-4 pl-12 ${ + category.isOpen ? 'bg-gray_r-2' : '' + }`} > - {child1Category.name} - </Link> - {child1Category.childs.length > 0 && ( - <div - className='ml-4 h-full' - onClick={() => toggleCategories(child1Category.id)} - > - {!child1Category.isOpen && ( - <ChevronDownIcon className='text-gray_r-11 w-5' /> - )} - {child1Category.isOpen && ( - <ChevronUpIcon className='text-gray_r-11 w-5' /> - )} - </div> - )} - </div> - {child1Category.isOpen && - child1Category.childs.map((child2Category) => ( <Link - key={child2Category.id} - href={`/shop/search?category=${child2Category.name}`} - className='flex w-full font-normal !text-gray_r-11 border-b border-gray_r-6 p-4 pl-16' + href={createSlug('/shop/category/', child1Category.name, child1Category.id)} + className='flex-1 font-normal !text-gray_r-11 flex flex-row items-center' > - {child2Category.name} + <div className='mr-2 flex justify-center items-center'> + <Image src={`https://erp.indoteknik.com/api/image/product.public.category/image_1920/${child1Category.id}`} alt='' width={25} height={25} /> + </div> + {child1Category.name} </Link> - ))} - </Fragment> - ))} - </Fragment> - ))} + {child1Category.childs.length > 0 && ( + <div + className='ml-4 h-full' + onClick={() => toggleCategories(child1Category.id)} + > + {!child1Category.isOpen && ( + <ChevronDownIcon className='text-gray_r-11 w-5' /> + )} + {child1Category.isOpen && ( + <ChevronUpIcon className='text-gray_r-11 w-5' /> + )} + </div> + )} + </div> + {child1Category.isOpen && + child1Category.childs.map((child2Category) => ( + <Link + key={child2Category.id} + href={createSlug('/shop/category/', child2Category.name, child2Category.id)} + className='flex w-full font-normal !text-gray_r-11 border-b border-gray_r-6 p-4 pl-16 flex-row' + > + <div className='mr-2 flex justify-center items-center'> + <Image src={`https://erp.indoteknik.com/api/image/product.public.category/image_1920/${child2Category.id}`} alt='' width={25} height={25} /> + </div> + {child2Category.name} + </Link> + ))} + </Fragment> + ))} + </Fragment> + ))} + </div> </div> </motion.div> </> diff --git a/src/lib/brand/components/BrandCard.jsx b/src/lib/brand/components/BrandCard.jsx index 731214ff..2d78d956 100644 --- a/src/lib/brand/components/BrandCard.jsx +++ b/src/lib/brand/components/BrandCard.jsx @@ -8,7 +8,7 @@ const BrandCard = ({ brand }) => { return ( <Link href={createSlug('/shop/brands/', brand.name, brand.id)} - className={`py-1 px-2 rounded border border-gray_r-6 flex justify-center items-center ${ + className={`py-1 px-2 border-gray_r-6 flex justify-center items-center hover:scale-110 transition duration-500 ease-in-out ${ isMobile ? 'h-16' : 'h-24' }`} > @@ -16,9 +16,9 @@ const BrandCard = ({ brand }) => { <Image src={brand.logo} alt={brand.name} - width={128} - height={128} - className='h-full w-full object-contain object-center' + width={50} + height={50} + className='h-full w-[122px] object-contain object-center' /> )} {!brand.logo && ( diff --git a/src/lib/cart/components/Cartheader.jsx b/src/lib/cart/components/Cartheader.jsx index 19f79bc9..ddb77c1f 100644 --- a/src/lib/cart/components/Cartheader.jsx +++ b/src/lib/cart/components/Cartheader.jsx @@ -1,14 +1,20 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { getCartApi } from '../api/CartApi' +import currencyFormat from '@/core/utils/currencyFormat' +import { createSlug } from '@/core/utils/slug' import useAuth from '@/core/hooks/useAuth' import { useRouter } from 'next/router' import odooApi from '@/core/api/odooApi' import { useProductCartContext } from '@/contexts/ProductCartContext' - +import Image from '@/core/components/elements/Image/Image' +import whatsappUrl from '@/core/utils/whatsappUrl' +import { AnimatePresence, motion } from 'framer-motion' +import style from '../../../../src-migrate/modules/cart/styles/item-promo.module.css' const { ShoppingCartIcon, PhotoIcon } = require('@heroicons/react/24/outline') const { default: Link } = require('next/link') const Cardheader = (cartCount) => { + const router = useRouter() const [subTotal, setSubTotal] = useState(null) const [buttonLoading, SetButtonTerapkan] = useState(false) @@ -19,7 +25,7 @@ const Cardheader = (cartCount) => { useProductCartContext() const [isHovered, setIsHovered] = useState(false) - + const [isTop, setIsTop] = useState(true) const products = useMemo(() => { return productCart?.products || [] }, [productCart]) @@ -42,7 +48,7 @@ const Cardheader = (cartCount) => { setIsloading(true) let cart = await getCartApi() setProductCart(cart) - setCountCart(cart.productTotal) + setCountCart(cart?.productTotal) setIsloading(false) }, [setProductCart, setIsloading]) @@ -75,14 +81,26 @@ const Cardheader = (cartCount) => { useEffect(() => { setCountCart(cartCount.cartCount) + setRefreshCart(false) }, [cartCount]) + useEffect(() => { + const handleScroll = () => { + setIsTop(window.scrollY === 0) + } + window.addEventListener('scroll', handleScroll) + return () => { + window.removeEventListener('scroll', handleScroll) + } + }, []) + const handleCheckout = async () => { SetButtonTerapkan(true) let checkoutAll = await odooApi('POST', `/api/v1/user/${auth.id}/cart/select-all`) router.push('/shop/checkout') } + return ( <div className='relative group'> <div> @@ -109,6 +127,246 @@ const Cardheader = (cartCount) => { </span> </Link> </div> + <AnimatePresence> + {isHovered && ( + <> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1, top: isTop ? 230 : 155 }} + exit={{ opacity: 0 }} + transition={{ duration: 0.15, top: { duration: 0.3 } }} + className={`fixed 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> + )} + {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> + )} + {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='bagian gambar flex-shrink-0'> + {product.cartType === 'promotion' && ( + <Image + src={product.imageProgram[0]} + alt={product.name} + className='object-contain object-center border border-gray_r-6 h-16 w-16 rounded-md' + /> + )} + {product.cartType === 'product' && ( + <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='bagian tulisan dan harga flex-1 min-w-0'> + {product.cartType === 'promotion' && ( + <p className='text-caption-2 font-medium text-gray-900 truncate dark:text-white'> + {product.name} + </p> + )} + {product.cartType === 'product' && ( + <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 flex-col w-3/4"> + {product.products?.map((product) => + <div key={product.id} className='md:ml-8 ml-4 mt-2 flex'> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className='md:h-12 md:w-12 md:min-w-[48px] h-10 w-10 min-w-[40px] border border-gray-300 rounded '> + {product?.image && <Image src={product.image} alt={product.name} width={40} height={40} className='w-full h-full object-fill' />} + </Link> + + <div className="ml-4 w-full flex flex-col gap-y-1"> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className="text-caption-2 font-medium text-gray-900 truncate dark:text-white"> + {product.displayName} + </Link> + + <div className='flex w-full'> + <div className="flex flex-col"> + {/* <div className="text-gray-500 text-caption-1">{product.code}</div> */} + <div> + <span className="text-gray-500 text-caption-1">Berat Barang: </span> + <span className="text-gray-500 text-caption-1">{product.packageWeight} Kg</span> + </div> + </div> + </div> + </div> + + </div> + )} + {product.freeProducts?.map((product) => + <div key={product.id} className='md:ml-8 ml-4 mt-2 flex'> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className='md:h-12 md:w-12 md:min-w-[48px] h-10 w-10 min-w-[40px] border border-gray-300 rounded '> + {product?.image && <Image src={product.image} alt={product.name} width={40} height={40} className='w-full h-full object-fill' />} + </Link> + + <div className="ml-4 w-full flex flex-col gap-y-1"> + <Link href={createSlug('/shop/product/', product.parent.name, product.parent.id.toString())} className="text-caption-2 font-medium text-gray-900 truncate dark:text-white"> + {product.displayName} + </Link> + + <div className='flex w-full'> + <div className="flex flex-col"> + {/* <div className="text-gray-500 text-caption-1">{product.code}</div> */} + <div> + <span className="text-gray-500 text-caption-1">Berat Barang: </span> + <span className="text-gray-500 text-caption-1">{product.packageWeight} Kg</span> + </div> + </div> + </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> + </> + )} + </motion.div> + </motion.div> + </> + )} + </AnimatePresence> </div> ) } diff --git a/src/lib/category/api/popularProduct.js b/src/lib/category/api/popularProduct.js new file mode 100644 index 00000000..3fdfc41c --- /dev/null +++ b/src/lib/category/api/popularProduct.js @@ -0,0 +1,32 @@ + +export const fetchPopulerProductSolr = async (category_id_ids) => { + let sort ='sort=qty_sold_f desc'; + try { + const queryParams = new URLSearchParams({ q: category_id_ids }); + const response = await fetch(`/solr/product/select?${queryParams.toString()}&rows=2000&fl=manufacture_name_s,manufacture_id_i,id,display_name_s,qty_sold_f,qty_sold_f&${sort}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + const promotions = await map(data.response.docs); + return promotions; + } catch (error) { + console.error("Error fetching promotion data:", error); + return []; + } + }; + + const map = async (promotions) => { + const result = []; + for (const promotion of promotions) { + const data = { + id: promotion.id, + name: promotion.display_name_s, + manufacture_name: promotion.manufacture_name_s, + manufacture_id: promotion.manufacture_id_i, + qty_sold: promotion.qty_sold_f, + }; + result.push(data); + } + return result; + };
\ No newline at end of file diff --git a/src/lib/category/components/Category.jsx b/src/lib/category/components/Category.jsx index e6ea5acf..91553295 100644 --- a/src/lib/category/components/Category.jsx +++ b/src/lib/category/components/Category.jsx @@ -2,11 +2,20 @@ import odooApi from '@/core/api/odooApi' import Link from '@/core/components/elements/Link/Link' import DesktopView from '@/core/components/views/DesktopView' import { createSlug } from '@/core/utils/slug' +import { ChevronRightIcon } from '@heroicons/react/24/outline' +import Image from 'next/image' import { useEffect, useState } from 'react' +import PopularBrand from './PopularBrand' +import { bannerApi } from '@/api/bannerApi'; +const { useQuery } = require('react-query') const Category = () => { const [categories, setCategories] = useState([]) + const [openCategories, setOpenCategory] = useState([]); + const [banner, setBanner] = useState([]); + const promotionProgram = useQuery('banner-promo-category-card', bannerApi({ type: 'banner-promo-category-card' })); + useEffect(() => { const loadCategories = async () => { let dataCategories = await odooApi('GET', '/api/v1/category/tree') @@ -26,46 +35,65 @@ const Category = () => { } loadCategories() }, []) - return ( <DesktopView> <div className='category-mega-box'> {categories?.map((category) => ( - <div key={category.id}> + <div key={category.id} className='flex'> <Link href={createSlug('/shop/category/', category.name, category.id)} - className='category-mega-box__parent' + className='category-mega-box__parent flex items-center' > + <div className='mr-2 flex justify-center items-center'> + <Image src={category.image} alt='' width={25} height={25} /> + </div> {category.name} </Link> <div className='category-mega-box__child-wrapper'> - <div className='grid grid-cols-3 gap-x-4 gap-y-6 max-h-full overflow-auto'> + <div className='grid grid-cols-3 gap-x-4 gap-y-6 max-h-full !w-[590px] overflow-auto'> {category.childs.map((child1Category) => ( - <div key={child1Category.id}> + <div key={child1Category.id} className='w-full'> <Link href={createSlug('/shop/category/', child1Category.name, child1Category.id)} - className='category-mega-box__child-one mb-4' + className='category-mega-box__child-one mb-4 w-full h-8 flex justify-center line-clamp-2' > {child1Category.name} </Link> - <div className='flex flex-col gap-y-3'> - {child1Category.childs.map((child2Category) => ( - <Link - href={createSlug( - '/shop/category/', - child2Category.name, - child2Category.id - )} - className='category-mega-box__child-two' - key={child2Category.id} - > - {child2Category.name} - </Link> + <div className='flex flex-col gap-y-3 w-full'> + {child1Category.childs.map((child2Category, index) => ( + (index < 4) && ( + <Link + href={createSlug('/shop/category/', child2Category.name, child2Category.id)} + className='category-mega-box__child-two truncate' + key={child2Category.id} + > + {child2Category.name} + </Link> + ) ))} + {child1Category.childs.length > 5 && ( + <div className='flex hover:bg-gray_r-8/35 rounded-10'> + <Link + href={createSlug('/shop/category/', child1Category.name, child1Category.id)} + className='category-mega-box__child-one flex items-center gap-4 font-bold hover:ml-4' + > + <p className='mt-2 mb-0 text-danger-500 font-semibold'>Lihat Semua</p> + <ChevronRightIcon className='w-4 text-danger-500 font-bold' /> + </Link> + </div> + )} </div> </div> ))} </div> + {/* <div className='category-mega-box__child-wrapper !w-[260px] !flex !flex-col !gap-4'> + <PopularBrand category={category} /> + {Array.isArray(promotionProgram?.data) && promotionProgram?.data.length > 0 && promotionProgram?.data[0]?.map((banner, index) => ( + <div key={index} className='flex w-60 h-20 object-cover'> + <Image src={`${banner.image}`} alt={`${banner.name}`} width={275} height={4} /> + </div> + ))} + </div> */} </div> </div> ))} diff --git a/src/lib/category/components/PopularBrand.jsx b/src/lib/category/components/PopularBrand.jsx new file mode 100644 index 00000000..8124b5b4 --- /dev/null +++ b/src/lib/category/components/PopularBrand.jsx @@ -0,0 +1,96 @@ +import odooApi from '@/core/api/odooApi' +import React, { useEffect, useState } from 'react' +import axios from 'axios'; +import { useQuery } from 'react-query' +import Link from '@/core/components/elements/Link/Link' +import { createSlug } from '@/core/utils/slug' +import Image from 'next/image' +import { ChevronRightIcon } from '@heroicons/react/24/outline' +import useProductSearch from '../../../lib/product/hooks/useProductSearch'; +import { SolrResponse } from "~/types/solr"; +import { fetchPopulerProductSolr } from '../api/popularProduct' + +const SOLR_HOST = process.env.SOLR_HOST + +const PopularBrand = ({ category }) => { + // const [topBrands, setTopBrands] = useState([]); + + // const fetchTopBrands = async () => { + // try { + // const items = await fetchPopulerProductSolr(`category_id_ids:(${category?.categoryDataIds?.join(' OR ')})`); + // const getTop12UniqueBrands = (prod) => { + // const brandMap = new Map(); + + // for (const product of prod) { + // const { manufacture_name, manufacture_id, qty_sold } = product; + + // if (brandMap.has(manufacture_name)) { + // // Update the existing brand's qty_sold + // brandMap.set(manufacture_name, { + // name: manufacture_name, + // id: manufacture_id, + // qty_sold: brandMap.get(manufacture_name).qty_sold + qty_sold + // }); + // } else { + // // Add a new brand to the map + // brandMap.set(manufacture_name, { + // name: manufacture_name, + // id: manufacture_id, + // qty_sold + // }); + // } + // } + + // // Convert the map to an array and sort by qty_sold in descending order + // const sortedBrands = Array.from(brandMap.values()).sort((a, b) => b.qty_sold - a.qty_sold); + + // // Return the top 12 brands + // return sortedBrands.slice(0, 18); + // }; + + // // Using the fetched products + // const products = items; + // const top12UniqueBrands = getTop12UniqueBrands(products); + + // // Set the top 12 brands to the state + // setTopBrands(top12UniqueBrands); + // } catch (error) { + // console.error("Error fetching data from Solr", error); + // throw error; + // } + // } + + // useEffect(() => { + // fetchTopBrands(); + // }, [category]); + + return ( + <div className='flex flex-col'> + {/* <div className='grid grid-cols-3 max-h-full w-full gap-2'> + {topBrands.map((brand, index) => ( + <div key={index} className='w-full flex items-center justify-center pb-2'> + <Link + href={createSlug('/shop/brands/', brand.name, brand.id)} + className='category-mega-box__child-one w-8 h-full flex items-center justify-center ' + > + <Image src={`https://erp.indoteknik.com/api/image/x_manufactures/x_logo_manufacture/${brand.id}` } alt={`${brand.name}`} width={104} height={44} objectFit='cover' /> + </Link> + </div> + ))} + </div> */} + {/* {topBrands.length > 8 && ( + <div className='flex hover:bg-gray_r-8/35 rounded-10'> + <Link + href={createSlug('/shop/category/', category.name, category.id)} + className='category-mega-box__child-one flex items-center gap-4 font-bold hover:ml-4' + > + <p className='mt-2 mb-0 text-danger-500 font-semibold'>Lihat Semua Brand</p> + <ChevronRightIcon className='w-4 text-danger-500 font-bold' /> + </Link> + </div> + )} */} + </div> + ) +} + +export default PopularBrand; diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 09a791ee..f63ef457 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -131,6 +131,7 @@ const Checkout = () => { setLoadingVoucher(true); let dataVoucher = await getVoucher(auth?.id, { source: query, + type: 'all,brand', }); SetListVoucher(dataVoucher); @@ -146,40 +147,94 @@ const Checkout = () => { }; const VoucherCode = async (code) => { - const source = 'code=' + code + '&source=' + query; // let dataVoucher = await findVoucher(code, auth.id, query); - let dataVoucher = await getVoucherNew(source); + let dataVoucher = await getVoucher(auth?.id, { + source: query, + code: code, + }); if (dataVoucher.length <= 0) { SetFindVoucher(1); return; } - let addNewLine = dataVoucher[0]; - let checkList = listVouchers?.findIndex( - (voucher) => voucher.code == addNewLine.code - ); - if (checkList >= 0) { - if (listVouchers[checkList].canApply) { - ToggleSwitch(code); - SetCodeVoucher(null); + dataVoucher.forEach((addNewLine) => { + if (addNewLine.applyType !== 'shipping') { + // Mencari voucher dalam listVouchers + let checkList = listVouchers?.findIndex( + (voucher) => voucher.code === addNewLine.code + ); + + if (checkList >= 0) { + if (listVouchers[checkList].canApply) { + ToggleSwitch(addNewLine.code); // Perbaikan: Gunakan code voucher yang benar + SetCodeVoucher(null); + } else { + SetSelisihHargaCode(listVouchers[checkList].differenceToApply); + SetFindVoucher(2); + } + return; // Hentikan eksekusi lebih lanjut pada iterasi ini + } + // Memeriksa apakah subtotal memenuhi syarat minimal pembelian + if (cartCheckout?.subtotal < addNewLine.minPurchaseAmount) { + SetSelisihHargaCode( + currencyFormat( + addNewLine.minPurchaseAmount - cartCheckout?.subtotal + ) + ); + SetFindVoucher(2); + return; + } else { + SetFindVoucher(3); + SetButtonTerapkan(true); + } + + // Tambahkan voucher ke list dan set voucher aktif + SetListVoucher((prevList) => [addNewLine, ...prevList]); + if (addNewLine.canApply) { + SetActiveVoucher(addNewLine.code); + } } else { - SetSelisihHargaCode(listVouchers[checkList].differenceToApply); - SetFindVoucher(2); + // Mencari voucher dalam listVoucherShippings + let checkList = listVoucherShippings?.findIndex( + (voucher) => voucher.code === addNewLine.code + ); + + if (checkList >= 0) { + if (listVoucherShippings[checkList].canApply) { + ToggleSwitch(addNewLine.code); // Perbaikan: Gunakan code voucher yang benar + SetCodeVoucher(null); + } else { + SetSelisihHargaCode( + listVoucherShippings[checkList].differenceToApply + ); + SetFindVoucher(2); + } + return; // Hentikan eksekusi lebih lanjut pada iterasi ini + } + + // Memeriksa apakah subtotal memenuhi syarat minimal pembelian + if (cartCheckout?.subtotal < addNewLine.minPurchaseAmount) { + SetSelisihHargaCode( + currencyFormat( + addNewLine.minPurchaseAmount - cartCheckout?.subtotal + ) + ); + SetFindVoucher(2); + return; + } else { + SetFindVoucher(3); + SetButtonTerapkan(true); + } + + // Tambahkan voucher ke list pengiriman dan set voucher aktif pengiriman + SetListVoucherShipping((prevList) => [addNewLine, ...prevList]); + if (addNewLine.canApply) { + setActiveVoucherShipping(addNewLine.code); + } } - return; - } - if (cartCheckout?.subtotal < addNewLine.minPurchaseAmount) { - SetSelisihHargaCode( - currencyFormat(addNewLine.minPurchaseAmount - cartCheckout?.subtotal) - ); - SetFindVoucher(2); - return; - } else { - SetFindVoucher(3); - SetButtonTerapkan(true); - } - SetListVoucher((prevList) => [addNewLine, ...prevList]); - SetActiveVoucher(addNewLine.code); + }); + + // let addNewLine = dataVoucher[0]; }; useEffect(() => { @@ -187,7 +242,7 @@ const Checkout = () => { }, [bottomPopup]); useEffect(() => { - voucher(); + // voucher(); const loadExpedisi = async () => { let dataExpedisi = await ExpedisiList(); dataExpedisi = dataExpedisi.map((expedisi) => ({ @@ -210,13 +265,23 @@ const Checkout = () => { }; }, []); - const hitungDiscountVoucher = (code) => { - let dataVoucherIndex = listVouchers.findIndex( - (voucher) => voucher.code == code - ); - let dataActiveVoucher = listVouchers[dataVoucherIndex]; + const hitungDiscountVoucher = (code, source) => { + let countDiscount = 0; + if (source === 'voucher') { + let dataVoucherIndex = listVouchers.findIndex( + (voucher) => voucher.code == code + ); + let dataActiveVoucher = listVouchers[dataVoucherIndex]; + + countDiscount = dataActiveVoucher.discountVoucher; + } else { + let dataVoucherIndex = listVoucherShippings.findIndex( + (voucher) => voucher.code == code + ); + let dataActiveVoucher = listVoucherShippings[dataVoucherIndex]; - let countDiscount = dataActiveVoucher.discountVoucher; + countDiscount = dataActiveVoucher.discountVoucher; + } /*if (dataActiveVoucher.discountType === 'percentage') { countDiscount = cartCheckout?.subtotal * (dataActiveVoucher.discountAmount / 100) @@ -233,14 +298,24 @@ const Checkout = () => { return countDiscount; }; - useEffect(() => { - if (!listVouchers) return; - if (!activeVoucher) return; + // useEffect(() => { + // if (!listVouchers) return; + // if (!activeVoucher) return; + + // console.log('voucher') + // const countDiscount = hitungDiscountVoucher(activeVoucher, 'voucher'); + + // SetDiscountVoucher(countDiscount); + // }, [activeVoucher, listVouchers]); - const countDiscount = hitungDiscountVoucher(activeVoucher); + // useEffect(() => { + // if (!listVoucherShippings) return; + // if (!activeVoucherShipping) return; - SetDiscountVoucher(countDiscount); - }, [activeVoucher, listVouchers]); + // const countDiscount = hitungDiscountVoucher(activeVoucherShipping, 'voucher_shipping'); + + // SetDiscountVoucherOngkir(countDiscount); + // }, [activeVoucherShipping, listVoucherShippings]); useEffect(() => { if (qVoucher === 'PASTIHEMAT' && listVouchers) { @@ -335,7 +410,7 @@ const Checkout = () => { Math.round(parseInt(finalShippingAmt * 1.1) / 1000) * 1000; const finalGT = GT < 0 ? 0 : GT; setGrandTotal(finalGT); - }, [biayaKirim, cartCheckout?.grandTotal, activeVoucher]); + }, [biayaKirim, cartCheckout?.grandTotal, activeVoucher, activeVoucherShipping]); const checkout = async () => { const file = poFile.current.files[0]; @@ -389,14 +464,22 @@ const Checkout = () => { if (typeof file !== 'undefined') data.po_file = await getFileBase64(file); const isCheckouted = await checkoutApi({ data }); + if (!isCheckouted?.id) { toast.error('Gagal melakukan transaksi, terjadi kesalahan internal'); return; - } - - gtagPurchase(products, biayaKirim, isCheckouted.name); + } else { + gtagPurchase(products, biayaKirim, isCheckouted.name); + + gtag('event', 'conversion', { + send_to: 'AW-954540379/nDymCL3BhaQYENvClMcD', + value: + cartCheckout?.grandTotal + + Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000, + currency: 'IDR', + transaction_id: isCheckouted.id, + }); - const midtrans = async () => { for (const product of products) deleteItemCart({ productId: product.id }); if (grandTotal > 0) { const payment = await axios.post( @@ -412,17 +495,25 @@ const Checkout = () => { '-' )}`; } - }; + } - gtag('event', 'conversion', { - send_to: 'AW-954540379/nDymCL3BhaQYENvClMcD', - value: - cartCheckout?.grandTotal + - Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000, - currency: 'IDR', - transaction_id: isCheckouted.id, - event_callback: midtrans, - }); + /* const midtrans = async () => { + for (const product of products) deleteItemCart({ productId: product.id }); + if (grandTotal > 0) { + const payment = await axios.post( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/midtrans-payment?transactionId=${isCheckouted.id}` + ); + setIsLoading(false); + window.location.href = payment.data.redirectUrl; + } else { + window.location.href = `${ + process.env.NEXT_PUBLIC_SELF_HOST + }/shop/checkout/success?order_id=${isCheckouted.name.replace( + /\//g, + '-' + )}`; + } + };*/ }; const handlingActivateCode = async () => { @@ -483,6 +574,10 @@ const Checkout = () => { const finalShippingAmt = biayaKirim - discShippingAmt; + const totalDiscountVoucher = + cartCheckout?.discountVoucher + + (cartCheckout?.discountVoucherShipping || 0); + return ( <> <BottomPopup @@ -593,10 +688,25 @@ const Checkout = () => { )} <hr className='mt-8 mb-4 border-gray_r-8' /> + {/* {!loadingVoucher && + listVouchers?.length === 1 && + listVoucherShippings?.length === 1} + { + <div className='flex items-center justify-center mt-4 mb-4'> + <div className='text-center'> + <h1 className='font-bold mb-4'>Tidak ada voucher tersedia</h1> + <p className='text-gray-500'> + Maaf, saat ini tidak ada voucher yang tersedia. + </p> + </div> + </div> + } */} {listVoucherShippings && listVoucherShippings?.length > 0 && ( <div> - <h3 className='font-semibold mb-4'>Promo Gratis Ongkir</h3> + <h3 className='font-semibold mb-4'> + Promo Extra Potongan Ongkir + </h3> {listVoucherShippings?.map((item) => ( <div key={item.id} className='relative'> <div @@ -731,16 +841,7 @@ const Checkout = () => { <hr className='mt-8 mb-4 border-gray_r-8' /> <div> - {!loadingVoucher && listVouchers?.length === 0 ? ( - <div className='flex items-center justify-center mt-4 mb-4'> - <div className='text-center'> - <h1 className='font-bold mb-4'>Tidak ada voucher tersedia</h1> - <p className='text-gray-500'> - Maaf, saat ini tidak ada voucher yang tersedia. - </p> - </div> - </div> - ) : ( + {!loadingVoucher && listVouchers?.length > 0 && ( <h3 className='font-semibold mb-4'> Promo Khusus Untuk {auth?.name} </h3> @@ -932,7 +1033,7 @@ const Checkout = () => { </div> <span className='leading-5'> Jika mengalami kesulitan dalam melakukan pembelian di website - Indoteknik. Hubungi kami disini + Indoteknik. <a href={whatsappUrl()}>Hubungi kami disini</a> </span> </Alert> </div> @@ -1004,7 +1105,12 @@ const Checkout = () => { <div className='p-4 flex flex-col gap-y-4'> {!!products && snakecaseKeys(products).map((item, index) => ( - <CartItem key={index} item={item} editable={false} /> + <CartItem + key={index} + item={item} + editable={false} + selfPicking={selectedExpedisi === '1,32' ? true : false} + /> ))} </div> @@ -1067,7 +1173,7 @@ const Checkout = () => { <div className='flex gap-x-2 justify-between'> <div className='text-gray_r-11'>Diskon Voucher</div> <div className='text-danger-500'> - - {currencyFormat(discountVoucher)} + - {currencyFormat(cartCheckout?.discountVoucher)} </div> </div> )} @@ -1083,7 +1189,7 @@ const Checkout = () => { <div className='text-gray_r-11'> Biaya Kirim <p className='text-xs mt-1'>{etdFix}</p> </div> - <div>{currencyFormat(biayaKirim)}</div> + <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000)}</div> </div> {activeVoucherShipping && voucherShippingAmt && ( <div className='flex gap-x-2 justify-between'> @@ -1135,10 +1241,10 @@ const Checkout = () => { className='object-contain object-center h-6 rounded-md' /> </span> - {activeVoucher ? ( + {activeVoucher || activeVoucherShipping ? ( <div className=''> <div className='text-left text-sm text-black font-semibold'> - Potongan Senilai {currencyFormat(discountVoucher)} + Potongan Senilai {currencyFormat(totalDiscountVoucher)} </div> <div className='text-left mt-1 text-green-600 text-xs'> Voucher berhasil digunakan @@ -1295,7 +1401,12 @@ const Checkout = () => { <div className='flex flex-col gap-y-8 border-t border-gray-300 pt-8'> {!!products && snakecaseKeys(products).map((item, index) => ( - <CartItem key={index} item={item} editable={false} /> + <CartItem + key={index} + item={item} + editable={false} + selfPicking={selectedExpedisi === '1,32' ? true : false} + /> ))} </div> </div> @@ -1362,7 +1473,7 @@ const Checkout = () => { <div className='flex gap-x-2 justify-between'> <div className='text-gray_r-11'>Diskon Voucher</div> <div className='text-danger-500'> - - {currencyFormat(discountVoucher)} + - {currencyFormat(cartCheckout?.discountVoucher)} </div> </div> )} @@ -1379,7 +1490,7 @@ const Checkout = () => { Biaya Kirim <p className='text-xs mt-1'>{etdFix}</p> </div> - <div>{currencyFormat(biayaKirim)}</div> + <div>{currencyFormat(Math.round(parseInt(biayaKirim * 1.1) / 1000) * 1000) }</div> </div> {activeVoucherShipping && voucherShippingAmt && ( <div className='flex gap-x-2 justify-between'> @@ -1431,10 +1542,10 @@ const Checkout = () => { className='object-contain object-center h-6 w-full rounded-md' /> </span> - {activeVoucher ? ( + {activeVoucher || activeVoucherShipping ? ( <div className=''> <div className='text-left text-sm text-black font-semibold'> - Hemat {currencyFormat(discountVoucher)} + Hemat {currencyFormat(totalDiscountVoucher)} </div> <div className='text-left mt-1 text-green-600 text-xs'> Voucher berhasil digunakan diff --git a/src/lib/home/api/CategoryPilihanApi.js b/src/lib/home/api/CategoryPilihanApi.js new file mode 100644 index 00000000..8a0b38d3 --- /dev/null +++ b/src/lib/home/api/CategoryPilihanApi.js @@ -0,0 +1,8 @@ +import odooApi from '@/core/api/odooApi' + +const categoryPilihanApi = async () => { + const dataCategoryPilihan = await odooApi('GET', '/api/v1/lob_homepage') + return dataCategoryPilihan +} + +export default categoryPilihanApi diff --git a/src/lib/home/api/categoryManagementApi.js b/src/lib/home/api/categoryManagementApi.js new file mode 100644 index 00000000..b70d60ce --- /dev/null +++ b/src/lib/home/api/categoryManagementApi.js @@ -0,0 +1,8 @@ +import odooApi from '@/core/api/odooApi' + +const categoryManagementApi = async () => { + const dataCategoryManagement = await odooApi('GET', '/api/v1/categories_management') + return dataCategoryManagement +} + +export default categoryManagementApi diff --git a/src/lib/home/components/CategoryDynamic.jsx b/src/lib/home/components/CategoryDynamic.jsx new file mode 100644 index 00000000..14015e28 --- /dev/null +++ b/src/lib/home/components/CategoryDynamic.jsx @@ -0,0 +1,132 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import useCategoryManagement from '../hooks/useCategoryManagement'; +import {fetchPopulerProductSolr} from '../api/categoryManagementApi' +import NextImage from 'next/image'; +import Link from "next/link"; +import { createSlug } from '@/core/utils/slug'; +import odooApi from '@/core/api/odooApi'; +import { Skeleton } from '@chakra-ui/react'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import 'swiper/css/navigation'; +import 'swiper/css/pagination'; +import { Navigation, Pagination, Autoplay } from 'swiper'; + +const CategoryDynamic = () => { + + const { categoryManagement } = useCategoryManagement(); + // const [categoryData, setCategoryData] = useState({}); + // const [subCategoryData, setSubCategoryData] = useState({}); + + // useEffect(() => { + // const fetchCategoryData = async () => { + // if (categoryManagement && categoryManagement.data) { + // const updatedCategoryData = {}; + // const updatedSubCategoryData = {}; + + // for (const category of categoryManagement.data) { + // const countLevel1 = await odooApi('GET', `/api/v1/category/numFound?parent_id=${category.categoryIdI}`); + + // updatedCategoryData[category.categoryIdI] = countLevel1?.numFound; + + // for (const subCategory of countLevel1?.children) { + // updatedSubCategoryData[subCategory.id] = subCategory?.numFound; + // } + // } + + // setCategoryData(updatedCategoryData); + // setSubCategoryData(updatedSubCategoryData); + // } + // }; + + // fetchCategoryData(); + // }, [categoryManagement.isLoading]); + + const swiperBanner = { + modules: [Pagination, ], + classNames:'mySwiper', + slidesPerView: 3, + spaceBetween:10, + pagination: { + dynamicBullets: true, + clickable: true, + } + }; + + return ( + <div> + {categoryManagement && categoryManagement.data?.map((category) => { + // const countLevel1 = categoryData[category.categoryIdI] || 0; + + return ( + <Skeleton key={category.id} isLoaded={categoryManagement}> + <div key={category.id}> + <div className='bagian-judul flex flex-row justify-start items-center gap-3 mb-4 mt-4'> + <div className='font-semibold sm:text-h-lg mr-2'>{category.name}</div> + {/* <Skeleton isLoaded={countLevel1 != 0}> + <p className={`text-gray_r-10 text-sm`}>{countLevel1} Produk tersedia</p> + </Skeleton> */} + <Link href={createSlug('/shop/category/', category?.name, category?.categoryIdI)} className="!text-red-500 font-semibold">Lihat Semua</Link> + </div> + + {/* Swiper for SubCategories */} + <Swiper {...swiperBanner} + > + {category.categories.map((subCategory) => { + // const countLevel2 = subCategoryData[subCategory.idLevel2] || 0; + + return ( + <SwiperSlide key={subCategory.id}> + <div className='border rounded justify-start items-start '> + <div className='p-3'> + <div className='flex flex-row border rounded mb-2 justify-start items-center'> + <NextImage + src={subCategory.image ? subCategory.image : "/images/noimage.jpeg"} + alt={subCategory.name} + width={90} + height={30} + className='object-fit p-4' + /> + <div className='bagian-judul flex flex-col justify-center items-start gap-2 ml-2'> + <div className='font-semibold text-lg mr-2'>{subCategory?.name}</div> + {/* <Skeleton isLoaded={countLevel2 != 0}> + <p className={`text-gray_r-10 text-sm`}> + {countLevel2} Produk tersedia + </p> + </Skeleton> */} + <Link href={createSlug('/shop/category/', subCategory?.name, subCategory?.idLevel2)} className="!text-red-500 font-semibold">Lihat Semua</Link> + </div> + </div> + <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px] min-h-[240px] content-start'> + {subCategory.childFrontendIdI.map((childCategory) => ( + <div key={childCategory.id} className=''> + <Link href={createSlug('/shop/category/', childCategory?.name, childCategory?.idLevel3)} className="flex flex-row gap-2 border rounded group hover:border-red-500"> + <NextImage + src={childCategory.image ? childCategory.image : "/images/noimage.jpeg"} + alt={childCategory.name} + className='p-2 ml-1' + width={40} + height={40} + /> + <div className='bagian-judul flex flex-col justify-center items-center gap-2 break-words line-clamp-2 group-hover:text-red-500'> + <div className='font-semibold line-clamp-2 group-hover:text-red-500 text-sm mr-2'>{childCategory.name}</div> + </div> + </Link> + </div> + ))} + </div> + </div> + </div> + </SwiperSlide> + ); + })} + </Swiper> + </div> + </Skeleton> + ); + })} + </div> + ); +}; + +export default CategoryDynamic; diff --git a/src/lib/home/components/CategoryDynamicMobile.jsx b/src/lib/home/components/CategoryDynamicMobile.jsx new file mode 100644 index 00000000..2877a5a7 --- /dev/null +++ b/src/lib/home/components/CategoryDynamicMobile.jsx @@ -0,0 +1,101 @@ +import React, { useEffect, useState } from 'react'; +import useCategoryManagement from '../hooks/useCategoryManagement'; +import NextImage from 'next/image'; +import Link from "next/link"; +import { createSlug } from '@/core/utils/slug'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; + +const CategoryDynamicMobile = () => { + const { categoryManagement } = useCategoryManagement() + const [selectedCategory, setSelectedCategory] = useState({}); + + useEffect(() => { + const loadPromo = async () => { + try { + if (categoryManagement.data?.length > 0) { + const initialSelections = categoryManagement.data.reduce((acc, category) => { + if (category.categories.length > 0) { + acc[category.id] = category.categories[0].idLevel2; + } + return acc; + }, {}); + setSelectedCategory(initialSelections); + } + } catch (loadError) { + // console.error("Error loading promo items:", loadError); + } + }; + + loadPromo(); + }, [categoryManagement.data]); + + const handleCategoryLevel2Click = (categoryIdI, idLevel2) => { + setSelectedCategory(prev => ({ + ...prev, + [categoryIdI]: idLevel2 + })); + }; + + return ( + <div className='p-4'> + {categoryManagement.data && categoryManagement.data.map((category) => ( + <div key={category.id}> + <div className='bagian-judul flex flex-row justify-between items-center gap-3 mb-4 mt-4'> + <div className='font-semibold sm:text-h-sm mr-2'>{category.name}</div> + <Link href={createSlug('/shop/category/', category?.name, category?.categoryIdI)} className="!text-red-500 font-semibold text-sm">Lihat Semua</Link> + </div> + <Swiper slidesPerView={2.3} spaceBetween={10}> + {category.categories.map((index) => ( + <SwiperSlide key={index.id}> + <div + onClick={() => handleCategoryLevel2Click(category.id, index?.idLevel2)} + className={`border flex justify-start items-center max-w-48 max-h-16 rounded ${selectedCategory[category.id] === index?.idLevel2 ? 'bg-red-50 border-red-500 text-red-500' : 'border-gray-200 text-gray-900'}`} + > + <div className='p-1 flex justify-start items-center'> + <div className='flex flex-row justify-center items-center'> + <NextImage + src={index.image ? index.image : "/images/noimage.jpeg"} + alt={index.name} + width={30} + height={30} + className='' + /> + <div className='bagian-judul flex flex-col justify-center items-start gap-1 ml-2'> + <div className='font-semibold text-[10px] line-clamp-1'>{index.name}</div> + </div> + </div> + </div> + </div> + </SwiperSlide> + ))} + </Swiper> + <div className='p-3 mt-2 border'> + <div className='grid grid-cols-2 gap-2 overflow-y-auto max-h-[240px]'> + {category.categories.map((index) => ( + selectedCategory[category.id] === index?.idLevel2 && index.childFrontendIdI.map((x) => ( + <div key={x.id}> + <Link href={createSlug('/shop/category/', x?.name, x?.idLevel3)} className="flex flex-row gap-1 border rounded group hover:border-red-500"> + <NextImage + src={x.image ? x.image : "/images/noimage.jpeg"} + alt={x.name} + width={40} + height={40} + className='p-2' + /> + <div className='bagian-judul flex flex-col justify-center items-start gap-1 break-words line-clamp-2 group-hover:text-red-500'> + <div className='font-semibold line-clamp-2 group-hover:text-red-500 text-[10px]'>{x.name}</div> + </div> + </Link> + </div> + )) + ))} + </div> + </div> + </div> + ))} + </div> + ); +}; + +export default CategoryDynamicMobile; diff --git a/src/lib/home/components/CategoryPilihan.jsx b/src/lib/home/components/CategoryPilihan.jsx new file mode 100644 index 00000000..6dbf771e --- /dev/null +++ b/src/lib/home/components/CategoryPilihan.jsx @@ -0,0 +1,123 @@ +import Image from 'next/image' +import useCategoryHome from '../hooks/useCategoryHome' +import Link from '@/core/components/elements/Link/Link' +import { createSlug } from '@/core/utils/slug' +import { useEffect, useState } from 'react'; +import { bannerApi } from '../../../api/bannerApi'; +const { useQuery } = require('react-query') +import { HeroBannerSkeleton } from '../../../components/skeleton/BannerSkeleton'; +import useCategoryPilihan from '../hooks/useCategoryPilihan'; +import useDevice from '@/core/hooks/useDevice' +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; + +const CategoryPilihan = ({ id, categories }) => { + const { isDesktop, isMobile } = useDevice() + const { categoryPilihan } = useCategoryPilihan(); + const heroBanner = useQuery('categoryPilihan', bannerApi({ type: 'banner-category-list' })); + return ( + categoryPilihan.length > 0 && ( + <section> + {isDesktop && ( + <div> + <div className='flex flex-row items-center mb-4'> + <div className='font-semibold sm:text-h-lg mr-2'>LOB Kategori Pilihan</div> + <p className='text-gray_r-10 text-sm'>200 Rb+ Produk Unggulan & 800+ Brand Rekomendasi tersedia!</p> + </div> + {heroBanner.data && + heroBanner.data?.length > 0 && ( + <div className='flex w-full h-full justify-center mb-4 bg-cover bg-center'> + <Link key={heroBanner.data[0].id} href={heroBanner.data[0].url}> + <Image + width={1260} + height={170} + quality={100} + src={heroBanner.data[0].image} + alt={heroBanner.data[0].name} + className='h-full object-cover w-full' + /> + </Link> + </div> + )} + <div className="group/item grid grid-cols-6 gap-y-2 w-full h-full col-span-2 "> + {categoryPilihan?.data?.map((category) => ( + <div key={category.id} className="KartuInti h-48 w-60 max-w-sm lg:max-w-full flex flex-col border-[1px] border-gray-200 relative group"> + <div className='KartuB absolute h-48 w-60 inset-0 flex items-center justify-center '> + <div className="group/edit flex items-center justify-end h-48 w-60 flex-col group-hover/item:visible"> + <div className=' h-36 flex justify-end items-end'> + <Image className='group-hover:scale-105 transition-transform duration-300 ' src={category?.image? category?.image : '/images/noimage.jpeg'} width={120} height={120} alt={category?.name} /> + </div> + <h2 className="text-gray-700 content-center h-12 border-t-[1px] px-1 w-60 border-gray-200 font-normal text-sm text-center">{category?.industries}</h2> + </div> + </div> + <div className='KartuA relative inset-0 flex h-36 w-60 items-center justify-center opacity-0 group-hover:opacity-75 group-hover:bg-[#E20613] transition-opacity '> + <Link + href={createSlug('/shop/lob/', category?.industries, category?.id)} + className='category-mega-box__parent text-white rounded-lg' + > + Lihat semua + </Link> + </div> + </div> + ))} + </div> + </div> + )} + {isMobile && ( + <div className='p-4'> + <div className='flex flex-row items-center mb-4'> + <div className='font-semibold sm:text-h-md mr-2'>LOB Kategori Pilihan</div> + {/* <p className='text-gray_r-10 text-sm'>200 Rb+ Produk Unggulan & 800+ Brand Rekomendasi tersedia!</p> */} + </div> + <div className='flex'> + {heroBanner.data && + heroBanner.data?.length > 0 && ( + <div className=' object-fill '> + <Link key={heroBanner.data[0].id} href={heroBanner.data[0].url}> + <Image + width={439} + height={150} + quality={100} + src={heroBanner.data[0].image} + alt={heroBanner.data[0].name} + className='object-cover' + /> + </Link> + </div> + )} + </div> + <Swiper slidesPerView={2.1} spaceBetween={10}> + {categoryPilihan?.data?.map((category) => ( + <SwiperSlide key={category.id}> + <div key={category.id} className="KartuInti mt-2 h-48 w-48 max-w-sm lg:max-w-full flex flex-col border-[1px] border-gray-200 relative group"> + <div className='KartuB absolute h-48 w-48 inset-0 flex items-center justify-center '> + <div className="group/edit flex items-center justify-end h-48 w-48 flex-col group-hover/item:visible"> + <div className=' h-36 flex justify-end items-end'> + <Image className='group-hover:scale-105 transition-transform duration-300 ' src={category?.image? category?.image : '/images/noimage.jpeg'} width={120} height={120} alt={category?.name} /> + </div> + <h2 className="text-gray-700 content-center h-12 border-t-[1px] px-1 w-48 border-gray-200 font-normal text-sm text-center">{category?.industries}</h2> + </div> + </div> + <div className='KartuA relative inset-0 flex h-36 w-48 items-center justify-center opacity-0 group-hover:opacity-75 group-hover:bg-[#E20613] transition-opacity '> + <Link + href={createSlug('/shop/lob/', category?.industries, category?.id)} + className='category-mega-box__parent text-white rounded-lg' + > + Lihat semua + </Link> + </div> + </div> + </SwiperSlide> + ))} + + </Swiper> + + </div> + )} + </section> + + ) + ) +} + +export default CategoryPilihan diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx index 6b64a444..b30fa5c9 100644 --- a/src/lib/home/components/PreferredBrand.jsx +++ b/src/lib/home/components/PreferredBrand.jsx @@ -1,4 +1,5 @@ import { Swiper, SwiperSlide } from 'swiper/react' +import { Navigation, Pagination, Autoplay } from 'swiper'; import { useCallback, useEffect, useState } from 'react' import usePreferredBrand from '../hooks/usePreferredBrand' import PreferredBrandSkeleton from './Skeleton/PreferredBrandSkeleton' @@ -38,7 +39,23 @@ const PreferredBrand = () => { const { preferredBrands } = usePreferredBrand(query) const { isMobile, isDesktop } = useDevice() - + const swiperBanner = { + modules:[Navigation, Pagination, Autoplay], + autoplay: { + delay: 4000, + disableOnInteraction: false + }, + loop: true, + className: 'h-[70px] md:h-[100px] w-full', + slidesPerView: isMobile ? 4 : 8, + spaceBetween: isMobile ? 12 : 0, + pagination: { + dynamicBullets: true, + dynamicMainBullets: isMobile ? 6 : 8, + clickable: true, + } + } + const preferredBrandsData = manufactures ? manufactures.slice(0, 20) : [] return ( <div className='px-4 sm:px-0'> <div className='flex justify-between items-center mb-4'> @@ -48,24 +65,21 @@ const PreferredBrand = () => { Lihat Semua </Link> )} - {isMobile && ( - <Link href='/shop/brands' className='!text-red-500 font-semibold sm:text-h-sm'> - Lihat Semua - </Link> + </div> + <div className=''> + {manufactures.isLoading && <PreferredBrandSkeleton />} + {!manufactures.isLoading && ( + <Swiper {...swiperBanner}> + {preferredBrandsData.map((manufacture) => ( + <SwiperSlide key={manufacture.id}> + <BrandCard brand={manufacture} /> + </SwiperSlide> + ))} + </Swiper> )} </div> - {manufactures.isLoading && <PreferredBrandSkeleton />} - {!manufactures.isLoading && ( - <Swiper slidesPerView={isMobile ? 3.5 : 7.5} spaceBetween={isMobile ? 12 : 24} freeMode> - {manufactures.map((manufacture) => ( - <SwiperSlide key={manufacture.id}> - <BrandCard brand={manufacture} /> - </SwiperSlide> - ))} - </Swiper> - )} </div> ) } -export default PreferredBrand +export default PreferredBrand
\ No newline at end of file diff --git a/src/lib/home/hooks/useCategoryManagement.js b/src/lib/home/hooks/useCategoryManagement.js new file mode 100644 index 00000000..c1dda585 --- /dev/null +++ b/src/lib/home/hooks/useCategoryManagement.js @@ -0,0 +1,13 @@ +import categoryManagementApi from '../api/categoryManagementApi' +import { useQuery } from 'react-query' + +const useCategoryManagement = () => { + const fetchCategoryManagement = async () => await categoryManagementApi() + const { isLoading, data } = useQuery('categoryManagementApi', fetchCategoryManagement) + + return { + categoryManagement: { data, isLoading } + } +} + +export default useCategoryManagement
\ No newline at end of file diff --git a/src/lib/home/hooks/useCategoryPilihan.js b/src/lib/home/hooks/useCategoryPilihan.js new file mode 100644 index 00000000..12a86f7e --- /dev/null +++ b/src/lib/home/hooks/useCategoryPilihan.js @@ -0,0 +1,13 @@ +import categoryPilihanApi from '../api/CategoryPilihanApi' +import { useQuery } from 'react-query' + +const useCategoryPilihan = () => { + const fetchCategoryPilihan = async () => await categoryPilihanApi() + const { isLoading, data } = useQuery('categoryPilihanApi', fetchCategoryPilihan) + + return { + categoryPilihan: { data, isLoading } + } +} + +export default useCategoryPilihan
\ No newline at end of file diff --git a/src/lib/lob/components/Breadcrumb.jsx b/src/lib/lob/components/Breadcrumb.jsx new file mode 100644 index 00000000..5722fd39 --- /dev/null +++ b/src/lib/lob/components/Breadcrumb.jsx @@ -0,0 +1,55 @@ +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( + `lob-breadcrumbs/${categoryId}`, + async () => await odooApi('GET', `/api/v1/lob_homepage/${categoryId}/category_id`) + ) + 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.industries}</BreadcrumbLink> + ) : ( + <BreadcrumbLink + as={Link} + href={createSlug('/shop/lob/', category.industries, category.id)} + className='!text-danger-500 whitespace-nowrap' + > + {category.industries} + </BreadcrumbLink> + )} + </BreadcrumbItem> + ))} + </ChakraBreadcrumb> + </Skeleton> + </div> + ) +} + +export default Breadcrumb diff --git a/src/lib/product/api/productSearchApi.js b/src/lib/product/api/productSearchApi.js index 1626b7b7..8ff8e57d 100644 --- a/src/lib/product/api/productSearchApi.js +++ b/src/lib/product/api/productSearchApi.js @@ -1,7 +1,7 @@ import _ from 'lodash-contrib' import axios from 'axios' -const productSearchApi = async ({ query, operation = 'AND' }) => { +const productSearchApi = async ({ query, operation = 'OR' }) => { const dataProductSearch = await axios( `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}&operation=${operation}` ) diff --git a/src/lib/product/components/CategorySection.jsx b/src/lib/product/components/CategorySection.jsx new file mode 100644 index 00000000..a287fa78 --- /dev/null +++ b/src/lib/product/components/CategorySection.jsx @@ -0,0 +1,105 @@ +import Image from "next/image"; +import Link from 'next/link'; +import { createSlug } from '@/core/utils/slug'; +import useDevice from '@/core/hooks/useDevice'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import { useQuery } from 'react-query'; +import { useRouter } from 'next/router'; +import { + ChevronDownIcon, + ChevronUpIcon, // Import ChevronUpIcon for toggling + DocumentCheckIcon, + HeartIcon, +} from '@heroicons/react/24/outline'; +import { useState } from 'react'; // Import useState +import { getIdFromSlug } from '@/core/utils/slug' + +const CategorySection = ({ categories }) => { + const { isDesktop, isMobile } = useDevice(); + const [isOpenCategory, setIsOpenCategory] = useState(false); // State to manage category visibility + + const handleToggleCategories = () => { + setIsOpenCategory(!isOpenCategory); + }; + + + const displayedCategories = isOpenCategory ? categories : categories.slice(0, 10); + + return ( + <section> + {isDesktop && ( + <div className="group/item grid grid-cols-5 gap-y-2 gap-x-2 w-full h-full col-span-2 "> + {displayedCategories.map((category) => ( + <Link href={createSlug('/shop/category/', category?.name, category?.id)} key={category?.id} passHref> + <div className="group transition-colors duration-300 "> + <div className="KartuInti h-12 w-26 max-w-sm lg:max-w-full flex flex-col border-[2px] border-gray-200 group-hover:border-red-400 rounded relative "> + <div className="flex items-center justify-start h-full px-1 flex-row "> + <Image className="h-full p-1" src={category?.image1920 ? category?.image1920 : '/images/noimage.jpeg'} width={56} height={48} alt={category?.name} /> + <h2 className="text-gray-700 group-hover:text-[#E20613] line-clamp-2 content-center h-fit w-60 px-1 font-semibold text-sm text-start">{category?.name}</h2> + </div> + </div> + </div> + </Link> + ))} + </div> + )} + {isDesktop && categories.length > 10 && ( + <div className="w-full flex justify-center mt-4"> + <button + onClick={handleToggleCategories} + className="flex justify-end mt-4 text-red-500 font-bold px-4 py-2 rounded" + > + {isOpenCategory ? 'Sembunyikan' : 'Lihat semua'} + {isOpenCategory ? ( + <ChevronUpIcon className="ml-auto w-5 font-bold" /> + ) : ( + <ChevronDownIcon className="ml-auto w-5 font-bold" /> + )} + </button> + </div> + )} + + {isMobile && ( + <div className="py-4"> + <Swiper slidesPerView={2.3} spaceBetween={10}> + {categories.map((category) => ( + <SwiperSlide key={category?.id}> + <Link href={createSlug('/shop/category/', category?.name, category?.id)} passHref> + <div className="group transition-colors duration-300"> + <div className="KartuInti min-h-16 max-h-16 w-26 max-w-sm lg:max-w-full flex flex-col border-[2px] border-gray-200 group-hover:bg-red-200 group-hover:border-red-400 rounded relative"> + <div className="flex items-center justify-center h-full px-1 flex-row"> + <Image + src={category?.image1920 ? category?.image1920 : '/images/noimage.jpeg'} + width={56} + height={48} + alt={category?.name} + className="p-3" + /> + <h2 className="text-gray-700 group-hover:text-[#E20613] line-clamp-2 content-center h-fit w-60 px-1 font-semibold text-sm text-start"> + {category?.name} + </h2> + </div> + </div> + </div> + </Link> + </SwiperSlide> + ))} + </Swiper> + {/* {categories.length > 10 && ( + <div className="w-full flex justify-end mt-4"> + <button + onClick={handleToggleCategories} + className="flex justify-end mt-4 bg-red-500 text-white text-sm px-4 py-2 rounded" + > + {isOpenCategory ? 'Sembunyikan Semua' : 'Lihat Semua'} + </button> + </div> + )} */} + </div> + )} + </section> + ) +} + +export default CategorySection diff --git a/src/lib/product/components/LobSectionCategory.jsx b/src/lib/product/components/LobSectionCategory.jsx new file mode 100644 index 00000000..5cd467e9 --- /dev/null +++ b/src/lib/product/components/LobSectionCategory.jsx @@ -0,0 +1,80 @@ +import Image from "next/image"; +import Link from 'next/link'; +import { createSlug } from '@/core/utils/slug'; +import useDevice from '@/core/hooks/useDevice'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import { useQuery } from 'react-query'; +import { useRouter } from 'next/router'; +import { + ChevronDownIcon, + ChevronUpIcon, // Import ChevronUpIcon for toggling + DocumentCheckIcon, + HeartIcon, +} from '@heroicons/react/24/outline'; +import { useState } from 'react'; // Import useState +import { getIdFromSlug } from '@/core/utils/slug' + +const LobSectionCategory = ({ categories }) => { + const { isDesktop, isMobile } = useDevice(); + const [isOpenCategory, setIsOpenCategory] = useState(false); // State to manage category visibility + + const handleToggleCategories = () => { + setIsOpenCategory(!isOpenCategory); + }; + + const displayedCategories = categories[0]?.categoryIds; + return ( + <section> + {isDesktop && ( + <div className="group/item grid grid-flow-col gap-y-2 gap-x-4 w-full h-full"> + {displayedCategories?.map((category) => ( + <Link + href={createSlug('/shop/category/', category?.name, category?.id)} + key={category?.id} + passHref + className="block hover:scale-105 transition-transform duration-300 bg-cover bg-center h-[144px]" + style={{ + backgroundImage: `url('${category?.image ? category?.image : 'https://erp.indoteknik.com/web/image?model=x_banner.banner&id=5&field=x_banner_image&unique=09202023100557'}')`, + }} + > + </Link> + ))} + </div> + )} + + {isMobile && ( + <div className="py-4"> + <Swiper slidesPerView={1.2} spaceBetween={10}> + {displayedCategories?.map((category) => ( + <SwiperSlide key={category?.id}> + <Link + href={createSlug('/shop/category/', category?.name, category?.id)} + key={category?.id} + passHref + className="block bg-cover bg-center h-[144px]" + style={{ + backgroundImage: `url('${category?.image ? category?.image : 'https://erp.indoteknik.com/web/image?model=x_banner.banner&id=5&field=x_banner_image&unique=09202023100557'}')`, + }} + > + </Link> + </SwiperSlide> + ))} + </Swiper> + {categories.length > 10 && ( + <div className="w-full flex justify-end mt-4"> + <button + onClick={handleToggleCategories} + className="flex justify-end mt-4 bg-red-500 text-white text-sm px-4 py-2 rounded" + > + {isOpenCategory ? 'Sembunyikan Semua' : 'Lihat Semua'} + </button> + </div> + )} + </div> + )} + </section> + ) +} + +export default LobSectionCategory diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index 35e2a665..22ce0533 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -146,13 +146,22 @@ 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={URL.manufacture} className='mb-1'> - {product.manufacture.name} + <div className='flex justify-between '> + {product?.manufacture?.name ? ( + <Link href={URL.manufacture} className='mb-1 mt-1'> + {product.manufacture.name} + </Link> + ) : ( + <div>-</div> + )} + {product?.isInBu && ( + <Link href='/panduan-pick-up-service' className='group'> + <Image src='/images/PICKUP-NOW.png' className='group-hover:scale-105 transition-transform duration-200' alt='pickup now' width={90} height={12} /> </Link> - ) : ( - <div>-</div> - )} + + + )} + </div> <Link href={URL.product} className={`mb-2 !text-gray_r-12 leading-6 block line-clamp-3 h-[64px]`} @@ -292,9 +301,18 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { </div> )} {product?.manufacture?.name ? ( - <Link href={URL.manufacture} className='mb-1'> + <div className='flex justify-between'> + <Link href={URL.manufacture} className='mb-1'> {product.manufacture.name} </Link> + {/* {product?.is_in_bu && ( + <div className='bg-red-500 rounded'> + <span className='p-[6px] text-xs text-white'> + Click & Pickup + </span> + </div> + )} */} + </div> ) : ( <div>-</div> )} diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx index a8073036..73fecab5 100644 --- a/src/lib/product/components/ProductFilterDesktop.jsx +++ b/src/lib/product/components/ProductFilterDesktop.jsx @@ -22,6 +22,7 @@ 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) @@ -107,7 +108,11 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu const slug = Array.isArray(router.query.slug) ? router.query.slug[0] : router.query.slug; if (slug) { - router.push(`${prefixUrl}/${slug}?${params}`) + if(prefixUrl.includes('category') || prefixUrl.includes('lob')){ + router.push(`${prefixUrl}?${params}`) + }else{ + router.push(`${prefixUrl}/${slug}?${params}`) + } } else { router.push(`${prefixUrl}?${params}`) } diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index fb9017f4..ab55cae0 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -26,6 +26,10 @@ import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton'; import SideBanner from '~/modules/side-banner'; import FooterBanner from '~/modules/footer-banner'; +import CategorySection from './CategorySection'; +import LobSectionCategory from './LobSectionCategory'; +import { getIdFromSlug } from '@/core/utils/slug' +import { data } from 'autoprefixer'; const ProductSearch = ({ query, @@ -37,11 +41,108 @@ const ProductSearch = ({ const { page = 1 } = query; const [q, setQ] = useState(query?.q || '*'); const [search, setSearch] = useState(query?.q || '*'); - const [limit, setLimit] = useState(query?.limit || 30); + const [limit, setLimit] = useState(router.query?.limit || 30); const [orderBy, setOrderBy] = useState(router.query?.orderBy); + const [finalQuery, setFinalQuery] = useState({}); + const [queryFinal, setQueryFinal] = useState({}); + const [dataCategoriesProduct, setDataCategoriesProduct] = useState([]) + const [dataCategoriesLob, setDataCategoriesLob] = useState([]) + const categoryId = getIdFromSlug(prefixUrl) + const [data, setData] = useState([]) + const [dataLob, setDataLob] = useState([]); if (defaultBrand) query.brand = defaultBrand.toLowerCase(); + const dataIdCategories = [] + useEffect(() => { + if(prefixUrl.includes('category')){ + const loadProduct = async () => { + const getCategoriesId = await odooApi('GET', `/api/v1/category/numFound?parent_id=${categoryId}`); + if (getCategoriesId) { + setDataCategoriesProduct(getCategoriesId); + } + }; + loadProduct(); + }else if(prefixUrl.includes('lob')){ + const loadProduct = async () => { + const lobData = await odooApi('GET', `/api/v1/lob_homepage/${categoryId}/category_id`); + + if (lobData) { + setDataLob(lobData); + } + }; + loadProduct(); + + } + }, [categoryId]); + + const collectIds = (category) => { + const ids = []; + function recurse(cat) { + if (cat && cat.id) { + ids.push(cat.id); + } + if (Array.isArray(cat.children)) { + cat.children.forEach(recurse); + } + } + recurse(category); + return ids; + }; + useEffect(() => { + if(prefixUrl.includes('category')){ + const ids = collectIds(dataCategoriesProduct); + const newQuery = { + fq: `category_id_ids:(${ids.join(' OR ')})`, + page : router.query.page? router.query.page : 1, + brand : router.query.brand? router.query.brand : '', + category : router.query.category? router.query.category : '', + priceFrom : router.query.priceFrom? router.query.priceFrom : '', + priceTo : router.query.priceTo? router.query.priceTo : '', + limit : router.query.limit? router.query.limit : '', + orderBy : router.query.orderBy? router.query.orderBy : '' + }; + setFinalQuery(newQuery); + } else if (prefixUrl.includes('lob')){ + + const fetchCategoryData = async () => { + if (dataLob[0]?.categoryIds) { + + for (const cate of dataLob[0].categoryIds) { + + dataIdCategories.push(cate.childId) + } + + + const mergedArray = dataIdCategories.flat(); + + const newQuery = { + fq: `category_id_ids:(${mergedArray.join(' OR ')})`, + category : router.query.category? router.query.category : '', + page : router.query.page? router.query.page : 1, + brand : router.query.brand? router.query.brand : '', + priceFrom : router.query.priceFrom? router.query.priceFrom : '', + priceTo : router.query.priceTo? router.query.priceTo : '', + limit : router.query.limit? router.query.limit : '', + orderBy : router.query.orderBy? router.query.orderBy : '' + }; + + setFinalQuery(newQuery); + + } + }; + fetchCategoryData(); + } + }, [dataCategoriesProduct, dataLob]); + + useEffect(() => { + if (prefixUrl.includes('category') || prefixUrl.includes('lob')) { + setQueryFinal({ ...finalQuery, q, limit, orderBy }); + } else { + setQueryFinal({ ...query, q, limit, orderBy }); + } + }, [prefixUrl,dataCategoriesProduct, query, finalQuery]); + const { productSearch } = useProductSearch({ - query: { ...query, q, limit, orderBy }, + query: queryFinal, operation: 'AND', }); const [products, setProducts] = useState(null); @@ -53,22 +154,24 @@ const ProductSearch = ({ const numRows = [30, 50, 80, 100]; const [brandValues, setBrand] = useState( !router.pathname.includes('brands') - ? query.brand - ? query.brand.split(',') + ? router.query.brand + ? router.query.brand.split(',') : [] : [] ); const [categoryValues, setCategory] = useState( - query?.category?.split(',') || [] + router.query?.category?.split(',') || router.query?.category?.split(',') ); - const [priceFrom, setPriceFrom] = useState(query?.priceFrom || null); - const [priceTo, setPriceTo] = useState(query?.priceTo || null); + + const [priceFrom, setPriceFrom] = useState(router.query?.priceFrom || null); + const [priceTo, setPriceTo] = useState(router.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; - + const [dataCategories, setDataCategories] = useState([]) + useEffect(() => { if (productFound == 0 && query.q && !spellings) { searchSpellApi({ query: query.q }).then((response) => { @@ -98,7 +201,7 @@ const ProductSearch = ({ }); } }, [productFound, query, spellings]); - + let id = [] useEffect(() => { const checkIfBrand = async () => { const brand = await axios( @@ -115,6 +218,21 @@ const ProductSearch = ({ checkIfBrand(); } }, [q]); + + useEffect(() => { + if(prefixUrl.includes('category')){ + const loadCategories = async () => { + const getCategories = await odooApi('GET', `/api/v1/category/child?parent_id=${categoryId}`) + if(getCategories){ + setDataCategories(getCategories) + } + } + loadCategories() + } + }, []) + + + const brands = []; for ( @@ -224,7 +342,7 @@ const ProductSearch = ({ q: router.query.q, orderBy: orderBy, brand: brandValues.join(','), - category: categoryValues.join(','), + category: categoryValues?.join(','), priceFrom, priceTo, }; @@ -263,7 +381,6 @@ const ProductSearch = ({ }; const isNotReadyStockPage = router.asPath !== '/shop/search?orderBy=stock'; - return ( <> <MobileView> @@ -324,6 +441,8 @@ const ProductSearch = ({ SpellingComponent )} </div> + <LobSectionCategory categories={dataLob}/> + <CategorySection categories={dataCategories}/> {productFound > 0 && ( <div className='flex items-center gap-x-2 mb-5 justify-between'> @@ -364,6 +483,7 @@ const ProductSearch = ({ pageCount={pageCount} currentPage={parseInt(page)} url={`${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} + // url={prefixUrl.includes('category') || prefixUrl.includes('lob')? `${prefixUrl}?${toQuery(_.omit(finalQuery, ['page']))}` : `${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} className='mt-6 mb-2' /> @@ -412,7 +532,10 @@ const ProductSearch = ({ <SideBanner /> </div> + <div className='w-9/12 pl-6'> + <LobSectionCategory categories={dataLob}/> + <CategorySection categories={dataCategories}/> {bannerPromotionHeader && bannerPromotionHeader?.image && ( <div className='mb-3'> <Image @@ -550,6 +673,7 @@ const ProductSearch = ({ pageCount={pageCount} currentPage={parseInt(page)} url={`${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} + // url={prefixUrl.includes('category') || prefixUrl.includes('lob')? `${prefixUrl}?${toQuery(_.omit(finalQuery, ['page']))}` : `${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} className='!justify-end' /> </div> diff --git a/src/lib/promo/api/productSearchApi.js b/src/lib/promo/api/productSearchApi.js new file mode 100644 index 00000000..2f792fd4 --- /dev/null +++ b/src/lib/promo/api/productSearchApi.js @@ -0,0 +1,11 @@ +import _ from 'lodash-contrib' +import axios from 'axios' + +const productSearchApi = async ({ query, operation = 'AND' }) => { + const dataProductSearch = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/promo?${query}&operation=${operation}` + ) + return dataProductSearch.data +} + +export default productSearchApi diff --git a/src/lib/promo/hooks/usePromotionSearch.js b/src/lib/promo/hooks/usePromotionSearch.js new file mode 100644 index 00000000..1a194646 --- /dev/null +++ b/src/lib/promo/hooks/usePromotionSearch.js @@ -0,0 +1,15 @@ +import { useQuery } from 'react-query' +import productSearchApi from '../api/productSearchApi' +import _ from 'lodash-contrib' + +const usePromotionSearch = ({ query, operation }) => { + const queryString = _.toQuery(query) + const fetchProductSearch = async () => await productSearchApi({ query: queryString , operation : operation}) + const productSearch = useQuery(`promoSearch-${queryString}`, fetchProductSearch) + + return { + productSearch + } +} + +export default usePromotionSearch diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx index df234dc2..0ad042de 100644 --- a/src/lib/quotation/components/Quotation.jsx +++ b/src/lib/quotation/components/Quotation.jsx @@ -9,6 +9,7 @@ import _ from 'lodash'; import { deleteItemCart, getCart, getItemCart } from '@/core/utils/cart'; import currencyFormat from '@/core/utils/currencyFormat'; import { toast } from 'react-hot-toast'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; // import checkoutApi from '@/lib/checkout/api/checkoutApi' import { useRouter } from 'next/router'; import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'; @@ -38,11 +39,12 @@ const { getProductsCheckout } = require('@/lib/checkout/api/checkoutApi'); const Quotation = () => { const router = useRouter(); const auth = useAuth(); - + const { data: cartCheckout } = useQuery('cartCheckout', () => getProductsCheckout() - ); +); +const { setRefreshCart } = useProductCartContext(); const SELF_PICKUP_ID = 32; const [products, setProducts] = useState(null); @@ -293,6 +295,7 @@ const Quotation = () => { if (isSuccess?.id) { for (const product of products) deleteItemCart({ productId: product.id }); router.push(`/shop/quotation/finish?id=${isSuccess.id}`); + setRefreshCart(true); return; } diff --git a/src/lib/quotation/components/Quotationheader.jsx b/src/lib/quotation/components/Quotationheader.jsx new file mode 100644 index 00000000..4529c977 --- /dev/null +++ b/src/lib/quotation/components/Quotationheader.jsx @@ -0,0 +1,265 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { createSlug } from '@/core/utils/slug'; +import useAuth from '@/core/hooks/useAuth'; +import { useRouter } from 'next/router'; +import odooApi from '@/core/api/odooApi'; +import { useProductCartContext } from '@/contexts/ProductCartContext'; +import Image from '@/core/components/elements/Image/Image'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import { AnimatePresence, motion } from 'framer-motion'; +import style from '../../../../src-migrate/modules/cart/styles/item-promo.module.css'; +import useTransactions from '../../transaction/hooks/useTransactions'; +import currencyFormat from '@/core/utils/currencyFormat'; +const { DocumentCheckIcon, PhotoIcon } = require('@heroicons/react/24/outline'); +const { default: Link } = require('next/link'); + +const Quotationheader = (quotationCount) => { + const auth = useAuth(); + const query = { + context: 'quotation', + site: auth?.webRole === null && auth?.site ? auth.site : null, + }; + + const router = useRouter(); + const [subTotal, setSubTotal] = useState(null); + const [buttonLoading, SetButtonTerapkan] = useState(false); + const itemLoading = [1, 2, 3]; + const [countQuotation, setCountQuotation] = useState(null); + const { productCart, setProductCart, refreshCart, setRefreshCart, isLoading, setIsloading, productQuotation, setProductQuotation } = + useProductCartContext(); + + const [isHovered, setIsHovered] = useState(false); + const [isTop, setIsTop] = useState(true); + + const qotation = useMemo(() => { + return productQuotation || []; + }, [productQuotation]); + + const handleMouseEnter = () => { + setIsHovered(true); + getCart(); + }; + + const handleMouseLeave = () => { + setIsHovered(false); + }; + + const getCart = () => { + if (!productQuotation && auth) { + refreshCartf(); + } + }; + let { transactions } = useTransactions({ query }); + + const refreshCartf = useCallback(async () => { + setIsloading(true); + let pendingTransactions = transactions?.data?.saleOrders.filter(transaction => transaction.status === 'draft'); + setProductQuotation(pendingTransactions); + setCountQuotation(pendingTransactions?.length ? pendingTransactions?.length : pendingTransactions?.length); + setIsloading(false); + }, [setProductQuotation, setIsloading]); + + useEffect(() => { + if (!qotation) return + + let calculateTotalDiscountAmount = 0 + for (const product of qotation) { + // if (qotation.quantity == '') continue + calculateTotalDiscountAmount += product.amountUntaxed + } + let subTotal = calculateTotalDiscountAmount + setSubTotal(subTotal) + }, [qotation]) + + useEffect(() => { + if (refreshCart) { + refreshCartf(); + } + setRefreshCart(false); + }, [ refreshCartf, setRefreshCart]); + + useEffect(() => { + setCountQuotation(quotationCount.quotationCount); + setProductQuotation(quotationCount.data); + }, [quotationCount]); + + useEffect(() => { + const handleScroll = () => { + setIsTop(window.scrollY === 0); + }; + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + const handleCheckout = async () => { + SetButtonTerapkan(true); + let checkoutAll = await odooApi('POST', `/api/v1/user/${auth.id}/cart/select-all`); + router.push('/my/quotations'); + }; + + return ( + <div className='relative group'> + <div> + <Link + href='/my/quotations' + target='_blank' + rel='noreferrer' + className='flex items-center gap-x-2 !text-gray_r-12/80' + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + <div className={`relative ${countQuotation > 0 && 'mr-2'}`}> + <DocumentCheckIcon className='w-7' /> + {countQuotation > 0 && ( + <span className='absolute -top-2 -right-2 badge-solid-red rounded-full w-5 h-5 flex items-center justify-center'> + {countQuotation} + </span> + )} + </div> + <span> + List + <br /> + Quotation + </span> + </Link> + </div> + <AnimatePresence> + {isHovered && ( + <> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1, top: isTop ? 230 : 155 }} + exit={{ opacity: 0 }} + transition={{ duration: 0.15, top: { duration: 0.3 } }} + className={`fixed 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'>Daftar Quotation</h5> + </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 Quotation Anda + </p> + </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 && qotation.length === 0 && !isLoading && ( + <div className='justify-center p-4'> + <p className='text-gray-500 text-center '> + Tidak Ada Quotation + </p> + </div> + )} + {auth && qotation.length > 0 && !isLoading && ( + <> + <ul role='list' className='divide-y divide-gray-200 dark:divide-gray-700'> + {qotation && + qotation?.map((product, index) => ( + <> + <li className='py-1 sm:py-2'> + <div className='flex justify-between border p-2 flex-col gap-y-2 hover:border-red-500'> + <Link + href={`/my/quotations/${product?.id}`} + className='hover:border-red-500' + > + <div className='flex justify-between mb-2'> + <div className='flex flex-row items-center'> + <p className='tanggal text-xs opacity-80 mr-[2px]'>Sales : </p> + <p className='tanggal text-xs text-red-500 font-semibold'>{product.sales}</p> + </div> + <div className='flex flex-row items-center'> + <p className='text-xs opacity-80 mr-[2px]'>Status :</p> + <p className='badge-red h-fit text-xs whitespace-nowrap'>Pending Quotation</p> + </div> + </div> + <div className='flex justify-between mb-2'> + <div className='flex flex-col items-start'> + <p className=' text-xs opacity-80 mr-[2px]'>No. Transaksi</p> + <p className=' text-sm text-red-500 font-semibold'> {product.name}</p> + </div> + <div className='flex flex-col items-end'> + <p className='text-xs opacity-80 mr-[2px]'>No. Purchase Order</p> + <p className='font-semibold text-sm text-red-500'> {product.purchaseOrderName ? product.purchaseOrderName : '-'}</p> + </div> + </div> + {/* <div className='my-0.5 h-0.5 bg-gray-200'></div> */} + <hr className='mt-3 mb-3 border border-gray-100' /> + <div className='bagian bawah flex justify-between mt-2'> + <p className='font-semibold text-sm'>Total</p> + <p className='font-semibold text-sm'>{currencyFormat(product.amountUntaxed)}</p> + </div> + </Link> + </div> + </li> + </> + ))} + </ul> + <hr /> + </> + )} + </div> + {auth && qotation.length > 0 && !isLoading && ( + <> + <div className='mt-3 ml-1'> + <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...' : 'Lihat Semua'} + </button> + </div> + </> + )} + </motion.div> + </motion.div> + </> + )} + </AnimatePresence> + </div> + ); +}; + +export default Quotationheader; diff --git a/src/lib/tracking-order/api/trackingOrder.js b/src/lib/tracking-order/api/trackingOrder.js new file mode 100644 index 00000000..cc48c40c --- /dev/null +++ b/src/lib/tracking-order/api/trackingOrder.js @@ -0,0 +1,8 @@ +import odooApi from "@/core/api/odooApi"; + +export const trackingOrder = async ({query}) => { + const params = new URLSearchParams(query).toString(); + const list = await odooApi('GET', `/api/v1/tracking_order?${params}`) + + return list; +} diff --git a/src/lib/tracking-order/component/TrackingOrder.jsx b/src/lib/tracking-order/component/TrackingOrder.jsx new file mode 100644 index 00000000..394979c1 --- /dev/null +++ b/src/lib/tracking-order/component/TrackingOrder.jsx @@ -0,0 +1,139 @@ +import { yupResolver } from '@hookform/resolvers/yup' +import React, { useEffect, useState } from 'react' +import { useForm } from 'react-hook-form' +import * as Yup from 'yup' +import Manifest from '@/lib/treckingAwb/component/Manifest' +import { trackingOrder } from '../api/trackingOrder' +import { useQuery } from 'react-query' +import { Spinner } from '@chakra-ui/react'; +import { Search } from 'lucide-react'; +import whatsappUrl from '@/core/utils/whatsappUrl'; +import Link from 'next/link' + +const TrackingOrder = () => { + const [idAWB, setIdAWB] = useState(null) + const [inputQuery, setInputQuery] = useState(null) + const [buttonClick, setButtonClick] = useState(false) + const [apiError, setApiError] = useState(null) // State to store API error message + + const closePopup = () => { + setIdAWB(null) + setButtonClick(false) + setInputQuery(null) + setApiError(null) // Reset error message on close + } + + const { + register, + handleSubmit, + formState: { errors }, + control, + reset + } = useForm({ + resolver: yupResolver(validationSchema), + defaultValues + }) + + const query = { + email: inputQuery?.email, + so: inputQuery?.id + } + + const { data: tracking, isLoading, isError, error } = useQuery( + ['tracking', query], + () => trackingOrder({ query: query }), + { + enabled: !!query.email && !!query.so, + onSuccess: (data) => { + if (buttonClick) { + if (data?.code === 403 || data?.code === 400 || data?.code === 404) { + setApiError(data?.description); + } else if (data?.pickings?.length > 0) { + setIdAWB(data.pickings[0]?.id); + } else { + setApiError('No pickings data available'); + } + setButtonClick(false); + setInputQuery(null); + } + }, + } + ); + + const onSubmitHandler = async (values) => { + setInputQuery(values) + setButtonClick(true) + } + + return ( + <div className='container mx-auto flex py-10 flex-col'> + <h1 className='text-h-sm md:text-title-sm font-semibold mb-6'>Tracking Order</h1> + <div className='flex justify-start items-start'> + <span className='text-base w-full'> + {`Untuk melacak pesanan Anda, masukkan Nomor Transaksi di kotak bawah ini dan masukkan Email login anda lalu tekan tombol "Lacak". Nomor Transaksi ini dapat Anda lihat dalam menu `} + <Link href='/my/transactions' className='text-red-500'> + Daftar Transaksi + </Link> + {`. Jika mengalami kesulitan `} + <Link href='https://wa.me/6281717181922' target='_blank' rel='noreferrer' className='text-red-500'> + hubungi kami + </Link> + {`.`} + </span> + </div> + <div> + <form onSubmit={handleSubmit(onSubmitHandler)} className='flex mt-4 flex-row w-full '> + <div className='w-[90%] grid grid-cols-2 gap-4'> + <div className='flex flex-col '> + <label className='form-label mb-2'>ID Pesanan*</label> + <input + {...register('id')} + placeholder='dapat dilihat pada email konfirmasi anda' + type='text' + className='form-input mb-2' + aria-invalid={errors.id?.message} + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.id?.message}</div> + </div> + <div className='flex flex-col '> + <label className='form-label mb-2'>Email Penagihan*</label> + <input + {...register('email')} + placeholder='Email yang anda gunakan saat pembayaran' + type='text' + className='form-input' + aria-invalid={errors.email?.message} + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.email?.message}</div> + </div> + </div> + <div className={` ${errors.id?.message ? 'mt-2' : 'mt-5'} flex items-center ml-4`}> + <button + type='submit' + className='bg-red-600 border border-red-600 rounded-md text-sm text-white w-24 h-11 mb-1 content-center flex flex-row justify-center items-center' + > + {isLoading && <Spinner size='xs' className='mr-2'/>} + {!isLoading && <Search size={16} strokeWidth={1} className='mr-2'/>} + <p>Lacak</p> + </button> + </div> + </form> + {/* Display the API error message */} + {apiError && <div className='text-danger-500 mt-4'>{apiError}</div>} + <Manifest idAWB={idAWB} closePopup={closePopup} /> + </div> + </div> + ) +} + +const validationSchema = Yup.object().shape({ + email: Yup.string().email('Format harus seperti contoh@email.com').required('Harus di-isi'), + id: Yup.string().required('Harus di-isi'), +}) + +const defaultValues = { + email: '', + id: '' +} + +export default TrackingOrder diff --git a/src/lib/treckingAwb/component/Manifest.jsx b/src/lib/treckingAwb/component/Manifest.jsx index cf2fa4ed..fbc95702 100644 --- a/src/lib/treckingAwb/component/Manifest.jsx +++ b/src/lib/treckingAwb/component/Manifest.jsx @@ -40,10 +40,18 @@ const Manifest = ({ idAWB, closePopup }) => { const getManifest = async () => { setIsLoading(true) const auth = getAuth() - const list = await odooApi( - 'GET', - `/api/v1/partner/${auth.partnerId}/stock-picking/${idAWB}/tracking` - ) + let list + if(auth){ + list = await odooApi( + 'GET', + `/api/v1/partner/${auth.partnerId}/stock-picking/${idAWB}/tracking` + ) + }else{ + list = await odooApi( + 'GET', + `/api/v1/stock-picking/${idAWB}/tracking` + ) + } setManifests(list) setIsLoading(false) } diff --git a/src/pages/_document.jsx b/src/pages/_document.jsx index cd60bd89..6af6294f 100644 --- a/src/pages/_document.jsx +++ b/src/pages/_document.jsx @@ -41,13 +41,13 @@ export default function MyDocument() { content='328wmjs7hcnz74rwsqzxvq50rmbtm2' /> - <Script + {/* <Script async strategy='beforeInteractive' src='https://www.googletagmanager.com/gtag/js?id=UA-10501937-1' - /> + /> */} - <Script + {/* <Script async id='google-analytics-ua' strategy='beforeInteractive' @@ -59,7 +59,7 @@ export default function MyDocument() { gtag('config', 'UA-10501937-1'); `, }} - /> + /> */} <Script async diff --git a/src/pages/api/activation-request.js b/src/pages/api/activation-request.js index 61dbb597..98d27f78 100644 --- a/src/pages/api/activation-request.js +++ b/src/pages/api/activation-request.js @@ -7,7 +7,7 @@ export default async function handler(req, res) { let result = await odooApi('POST', '/api/v1/user/activation-request', { email }) if (result.activationRequest) { mailer.sendMail({ - from: 'sales@indoteknik.com', + from: 'Indoteknik.com <noreply@indoteknik.com>', to: result.user.email, subject: 'Permintaan Aktivasi Akun Indoteknik', html: ` diff --git a/src/pages/api/shop/product-homepage.js b/src/pages/api/shop/product-homepage.js index 02c01ee0..61732c77 100644 --- a/src/pages/api/shop/product-homepage.js +++ b/src/pages/api/shop/product-homepage.js @@ -36,7 +36,8 @@ const respoonseMap = (productHomepage, products) => { name: productHomepage.name_s, image: productHomepage.image_s, url: productHomepage.url_s, - products: products + products: products, + categoryIds: productHomepage.category_id_ids, } return productMapped diff --git a/src/pages/api/shop/promo.js b/src/pages/api/shop/promo.js new file mode 100644 index 00000000..221a9adb --- /dev/null +++ b/src/pages/api/shop/promo.js @@ -0,0 +1,202 @@ +import { productMappingSolr, promoMappingSolr } from '@/utils/solrMapping'; +import axios from 'axios'; +import camelcaseObjectDeep from 'camelcase-object-deep'; + +export default async function handler(req, res) { + const { + q = '*', + page = 1, + brand = '', + category = '', + priceFrom = 0, + priceTo = 0, + orderBy = 'if(exists(sequence_i),0,1) asc, sequence_i asc,', + operation = 'AND', + fq = '', + limit = 30, + } = req.query; + + let { stock = '' } = req.query; + + let paramOrderBy = 'if(exists(sequence_i),0,1) asc, sequence_i asc,'; + switch (orderBy) { + case 'price-asc': + paramOrderBy += 'price_tier1_v2_f ASC'; + break; + case 'price-desc': + paramOrderBy += 'price_tier1_v2_f DESC'; + break; + case 'popular': + paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,'; + break; + case 'popular-weekly': + paramOrderBy += 'search_rank_weekly_i DESC'; + break; + case 'stock': + paramOrderBy += 'product_rating_f DESC, stock_total_f DESC'; + break; + case 'flashsale-price-asc': + paramOrderBy += 'flashsale_price_f ASC'; + break; + default: + paramOrderBy += ''; + break; + } + + let checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/); + let newQ = checkQ.length > 1 ? escapeSolrQuery(q) + '*' : escapeSolrQuery(q); + + let offset = (page - 1) * limit; + let parameter = [ + 'facet.field=manufacture_name_s', + 'facet.field=category_name', + 'facet=true', + 'indent=true', + // `facet.query=${escapeSolrQuery(q)}`, + `q.op=${operation}`, + `q=${q}`, + // 'qf=name_s', + `start=${parseInt(offset)}`, + `rows=${limit}`, + `sort=${paramOrderBy}`, + `fq=product_ids:[* TO *]`, + ]; + + if (priceFrom > 0 || priceTo > 0) { + parameter.push( + `fq=price_tier1_v2_f:[${priceFrom == '' ? '*' : priceFrom} TO ${ + priceTo == '' ? '*' : priceTo + }]` + ); + } + + let { auth } = req.cookies; + if (auth) { + auth = JSON.parse(auth); + if (auth.feature.onlyReadyStock) stock = true; + } + + if (brand) + parameter.push( + `fq=${brand + .split(',') + .map( + (manufacturer) => + `manufacture_name_s:"${encodeURIComponent(manufacturer)}"` + ) + .join(' OR ')}` + ); + if (category) + parameter.push( + `fq=${category + .split(',') + .map((cat) => `category_name:"${encodeURIComponent(cat)}"`) + .join(' OR ')}` + ); + // if (category) parameter.push(`fq=category_name:${capitalizeFirstLetter(category.replace(/,/g, ' OR '))}`) + if (stock) parameter.push(`fq=stock_total_f:{1 TO *}`); + + // Single fq in url params + if (typeof fq === 'string') parameter.push(`fq=${fq}`); + // Multi fq in url params + if (Array.isArray(fq)) + parameter = parameter.concat(fq.map((val) => `fq=${val}`)); + + let result = await axios( + process.env.SOLR_HOST + '/solr/promotion_program_lines/select?' + parameter.join('&') + ); + try { + result.data.response.products = promoMappingSolr( + result.data.response.docs + ); + + result.data.responseHeader.params.start = parseInt( + result.data.responseHeader.params.start + ); + result.data.responseHeader.params.rows = parseInt( + result.data.responseHeader.params.rows + ); + delete result.data.response.docs; + // result.data = camelcaseObjectDeep(result.data); + result.data = result.data; + res.status(200).json(result.data); + } catch (error) { + res.status(400).json({ error: error.message }); + } +} + +const escapeSolrQuery = (query) => { + if (query == '*') return query; + + query = query.replace(/-/g, ' '); + + const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g; + const words = query.split(/\s+/); + const escapedWords = words.map((word) => { + if (specialChars.test(word)) { + return word.replace(specialChars, '\\$1'); + } + return word; + }); + + return escapedWords.join(' '); +}; + + +/*const productResponseMap = (products, pricelist) => { + return products.map((product) => { + let price = product.price_tier1_v2_f || 0 + let priceDiscount = product.price_discount_f || 0 + let discountPercentage = product.discount_f || 0 + + if (pricelist) { + // const pricelistDiscount = product?.[`price_${pricelist}_f`] || false + // const pricelistDiscountPerc = product?.[`discount_${pricelist}_f`] || false + + // if (pricelistDiscount && pricelistDiscount > 0) priceDiscount = pricelistDiscount + // if (pricelistDiscountPerc && pricelistDiscountPerc > 0) + // discountPercentage = pricelistDiscountPerc + + price = product?.[`price_${pricelist}_f`] || 0 + } + + if (product?.flashsale_id_i > 0) { + price = product?.flashsale_base_price_f || 0 + priceDiscount = product?.flashsale_price_f || 0 + discountPercentage = product?.flashsale_discount_f || 0 + } + + let productMapped = { + id: product.product_id_i || '', + image: product.image_s || '', + code: product.default_code_s || '', + name: product.name_s || '', + lowestPrice: { price, priceDiscount, discountPercentage }, + variantTotal: product.variant_total_i || 0, + stockTotal: product.stock_total_f || 0, + weight: product.weight_f || 0, + manufacture: {}, + categories: [], + flashSale: { + id: product?.flashsale_id_i, + name: product?.product?.flashsale_name_s, + tag : product?.flashsale_tag_s || 'FLASH SALE' + } + } + + if (product.manufacture_id_i && product.manufacture_name_s) { + productMapped.manufacture = { + id: product.manufacture_id_i || '', + name: product.manufacture_name_s || '' + } + } + + productMapped.categories = [ + { + id: product.category_id_i || '', + name: product.category_name_s || '' + } + ] + return productMapped + }) +}*/ diff --git a/src/pages/google_merchant/products/[page].js b/src/pages/google_merchant/products/[page].js index 6e0eb703..0c2cf3c5 100644 --- a/src/pages/google_merchant/products/[page].js +++ b/src/pages/google_merchant/products/[page].js @@ -50,7 +50,7 @@ export async function getServerSideProps({ res, query }) { let categoryId = null; if (brandId && brandId in brandsData) { - categoryId = brandsData[brandId].category_ids?.[0] ?? null; + categoryId = brandsData[brandId]?.category_ids?.[0] ?? null; } else { const solrBrand = await getBrandById(brandId); brandsData[brandId] = solrBrand; @@ -58,7 +58,7 @@ export async function getServerSideProps({ res, query }) { } if (categoryId && categoryId in categoriesData) { - categoryName = categoriesData[categoryId].name_s ?? null; + categoryName = categoriesData[categoryId]?.name_s ?? null; } else { const solrCategory = await getCategoryById(categoryId); categoriesData[categoryId] = solrCategory; diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 4493fe31..613950a6 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -1,6 +1,5 @@ import dynamic from 'next/dynamic'; -import { useRef } from 'react'; - +import { useEffect, useRef, useState } from 'react'; import { HeroBannerSkeleton } from '@/components/skeleton/BannerSkeleton'; import { PopularProductSkeleton } from '@/components/skeleton/PopularProductSkeleton'; import Seo from '@/core/components/Seo'; @@ -12,8 +11,11 @@ import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBran import BannerPromoSkeleton from '@/lib/home/components/Skeleton/BannerPromoSkeleton'; import PromotinProgram from '@/lib/promotinProgram/components/HomePage'; import PagePopupIformation from '~/modules/popup-information'; -import useProductDetail from '~/modules/product-detail/stores/useProductDetail'; +import CategoryPilihan from '../lib/home/components/CategoryPilihan'; +import odooApi from '@/core/api/odooApi'; import { getAuth } from '~/libs/auth'; +// import { getAuth } from '~/libs/auth'; +import useProductDetail from '~/modules/product-detail/stores/useProductDetail'; const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout') @@ -58,12 +60,23 @@ const BannerSection = dynamic(() => const CategoryHomeId = dynamic(() => import('@/lib/home/components/CategoryHomeId') ); + +const CategoryDynamic = dynamic(() => + import('@/lib/home/components/CategoryDynamic') +); + +const CategoryDynamicMobile = dynamic(() => +import('@/lib/home/components/CategoryDynamicMobile') +); + const CustomerReviews = dynamic(() => import('@/lib/review/components/CustomerReviews') ); const ServiceList = dynamic(() => import('@/lib/home/components/ServiceList')); -export default function Home() { + + +export default function Home({categoryId}) { const bannerRef = useRef(null); const wrapperRef = useRef(null); @@ -74,7 +87,19 @@ export default function Home() { bannerRef.current?.querySelector(':first-child')?.clientHeight + 'px'; }; + useEffect(() => { + const loadCategories = async () => { + const getCategories = await odooApi('GET', '/api/v1/category/child?partner_id='+{categoryId}) + if(getCategories){ + setDataCategories(getCategories) + } + } + loadCategories() + }, []) + + const [dataCategories, setDataCategories] = useState([]) return ( + <> <BasicLayout> <Seo title='Indoteknik.com: B2B Industrial Supply & Solution' @@ -82,11 +107,9 @@ export default function Home() { additionalMetaTags={[ { name: 'keywords', - content: - 'indoteknik, indoteknik.com, toko teknik, toko perkakas, jual genset, jual fogging, jual krisbow, harga krisbow, harga alat safety, harga pompa air', + content: 'indoteknik, indoteknik.com, toko teknik, toko perkakas, jual genset, jual fogging, jual krisbow, harga krisbow, harga alat safety, harga pompa air', }, - ]} - /> + ]} /> <PagePopupIformation /> @@ -126,18 +149,21 @@ export default function Home() { </> )} <PromotinProgram /> + {dataCategories &&( + <CategoryPilihan categories={dataCategories} /> + )} + <CategoryDynamic /> <CategoryHomeId /> <BannerSection /> <CustomerReviews /> </div> </div> - </DesktopView> - - <MobileView> + </DesktopView> + <MobileView> <DelayRender renderAfter={200}> <HeroBanner /> </DelayRender> - <div className='flex flex-col gap-y-12 my-6'> + <div className='flex flex-col gap-y-4 my-6'> <DelayRender renderAfter={400}> <ServiceList /> </DelayRender> @@ -159,6 +185,12 @@ export default function Home() { <DelayRender renderAfter={600}> <PromotinProgram /> </DelayRender> + <DelayRender renderAfter={600}> + {dataCategories &&( + <CategoryPilihan categories={dataCategories} /> + )} + <CategoryDynamicMobile /> + </DelayRender> <DelayRender renderAfter={800}> <PopularProduct /> </DelayRender> @@ -172,5 +204,6 @@ export default function Home() { </div> </MobileView> </BasicLayout> + </> ); }
\ No newline at end of file diff --git a/src/pages/shop/brands/[slug].jsx b/src/pages/shop/brands/[slug].jsx index e786ef78..88e9b302 100644 --- a/src/pages/shop/brands/[slug].jsx +++ b/src/pages/shop/brands/[slug].jsx @@ -18,7 +18,7 @@ export default function BrandDetail() { const brandName = getNameFromSlug(slug) const id = getIdFromSlug(slug) const {brand} = useBrand({id}) - if (!brand || !brand.data || _.isEmpty(brand.data)) { + if ( !brand.isLoading && _.isEmpty(brand.data)) { return <PageNotFound />; } return ( diff --git a/src/pages/shop/category/[slug].jsx b/src/pages/shop/category/[slug].jsx index 1afe30bf..11840d47 100644 --- a/src/pages/shop/category/[slug].jsx +++ b/src/pages/shop/category/[slug].jsx @@ -5,6 +5,8 @@ import { useRouter } from 'next/router'; import Seo from '@/core/components/Seo'; import { getIdFromSlug, getNameFromSlug } from '@/core/utils/slug'; import Breadcrumb from '@/lib/category/components/Breadcrumb'; +import { useEffect, useState } from 'react'; +import odooApi from '@/core/api/odooApi'; const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout') @@ -12,10 +14,14 @@ const BasicLayout = dynamic(() => const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch') ); +const CategorySection = dynamic(() => + import('@/lib/product/components/CategorySection') +) export default function CategoryDetail() { const router = useRouter(); const { slug = '', page = 1 } = router.query; + const [dataCategories, setDataCategories] = useState([]) const categoryName = getNameFromSlug(slug); const categoryId = getIdFromSlug(slug); @@ -43,8 +49,9 @@ export default function CategoryDetail() { <Breadcrumb categoryId={categoryId} /> + {!_.isEmpty(router.query) && ( - <ProductSearch query={query} prefixUrl={`/shop/category/${slug}`} /> + <ProductSearch query={query} categories ={categoryId} prefixUrl={`/shop/category/${slug}`} /> )} </BasicLayout> ); diff --git a/src/pages/shop/lob/[slug].jsx b/src/pages/shop/lob/[slug].jsx new file mode 100644 index 00000000..d939c25c --- /dev/null +++ b/src/pages/shop/lob/[slug].jsx @@ -0,0 +1,48 @@ +import _ from 'lodash'; +import dynamic from 'next/dynamic'; +import { useRouter } from 'next/router'; +import Seo from '@/core/components/Seo'; +import { getIdFromSlug, getNameFromSlug } from '@/core/utils/slug'; +import Breadcrumb from '../../../lib/lob/components/Breadcrumb'; +import { useEffect, useState } from 'react'; +import odooApi from '@/core/api/odooApi'; +import { div } from 'lodash-contrib'; + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')); +const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch')); +const CategorySection = dynamic(() => import('@/lib/product/components/CategorySection')); + +export default function CategoryDetail() { + const router = useRouter(); + const { slug = '', page = 1 } = router.query; + const [dataLob, setDataLob] = useState([]); + const [finalQuery, setFinalQuery] = useState({}); + const [dataCategoriesProduct, setDataCategoriesProduct] = useState([]) + const [data, setData] = useState([]) + const dataIdCategories = [] + + const categoryName = getNameFromSlug(slug); + const lobId = getIdFromSlug(slug); + const q = router?.query.q || null; + + return ( + <BasicLayout> + <Seo + title={`Beli ${categoryName} di Indoteknik`} + description={`Jual ${categoryName} Kirim Jakarta Surabaya Semarang Makassar Manado Denpasar Balikpapan Medan Palembang Lampung Bali Bandung Makassar Manado.`} + additionalMetaTags={[ + { + property: 'keywords', + content: `Jual ${categoryName}, harga ${categoryName}, ${categoryName} murah, toko ${categoryName}, ${categoryName} jakarta, ${categoryName} surabaya`, + }, + ]} + /> + + <Breadcrumb categoryId={getIdFromSlug(slug)} /> + + {!_.isEmpty(router.query) && ( + <ProductSearch query={router.query} categories={getIdFromSlug(slug)} prefixUrl={`/shop/lob/${slug}`} /> + )} + </BasicLayout> + ); +} diff --git a/src/pages/shop/product/variant/[slug].jsx b/src/pages/shop/product/variant/[slug].jsx index cb335e0a..42f38774 100644 --- a/src/pages/shop/product/variant/[slug].jsx +++ b/src/pages/shop/product/variant/[slug].jsx @@ -69,6 +69,8 @@ export default function ProductDetail({ product }) { <Seo title={product?.name || '' + ' - Indoteknik.com' || ''} description='Temukan pilihan produk B2B Industri & Alat Teknik untuk Perusahaan, UMKM & Pemerintah dengan lengkap, mudah dan transparan.' + noindex={true} + nofollow={true} openGraph={{ url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath, images: [ diff --git a/src/pages/shop/promo/[slug].jsx b/src/pages/shop/promo/[slug].jsx new file mode 100644 index 00000000..cfb2c841 --- /dev/null +++ b/src/pages/shop/promo/[slug].jsx @@ -0,0 +1,394 @@ +import dynamic from 'next/dynamic' +import NextImage from 'next/image'; +import { useEffect, useState } from 'react' +import { useRouter } from 'next/router' +import Seo from '../../../core/components/Seo.jsx' +import Promocrumb from '../../../lib/promo/components/Promocrumb.jsx' +import LogoSpinner from '../../../core/components/elements/Spinner/LogoSpinner.jsx' +import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card.tsx' +import React from 'react' +import DesktopView from '../../../core/components/views/DesktopView.jsx'; +import MobileView from '../../../core/components/views/MobileView.jsx'; +import 'swiper/swiper-bundle.css'; +import useDevice from '../../../core/hooks/useDevice.js' +import ProductFilterDesktop from '../../../lib/product/components/ProductFilterDesktopPromotion.jsx'; +import ProductFilter from '../../../lib/product/components/ProductFilter.jsx'; +import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react'; +import { formatCurrency } from '../../../core/utils/formatValue.js'; +import Pagination from '../../../core/components/elements/Pagination/Pagination.js'; +import whatsappUrl from '../../../core/utils/whatsappUrl.js'; +import _ from 'lodash'; +import useActive from '../../../core/hooks/useActive.js'; +import useProductSearch from '../../../lib/promo/hooks/usePromotionSearch.js'; + +const BasicLayout = dynamic(() => import('../../../core/components/layouts/BasicLayout.jsx')) + +export default function PromoDetail() { + const router = useRouter() + const { slug = '', brand ='', category='', page = '1' } = router.query + const [currentPage, setCurrentPage] = useState(parseInt(10) || 1); + const [orderBy, setOrderBy] = useState(router.query?.orderBy); + const popup = useActive(); + const prefixUrl = `/shop/promo/${slug}` + const [queryFinal, setQueryFinal] = useState({}); + const [limit, setLimit] = useState(30); + const [q, setQ] = useState('*'); + const [finalQuery, setFinalQuery] = useState({}); + const [products, setProducts] = useState(null); + const [brandValues, setBrand] = useState( + !router.pathname.includes('brands') + ? router.query.brand + ? router.query.brand.split(',') + : [] + : [] + ); + + const [categoryValues, setCategory] = useState( + router.query?.category?.split(',') || router.query?.category?.split(',') + ); + + const [priceFrom, setPriceFrom] = useState(router.query?.priceFrom || null); + const [priceTo, setPriceTo] = useState(router.query?.priceTo || null); + + + useEffect(() => { + const newQuery = { + fq: `type_value_s:${slug}`, + page : router.query.page? router.query.page : 1, + brand : router.query.brand? router.query.brand : '', + category : router.query.category? router.query.category : '', + priceFrom : router.query.priceFrom? router.query.priceFrom : '', + priceTo : router.query.priceTo? router.query.priceTo : '', + limit : router.query.limit? router.query.limit : '', + orderBy : router.query.orderBy? router.query.orderBy : '' + }; + setFinalQuery(newQuery); +}, [router.query, prefixUrl, slug, brand, category, priceFrom, priceTo, currentPage]); + useEffect(() => { + setQueryFinal({ ...finalQuery, q, limit, orderBy }); + }, [router.query, prefixUrl, slug, brand, category, priceFrom, priceTo, currentPage, finalQuery]); + + const { productSearch } = useProductSearch({ + query: queryFinal, + operation: 'OR', + }); + + + 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(() => { + setProducts(productSearch.data?.response?.products); + }, [productSearch]); + + const brands = []; + for ( + let i = 0; + i < productSearch.data?.facet_counts?.facet_fields?.manufacture_name_s.length; + i += 2 + ) { + const brand = + productSearch.data?.facet_counts?.facet_fields?.manufacture_name_s[i]; + const qty = + productSearch.data?.facet_counts?.facet_fields?.manufacture_name_s[i + 1]; + if (qty > 0) { + brands.push({ brand, qty }); + } + } + + const categories = []; + for ( + let i = 0; + i < productSearch.data?.facet_counts?.facet_fields?.category_name.length; + i += 2 + ) { + const name = productSearch.data?.facet_counts?.facet_fields?.category_name[i]; + const qty = + productSearch.data?.facet_counts?.facet_fields?.category_name[i + 1]; + if (qty > 0) { + categories.push({ name, qty }); + } + } + + function capitalizeFirstLetter(string) { + string = string.replace(/_/g, ' '); + return string.replace(/(^\w|\s\w)/g, function(match) { + return match.toUpperCase(); + }); + } + + const handleDeleteFilter = async (source, value) => { + let params = { + q: router.query.q, + orderBy: '', + brand: brandValues.join(','), + category: categoryValues?.join(','), + priceFrom: priceFrom || '', + priceTo: priceTo || '', + }; + + let brands = brandValues; + let catagories = categoryValues; + switch (source) { + case 'brands': + brands = brandValues.filter((item) => item !== value); + params.brand = brands.join(','); + await setBrandValues(brands); + break; + case 'category': + catagories = categoryValues.filter((item) => item !== value); + params.category = catagories.join(','); + await setCategoryValues(catagories); + break; + case 'price': + params.priceFrom = ''; + params.priceTo = ''; + break; + case 'delete': + params = { + q: router.query.q, + orderBy: '', + brand: '', + category: '', + priceFrom: '', + priceTo: '', + }; + break; + } + + handleSubmitFilter(params); + }; + const handleSubmitFilter = (params) => { + params = _.pickBy(params, _.identity); + params = toQuery(params); + router.push(`${slug}?${params}`); + }; + + + const toQuery = (obj) => { + const str = Object.keys(obj) + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`) + .join('&') + return str + } + + const whatPromo = capitalizeFirstLetter(slug) + const queryWithoutSlug = _.omit(router.query, ['slug']) + + return ( + <BasicLayout> + <Seo + title={`Promo ${Array.isArray(slug) ? slug[0] : slug} Terkini`} + description='B2B Marketplace MRO & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & Harga Kompetitif' + /> + <Promocrumb brandName={whatPromo} /> + <MobileView> + <div className='p-4 pt-0'> + <h1 className='mb-2 font-semibold text-h-sm'>Promo {whatPromo}</h1> + + <FilterChoicesComponent + brandValues={brandValues} + categoryValues={categoryValues} + priceFrom={priceFrom} + priceTo={priceTo} + handleDeleteFilter={handleDeleteFilter} + /> + {products?.length >= 1 && ( + <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> + )} + {productSearch.isLoading && <div className='container flex justify-center my-4'> + <LogoSpinner width={48} height={48} /> + </div>} + {products && ( + <> + <div className='grid grid-cols-1 gap-x-1 gap-y-1'> + {products?.map((promotion) => ( + <div key={promotion.id} className="min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-1/2 lg:w-1/3 xl:w-1/4 "> + <ProductPromoCard promotion={promotion}/> + </div> + ))} + </div> + </> + ) } + + <Pagination + pageCount={pageCount} + currentPage={parseInt(page)} + url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`} + className='mt-6 mb-2' + /> + <ProductFilter + active={popup.active} + close={popup.deactivate} + brands={brands || []} + categories={categories || []} + prefixUrl={router.asPath.includes('?') ? `${router.asPath}` : `${router.asPath}?`} + defaultBrand={null} + /> + </div> + + </MobileView> + <DesktopView> + <div className='container mx-auto flex mb-3 flex-col'> + <div className='w-full pl-6'> + <h1 className='text-2xl mb-2 font-semibold'>Promo {whatPromo}</h1> + <div className=' w-full h-full flex flex-row items-center '> + + <div className='detail-filter w-1/2 flex justify-start items-center mt-4'> + + <FilterChoicesComponent + brandValues={brandValues} + categoryValues={categoryValues} + priceFrom={priceFrom} + priceTo={priceTo} + handleDeleteFilter={handleDeleteFilter} + /> + </div> + <div className='Filter w-1/2 flex flex-col'> + + <ProductFilterDesktop + brands={brands || []} + categories={categories || []} + prefixUrl={'/shop/promo'} + // defaultBrand={null} + /> + </div> + </div> + {productSearch.isLoading ? ( + <div className='container flex justify-center my-4'> + <LogoSpinner width={48} height={48} /> + </div> + ) : products && products.length >= 1 ? ( + <> + <div className='grid grid-cols-1 gap-x-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3'> + {products?.map((promotion) => ( + <div key={promotion.id} className="min-w-[400px] max-w-[400px] mb-[20px] sm:min-w-[350px] md:min-w-[380px] lg:min-w-[400px] xl:min-w-[400px] "> + <ProductPromoCard promotion={promotion}/> + </div> + ))} + </div> + </> + ) : ( + <div className="text-center my-8"> + <p>Belum ada promo pada kategori ini</p> + </div> + )} + <div className='flex justify-between items-center mt-6 mb-2'> + <div className='pt-2 pb-6 flex items-center gap-x-3'> + <NextImage + src='/images/logo-question.png' + alt='Logo Question Indoteknik' + width={60} + height={60} + /> + <div className='text-gray_r-12/90'> + <span> + Barang yang anda cari tidak ada?{' '} + <a + href={ + router.query?.q + ? whatsappUrl('productSearch', { + name: router.query.q, + }) + : whatsappUrl() + } + className='text-danger-500' + > + Hubungi Kami + </a> + </span> + </div> + </div> + + + + <Pagination + pageCount={pageCount} + currentPage={parseInt(page)} + url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`} + className='mt-6 mb-2' + /> + </div> + + </div> + </div> + </DesktopView> + </BasicLayout> + ) + } + +const FilterChoicesComponent = ({ + brandValues, + categoryValues, + priceFrom, + priceTo, + handleDeleteFilter, + }) => ( + <div className='flex items-center mb-4'> + <HStack spacing={2} className='flex-wrap'> + {brandValues?.map((value, index) => ( + <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' + > + <TagLabel>{value}</TagLabel> + <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)} + /> + </Tag> + )} + {brandValues?.length > 0 || + categoryValues?.length > 0 || + priceFrom || + priceTo ? ( + <span> + <button + className='btn-transparent py-2 px-5 h-[40px] text-red-700' + onClick={() => handleDeleteFilter('delete')} + > + Hapus Semua + </button> + </span> + ) : ( + '' + )} + </HStack> + </div> +); diff --git a/src/pages/shop/promo/[slug].tsx b/src/pages/shop/promo/[slug].tsx deleted file mode 100644 index aaee1249..00000000 --- a/src/pages/shop/promo/[slug].tsx +++ /dev/null @@ -1,523 +0,0 @@ -import dynamic from 'next/dynamic' -import NextImage from 'next/image'; -import { useEffect, useState } from 'react' -import { useRouter } from 'next/router' -import Seo from '../../../core/components/Seo' -import Promocrumb from '../../../lib/promo/components/Promocrumb' -import { fetchPromoItemsSolr, fetchVariantSolr } from '../../../api/promoApi' -import LogoSpinner from '../../../core/components/elements/Spinner/LogoSpinner.jsx' -import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card' -import { IPromotion } from '../../../../src-migrate/types/promotion' -import React from 'react' -import { SolrResponse } from "../../../../src-migrate/types/solr.ts"; -import DesktopView from '../../../core/components/views/DesktopView'; -import MobileView from '../../../core/components/views/MobileView'; -import 'swiper/swiper-bundle.css'; -import useDevice from '../../../core/hooks/useDevice' -import ProductFilterDesktop from '../../../lib/product/components/ProductFilterDesktopPromotion'; -import ProductFilter from '../../../lib/product/components/ProductFilter'; -import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react'; -import { formatCurrency } from '../../../core/utils/formatValue'; -import Pagination from '../../../core/components/elements/Pagination/Pagination'; -import SideBanner from '../../../../src-migrate/modules/side-banner'; -import whatsappUrl from '../../../core/utils/whatsappUrl'; -import { cons, toQuery } from 'lodash-contrib'; -import _ from 'lodash'; -import useActive from '../../../core/hooks/useActive'; - -const BasicLayout = dynamic(() => import('../../../core/components/layouts/BasicLayout')) - -export default function PromoDetail() { - const router = useRouter() - const { slug = '', brand ='', category='', priceFrom = '', priceTo = '', page = '1' } = router.query - const [promoItems, setPromoItems] = useState<any[]>([]) - const [promoData, setPromoData] = useState<IPromotion[] | null>(null) - const [currentPage, setCurrentPage] = useState(parseInt(page as string, 10) || 1); - const itemsPerPage = 12; // Jumlah item yang ingin ditampilkan per halaman - const [loading, setLoading] = useState(true); - const { isMobile, isDesktop } = useDevice() - const [brands, setBrands] = useState<Brand[]>([]); - const [categories, setCategories] = useState<Category[]>([]); - const [brandValues, setBrandValues] = useState<string[]>([]); - const [categoryValues, setCategoryValues] = useState<string[]>([]); - const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular'); - const popup = useActive(); - const prefixUrl = `/shop/promo/${slug}` - - useEffect(() => { - if (router.query.brand) { - let brandsArray: string[] = []; - if (Array.isArray(router.query.brand)) { - brandsArray = router.query.brand; - } else if (typeof router.query.brand === 'string') { - brandsArray = router.query.brand.split(',').map((brand) => brand.trim()); - } - setBrandValues(brandsArray); - } else { - setBrandValues([]); - } - - if (router.query.category) { - let categoriesArray: string[] = []; - - if (Array.isArray(router.query.category)) { - categoriesArray = router.query.category; - } else if (typeof router.query.category === 'string') { - categoriesArray = router.query.category.split(',').map((category) => category.trim()); - } - setCategoryValues(categoriesArray); - } else { - setCategoryValues([]); - } - }, [router.query.brand, router.query.category]); - - interface Brand { - brand: string; - qty: number; - } - - interface Category { - name: string; - qty: number; - } - - useEffect(() => { - const loadPromo = async () => { - setLoading(true); - const brandsData: Brand[] = []; - const categoriesData: Category[] = []; - - const pageNumber = Array.isArray(page) ? parseInt(page[0], 10) : parseInt(page, 10); - setCurrentPage(pageNumber) - - try { - const items = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug}`,0,100); - setPromoItems(items); - - if (items.length === 0) { - setPromoData([]) - setLoading(false); - return; - } - - const brandArray = Array.isArray(brand) ? brand : brand.split(','); - const categoryArray = Array.isArray(category) ? category : category.split(','); - - const promoDataPromises = items.map(async (item) => { - - try { - let brandQuery = ''; - if (brand) { - brandQuery = brandArray.map(b => `manufacture_name_s:${b}`).join(' OR '); - brandQuery = `(${brandQuery})`; - } - - let categoryQuery = ''; - if (category) { - categoryQuery = categoryArray.map(c => `category_name:${c}`).join(' OR '); - categoryQuery = `(${categoryQuery})`; - } - - let priceQuery = ''; - if (priceFrom && priceTo) { - priceQuery = `price_f:[${priceFrom} TO ${priceTo}]`; - } else if (priceFrom) { - priceQuery = `price_f:[${priceFrom} TO *]`; - } else if (priceTo) { - priceQuery = `price_f:[* TO ${priceTo}]`; - } - - let combinedQuery = ''; - let combinedQueryPrice = `${priceQuery}`; - if (brand && category && priceFrom || priceTo) { - combinedQuery = `${brandQuery} AND ${categoryQuery} `; - } else if (brand && category) { - combinedQuery = `${brandQuery} AND ${categoryQuery}`; - } else if (brand && priceFrom || priceTo) { - combinedQuery = `${brandQuery}`; - } else if (category && priceFrom || priceTo) { - combinedQuery = `${categoryQuery}`; - } else if (brand) { - combinedQuery = brandQuery; - } else if (category) { - combinedQuery = categoryQuery; - } - - if (combinedQuery && priceFrom || priceTo) { - const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`); - const product = response.response.docs[0]; - const product_id = product.id; - const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} AND ${combinedQueryPrice}`,0,100); - return response2; - }else if(combinedQuery){ - const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`); - const product = response.response.docs[0]; - const product_id = product.id; - const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} `,0,100); - return response2; - } else { - const response = await fetchPromoItemsSolr(`id:${item.id}`,0,100); - return response; - } - } catch (fetchError) { - return []; - } - }); - - const promoDataArray = await Promise.all(promoDataPromises); - const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); - setPromoData(mergedPromoData); - - const dataBrandCategoryPromises = promoDataArray.map(async (promoData) => { - if (promoData) { - const dataBrandCategory = promoData.map(async (item) => { - let response; - if(category){ - const categoryQuery = categoryArray.map(c => `category_name:${c}`).join(' OR '); - response = await fetchVariantSolr(`id:${item.products[0].product_id} AND (${categoryQuery})`); - }else{ - response = await fetchVariantSolr(`id:${item.products[0].product_id}`) - } - - - if (response.response?.docs?.length > 0) { - const product = response.response.docs[0]; - const manufactureNameS = product.manufacture_name; - if (Array.isArray(manufactureNameS)) { - for (let i = 0; i < manufactureNameS.length; i += 2) { - const brand = manufactureNameS[i]; - const qty = 1; - const existingBrandIndex = brandsData.findIndex(b => b.brand === brand); - if (existingBrandIndex !== -1) { - brandsData[existingBrandIndex].qty += qty; - } else { - brandsData.push({ brand, qty }); - } - } - } - - const categoryNameS = product.category_name; - if (Array.isArray(categoryNameS)) { - for (let i = 0; i < categoryNameS.length; i += 2) { - const name = categoryNameS[i]; - const qty = 1; - const existingCategoryIndex = categoriesData.findIndex(c => c.name === name); - if (existingCategoryIndex !== -1) { - categoriesData[existingCategoryIndex].qty += qty; - } else { - categoriesData.push({ name, qty }); - } - } - } - } - }); - - return Promise.all(dataBrandCategory); - } - }); - - await Promise.all(dataBrandCategoryPromises); - setBrands(brandsData); - setCategories(categoriesData); - setLoading(false); - - } catch (loadError) { - // console.error("Error loading promo items:", loadError) - setLoading(false); - } - } - - if (slug) { - loadPromo() - } - },[slug, brand, category, priceFrom, priceTo, currentPage]); - - - function capitalizeFirstLetter(string) { - string = string.replace(/_/g, ' '); - return string.replace(/(^\w|\s\w)/g, function(match) { - return match.toUpperCase(); - }); - } - - const handleDeleteFilter = async (source, value) => { - let params = { - q: router.query.q, - orderBy: '', - brand: brandValues.join(','), - category: categoryValues.join(','), - priceFrom: priceFrom || '', - priceTo: priceTo || '', - }; - - let brands = brandValues; - let catagories = categoryValues; - switch (source) { - case 'brands': - brands = brandValues.filter((item) => item !== value); - params.brand = brands.join(','); - await setBrandValues(brands); - break; - case 'category': - catagories = categoryValues.filter((item) => item !== value); - params.category = catagories.join(','); - await setCategoryValues(catagories); - break; - case 'price': - params.priceFrom = ''; - params.priceTo = ''; - break; - case 'delete': - params = { - q: router.query.q, - orderBy: '', - brand: '', - category: '', - priceFrom: '', - priceTo: '', - }; - break; - } - - handleSubmitFilter(params); - }; - const handleSubmitFilter = (params) => { - params = _.pickBy(params, _.identity); - params = toQuery(params); - router.push(`${slug}?${params}`); - }; - - const visiblePromotions = promoData?.slice( (currentPage-1) * itemsPerPage, currentPage * 12) - - const toQuery = (obj) => { - const str = Object.keys(obj) - .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`) - .join('&') - return str - } - - const whatPromo = capitalizeFirstLetter(slug) - const queryWithoutSlug = _.omit(router.query, ['slug']) - const queryString = toQuery(queryWithoutSlug) - - return ( - <BasicLayout> - <Seo - title={`Promo ${Array.isArray(slug) ? slug[0] : slug} Terkini`} - description='B2B Marketplace MRO & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & Harga Kompetitif' - /> - <Promocrumb brandName={whatPromo} /> - <MobileView> - <div className='p-4 pt-0'> - <h1 className='mb-2 font-semibold text-h-sm'>Promo {whatPromo}</h1> - - <FilterChoicesComponent - brandValues={brandValues} - categoryValues={categoryValues} - priceFrom={priceFrom} - priceTo={priceTo} - handleDeleteFilter={handleDeleteFilter} - /> - {promoItems.length >= 1 && ( - <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> - )} - - {loading ? ( - <div className='container flex justify-center my-4'> - <LogoSpinner width={48} height={48} /> - </div> - ) : promoData && promoItems.length >= 1 ? ( - <> - <div className='grid grid-cols-1 gap-x-1 gap-y-1'> - {visiblePromotions?.map((promotion) => ( - <div key={promotion.id} className="min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-1/2 lg:w-1/3 xl:w-1/4 "> - <ProductPromoCard promotion={promotion}/> - </div> - ))} - </div> - </> - ) : ( - <div className="text-center my-8"> - <p>Belum ada promo pada kategori ini</p> - </div> - )} - - <Pagination - pageCount={Math.ceil((promoData?.length ?? 0) / itemsPerPage)} - currentPage={currentPage} - url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`} - className='mt-6 mb-2' - /> - <ProductFilter - active={popup.active} - close={popup.deactivate} - brands={brands || []} - categories={categories || []} - prefixUrl={router.asPath.includes('?') ? `${router.asPath}` : `${router.asPath}?`} - defaultBrand={null} - /> - </div> - - </MobileView> - <DesktopView> - <div className='container mx-auto flex mb-3 flex-col'> - <div className='w-full pl-6'> - <h1 className='text-2xl mb-2 font-semibold'>Promo {whatPromo}</h1> - <div className=' w-full h-full flex flex-row items-center '> - - <div className='detail-filter w-1/2 flex justify-start items-center mt-4'> - - <FilterChoicesComponent - brandValues={brandValues} - categoryValues={categoryValues} - priceFrom={priceFrom} - priceTo={priceTo} - handleDeleteFilter={handleDeleteFilter} - /> - </div> - <div className='Filter w-1/2 flex flex-col'> - - <ProductFilterDesktop - brands={brands || []} - categories={categories || []} - prefixUrl={'/shop/promo'} - // defaultBrand={null} - /> - </div> - </div> - {loading ? ( - <div className='container flex justify-center my-4'> - <LogoSpinner width={48} height={48} /> - </div> - ) : promoData && promoItems.length >= 1 ? ( - <> - <div className='grid grid-cols-1 gap-x-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3'> - {visiblePromotions?.map((promotion) => ( - <div key={promotion.id} className="min-w-[400px] max-w-[400px] mb-[20px] sm:min-w-[350px] md:min-w-[380px] lg:min-w-[400px] xl:min-w-[400px] "> - <ProductPromoCard promotion={promotion}/> - </div> - ))} - </div> - </> - ) : ( - <div className="text-center my-8"> - <p>Belum ada promo pada kategori ini</p> - </div> - )} - <div className='flex justify-between items-center mt-6 mb-2'> - <div className='pt-2 pb-6 flex items-center gap-x-3'> - <NextImage - src='/images/logo-question.png' - alt='Logo Question Indoteknik' - width={60} - height={60} - /> - <div className='text-gray_r-12/90'> - <span> - Barang yang anda cari tidak ada?{' '} - <a - href={ - router.query?.q - ? whatsappUrl('productSearch', { - name: router.query.q, - }) - : whatsappUrl() - } - className='text-danger-500' - > - Hubungi Kami - </a> - </span> - </div> - </div> - - - - <Pagination - pageCount={Math.ceil((promoData?.length ?? 0) / itemsPerPage)} - currentPage={currentPage} - url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`} - className='mt-6 mb-2' - /> - </div> - - </div> - </div> - </DesktopView> - </BasicLayout> - ) - } - -const FilterChoicesComponent = ({ - brandValues, - categoryValues, - priceFrom, - priceTo, - handleDeleteFilter, - }) => ( - <div className='flex items-center mb-4'> - <HStack spacing={2} className='flex-wrap'> - {brandValues?.map((value, index) => ( - <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' - > - <TagLabel>{value}</TagLabel> - <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)} - /> - </Tag> - )} - {brandValues?.length > 0 || - categoryValues?.length > 0 || - priceFrom || - priceTo ? ( - <span> - <button - className='btn-transparent py-2 px-5 h-[40px] text-red-700' - onClick={() => handleDeleteFilter('delete')} - > - Hapus Semua - </button> - </span> - ) : ( - '' - )} - </HStack> - </div> -); diff --git a/src/pages/sitemap/brands.xml.js b/src/pages/sitemap/brands.xml.js index c85c40e9..65a84e97 100644 --- a/src/pages/sitemap/brands.xml.js +++ b/src/pages/sitemap/brands.xml.js @@ -15,8 +15,8 @@ export async function getServerSideProps({ res }) { const url = sitemap.ele('url') url.ele('loc', createSlug(baseUrl, brand.name, brand.id)) url.ele('lastmod', date.toISOString().slice(0, 10)) - url.ele('changefreq', 'weekly') - url.ele('priority', '0.6') + url.ele('changefreq', 'daily') + url.ele('priority', '1.0') }) res.setHeader('Content-Type', 'text/xml') diff --git a/src/pages/sitemap/products/[page].js b/src/pages/sitemap/products/[page].js index 2f9c3198..e39755d6 100644 --- a/src/pages/sitemap/products/[page].js +++ b/src/pages/sitemap/products/[page].js @@ -19,7 +19,7 @@ export async function getServerSideProps({ query, res }) { const url = sitemap.ele('url') url.ele('loc', createSlug(baseUrl, product.name, product.id)) url.ele('lastmod', date.toISOString().slice(0, 10)) - url.ele('changefreq', 'weekly') + url.ele('changefreq', 'daily') url.ele('priority', '0.8') }) diff --git a/src/pages/tracking-order.jsx b/src/pages/tracking-order.jsx new file mode 100644 index 00000000..002acd42 --- /dev/null +++ b/src/pages/tracking-order.jsx @@ -0,0 +1,27 @@ +import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' +import SimpleFooter from '@/core/components/elements/Footer/SimpleFooter'; +import BasicLayout from '@/core/components/layouts/BasicLayout'; +import DesktopView from '@/core/components/views/DesktopView'; +import MobileView from '@/core/components/views/MobileView'; +const PageTrackingOrder = dynamic(() => import('@/lib/tracking-order/component/TrackingOrder')) + +export default function TrackingOrder() { + return ( + <> + <Seo title='Tracking Order - Indoteknik.com' /> + + <DesktopView> + <BasicLayout> + <PageTrackingOrder/> + </BasicLayout> + </DesktopView> + + <MobileView> + <BasicLayout> + <PageTrackingOrder/> + </BasicLayout> + </MobileView> + </> + ); +} diff --git a/src/styles/globals.css b/src/styles/globals.css index f6561b00..b3ca85f4 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -563,7 +563,8 @@ button { } .category-mega-box > div:hover .category-mega-box__parent { - @apply bg-gray_r-5; + @apply bg-gray_r-5 + w-full; } .category-mega-box > div:hover .category-mega-box__child-wrapper { @@ -583,12 +584,11 @@ button { @apply absolute left-[100%] top-12 - w-[40vw] bg-gray_r-1/90 backdrop-blur-md border border-gray_r-6 - p-6 + p-6 opacity-0 h-full transition-all @@ -604,6 +604,7 @@ button { transition-colors ease-linear duration-100 + w-fit font-semibold; } @@ -613,6 +614,7 @@ button { transition-colors ease-linear duration-100 + w-full font-normal; } diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js index d4694eb2..0d50b99b 100644 --- a/src/utils/solrMapping.js +++ b/src/utils/solrMapping.js @@ -1,3 +1,29 @@ +export const promoMappingSolr = (promotions) => { + return promotions.map((promotion) =>{ + let productMapped = { + id: promotion.id, + program_id: promotion.program_id_i, + name: promotion.name_s, + type: { + value: promotion.type_value_s, + label: promotion.type_label_s, + }, + limit: promotion.package_limit_i, + limit_user: promotion.package_limit_user_i, + limit_trx: promotion.package_limit_trx_i, + price: promotion.price_f, + sequence: promotion.sequence_i, + total_qty: promotion.total_qty_i, + products: JSON.parse(promotion.products_s) || '', + product_id: promotion.product_ids[0], + qty_sold_f:promotion.total_qty_sold_f, + free_products: JSON.parse(promotion.free_products_s) + }; + return productMapped; + }) +}; + + export const productMappingSolr = (products, pricelist) => { return products.map((product) => { let price = product.price_tier1_v2_f || 0; @@ -38,6 +64,7 @@ export const productMappingSolr = (products, pricelist) => { qtySold: product?.qty_sold_f || 0, isTkdn:product?.tkdn_b || false, isSni:product?.sni_b || false, + is_in_bu:product?.is_in_bu_b || false, voucherPastiHemat:product?.voucher_pastihemat || [] }; @@ -122,3 +149,4 @@ const flashsaleTime = (endDate) => { isFlashSale: flashsaleEndDate > currentTime, }; }; + |
