summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/core/components/elements/Navbar/NavbarDesktop.jsx7
-rw-r--r--src/core/components/elements/Navbar/Search.jsx42
-rw-r--r--src/core/components/elements/Product/cartProductsList.jsx5
-rw-r--r--src/core/utils/formatValue.js10
-rw-r--r--src/lib/auth/components/IsAuth.jsx2
-rw-r--r--src/lib/auth/hooks/useLogin.js2
-rw-r--r--src/lib/brand/components/Brand.jsx125
-rw-r--r--src/lib/brand/components/Breadcrumb.jsx40
-rw-r--r--src/lib/cart/components/Cartheader.jsx325
-rw-r--r--src/lib/category/components/Breadcrumb.jsx56
-rw-r--r--src/lib/category/components/Category.jsx2
-rw-r--r--src/lib/checkout/components/Checkout.jsx93
-rw-r--r--src/lib/flashSale/components/FlashSale.jsx2
-rw-r--r--src/lib/product/components/Product/Breadcrumb.jsx69
-rw-r--r--src/lib/product/components/Product/Product.jsx38
-rw-r--r--src/lib/product/components/Product/ProductDesktop.jsx126
-rw-r--r--src/lib/product/components/Product/ProductMobile.jsx31
-rw-r--r--src/lib/product/components/ProductCard.jsx31
-rw-r--r--src/lib/product/components/ProductFilter.jsx120
-rw-r--r--src/lib/product/components/ProductFilterDesktop.jsx199
-rw-r--r--src/lib/product/components/ProductSearch.jsx147
-rw-r--r--src/lib/promotinProgram/components/PromotionType.jsx2
-rw-r--r--src/lib/variant/components/VariantCard.jsx2
-rw-r--r--src/pages/api/shop/search.js19
-rw-r--r--src/pages/shop/brands/[slug].jsx3
-rw-r--r--src/pages/shop/cart.jsx15
-rw-r--r--src/pages/shop/category/[slug].jsx10
-rw-r--r--src/pages/shop/product/[slug].jsx33
-rw-r--r--src/pages/shop/search.jsx20
-rw-r--r--src/utils/solrMapping.js6
30 files changed, 1137 insertions, 445 deletions
diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx
index fb94e4a6..d9f5658e 100644
--- a/src/core/components/elements/Navbar/NavbarDesktop.jsx
+++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx
@@ -40,7 +40,7 @@ const NavbarDesktop = () => {
const router = useRouter()
const { product } = useProductContext()
-
+
useEffect(() => {
if (router.pathname === '/shop/product/[slug]') {
setPayloadWa({
@@ -110,7 +110,9 @@ const NavbarDesktop = () => {
<br />
Quotation
</Link>
- <Cardheader cartCount={cartCount}/>
+
+ <Cardheader cartCount={cartCount} />
+
<Link
target='_blank'
rel='noreferrer'
@@ -134,6 +136,7 @@ const NavbarDesktop = () => {
</a>
</div>
</div>
+
<div className='container mx-auto mt-6'>
<div className='flex'>
<button
diff --git a/src/core/components/elements/Navbar/Search.jsx b/src/core/components/elements/Navbar/Search.jsx
index f4a8ab3a..e4f89103 100644
--- a/src/core/components/elements/Navbar/Search.jsx
+++ b/src/core/components/elements/Navbar/Search.jsx
@@ -3,12 +3,18 @@ import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
import { useCallback, useEffect, useRef, useState } from 'react'
import Link from '../Link/Link'
import { useRouter } from 'next/router'
+import { getIdFromSlug, getNameFromSlug } from '@/core/utils/slug'
const Search = () => {
const router = useRouter()
const queryRef = useRef()
+ const { slug = '' } = router.query
const [query, setQuery] = useState(router.query.q || '')
const [suggestions, setSuggestions] = useState([])
+ const [segment, setSegment] = useState(null)
+ const [optionSegment, setOptionSegment] = useState(null)
+ const [optoinSelected, setOptionSelected] = useState(getNameFromSlug(slug) || 'default')
+ const [pathSegments, setPathSegments] = useState(router?.asPath.split('/') || [])
const loadSuggestion = useCallback(() => {
if (query && document.activeElement == queryRef.current) {
@@ -26,14 +32,29 @@ const Search = () => {
} else {
setSuggestions([])
}
+ setFilterSearch()
}, [loadSuggestion, query])
const handleSubmit = (e) => {
e.preventDefault()
- if (query) {
- router.push(`/shop/search?q=${query}`)
+ if (optionSegment && optoinSelected !== 'default' && optoinSelected) {
+ router.push(`/shop/${segment}/${slug}?q=${query}`)
} else {
- queryRef.current.focus()
+ if (query) {
+ router.push(`/shop/search?q=${query}`)
+ } else {
+ queryRef.current.focus()
+ }
+ }
+ }
+
+ const handleSelectChange = async (e) => {
+ await setOptionSelected(e.target.value)
+ }
+ const setFilterSearch = async () => {
+ if (router.pathname.includes('brands') && pathSegments[3]) {
+ await setSegment(pathSegments[2])
+ await setOptionSegment(getNameFromSlug(slug))
}
}
@@ -46,10 +67,23 @@ const Search = () => {
return (
<>
<form onSubmit={handleSubmit} className='flex-1 flex relative'>
+ {segment && (
+ <select
+ value={optoinSelected}
+ onChange={handleSelectChange}
+ className='form-select p-3 rounded-l-sm bg-white border border-gray_r-6 border-r-0 capitalize'
+ >
+ <option value='default'>Di Indoteknik</option>
+ <option value={optionSegment}>Di {optionSegment}</option>
+ </select>
+ )}
+
<input
type='text'
ref={queryRef}
- className='form-input p-3 rounded-r-none border-r-0 focus:border-gray_r-6'
+ className={`form-input p-3 ${
+ segment ? 'rounded-l-none border-l-0' : ''
+ } rounded-r-none border-r-0 focus:border-gray_r-6`}
placeholder='Ketik nama, part number, merk'
value={query}
onChange={(e) => setQuery(e.target.value)}
diff --git a/src/core/components/elements/Product/cartProductsList.jsx b/src/core/components/elements/Product/cartProductsList.jsx
index 32df992b..9662cd3b 100644
--- a/src/core/components/elements/Product/cartProductsList.jsx
+++ b/src/core/components/elements/Product/cartProductsList.jsx
@@ -15,7 +15,6 @@ const CardProdcuctsList = ({
updateQuantity = () => {},
setDeleteConfirmation = () => {}
}) => {
-
return (
<table className='table-cart'>
<thead>
@@ -144,7 +143,9 @@ const CardProdcuctsList = ({
{product?.code}{' '}
{product?.attributes.length > 0 ? `| ${product?.attributes.join(', ')}` : ''}
</div>
- <div className='text-gray_r-11 mt-2'>Berat item : {product?.weight} Kg</div>
+ <div className='text-gray_r-11 mt-2'>
+ Berat item : {product?.weight} Kg x {product?.quantity} Barang
+ </div>
</div>
</td>
<td className='relative'>
diff --git a/src/core/utils/formatValue.js b/src/core/utils/formatValue.js
new file mode 100644
index 00000000..f2c17769
--- /dev/null
+++ b/src/core/utils/formatValue.js
@@ -0,0 +1,10 @@
+const sellingProductFormat = (value) => {
+ if (value > 1000) {
+ const formattedValue = (value / 1000).toFixed(0).replace('.', ',')
+ return formattedValue + 'rb+';
+ } else {
+ return value.toString()
+ }
+}
+
+export { sellingProductFormat }
diff --git a/src/lib/auth/components/IsAuth.jsx b/src/lib/auth/components/IsAuth.jsx
index a32e648c..1948ae0c 100644
--- a/src/lib/auth/components/IsAuth.jsx
+++ b/src/lib/auth/components/IsAuth.jsx
@@ -8,7 +8,7 @@ const IsAuth = ({ children }) => {
useEffect(() => {
if (!getAuth() && router.pathname != '/login') {
- router.replace(`/login?next=${router.asPath}`)
+ router.replace(`/login?next=${encodeURIComponent(router.asPath)}`)
} else {
setResponse(children)
}
diff --git a/src/lib/auth/hooks/useLogin.js b/src/lib/auth/hooks/useLogin.js
index 34605614..17731a86 100644
--- a/src/lib/auth/hooks/useLogin.js
+++ b/src/lib/auth/hooks/useLogin.js
@@ -71,7 +71,7 @@ const useLogin = () => {
if (data.isAuth) {
session.odooUser = data.user
setCookie('auth', JSON.stringify(session?.odooUser))
- router.push(router?.query?.next ?? '/')
+ router.push(decodeURIComponent(router?.query?.next) ?? '/')
return
}
}
diff --git a/src/lib/brand/components/Brand.jsx b/src/lib/brand/components/Brand.jsx
index 3c411969..4afbcb3e 100644
--- a/src/lib/brand/components/Brand.jsx
+++ b/src/lib/brand/components/Brand.jsx
@@ -6,10 +6,10 @@ import { Pagination, Autoplay } from 'swiper'
import 'swiper/css'
import 'swiper/css/pagination'
import 'swiper/css/autoplay'
-import Divider from '@/core/components/elements/Divider/Divider'
-import ImageSkeleton from '@/core/components/elements/Skeleton/ImageSkeleton'
import MobileView from '@/core/components/views/MobileView'
import DesktopView from '@/core/components/views/DesktopView'
+import { Skeleton } from '@chakra-ui/react'
+import classNames from 'classnames'
const swiperBanner = {
pagination: { dynamicBullets: true },
@@ -28,65 +28,77 @@ const Brand = ({ id }) => {
<MobileView>
<>
<div>
- {brand.isLoading && <ImageSkeleton />}
- {brand.data?.banners?.length == 0 && (
- <Image
- src='/images/default-banner-brand.jpg'
- alt='Brand - Indoteknik'
- width={1024}
- height={512}
- className='w-full h-auto'
- />
- )}
- {brand.data && (
- <>
- <Swiper
- slidesPerView={1}
- pagination={swiperBanner.pagination}
- modules={swiperBanner.modules}
- autoplay={swiperBanner.autoplay}
- className='border-b border-gray_r-6'
- >
- {brand.data?.banners?.map((banner, index) => (
- <SwiperSlide key={index}>
+ <Skeleton
+ isLoaded={!brand.isLoading}
+ aspectRatio='4/2'
+ className={classNames({
+ 'mb-6': brand.isLoading
+ })}
+ >
+ {brand.data?.banners?.length == 0 && (
+ <Image
+ src='/images/default-banner-brand.jpg'
+ alt='Brand - Indoteknik'
+ width={1024}
+ height={512}
+ className='w-full h-auto'
+ />
+ )}
+
+ {brand.data && (
+ <>
+ <Swiper
+ slidesPerView={1}
+ pagination={swiperBanner.pagination}
+ modules={swiperBanner.modules}
+ autoplay={swiperBanner.autoplay}
+ className='border-b border-gray_r-6'
+ >
+ {brand.data?.banners?.map((banner, index) => (
+ <SwiperSlide key={index}>
+ <Image
+ src={banner}
+ alt={`Brand ${brand.data?.name} - Indoteknik`}
+ width={1024}
+ height={512}
+ className='w-full h-auto'
+ />
+ </SwiperSlide>
+ ))}
+ </Swiper>
+ <div className='p-4'>
+ <div className='text-caption-1 text-gray_r-11 mb-2'>Produk dari brand:</div>
+ {brand?.data?.logo && (
<Image
- src={banner}
- alt={`Brand ${brand.data?.name} - Indoteknik`}
- width={1024}
- height={512}
- className='w-full h-auto'
+ src={brand?.data?.logo}
+ alt={brand?.data?.name}
+ className='w-32 p-2 border borde-gray_r-6 rounded'
+ width={256}
+ height={128}
/>
- </SwiperSlide>
- ))}
- </Swiper>
- <div className='p-4'>
- <div className='text-caption-1 text-gray_r-11 mb-2'>Produk dari brand:</div>
- {brand?.data?.logo && (
- <Image
- src={brand?.data?.logo}
- alt={brand?.data?.name}
- className='w-32 p-2 border borde-gray_r-6 rounded'
- width={256}
- height={128}
- />
- )}
- {!brand?.data?.logo && (
- <div className='bg-danger-500 text-white text-center text-caption-1 py-2 px-4 rounded w-fit'>
- {brand?.data?.name}
- </div>
- )}
- </div>
- </>
- )}
+ )}
+ {!brand?.data?.logo && (
+ <div className='bg-danger-500 text-white text-center text-caption-1 py-2 px-4 rounded w-fit'>
+ {brand?.data?.name}
+ </div>
+ )}
+ </div>
+ </>
+ )}
+ </Skeleton>
</div>
- <Divider />
</>
</MobileView>
<DesktopView>
- <div className='container mx-auto mt-10 mb-3'>
- <div className='min-h-[150px]'>
- {brand.isLoading && <ImageSkeleton />}
+ <div className='container mx-auto'>
+ <Skeleton
+ isLoaded={!brand.isLoading}
+ aspectRatio='4/1'
+ className={classNames({
+ 'mb-6': brand.isLoading
+ })}
+ >
{brand.data?.banners?.length == 0 && (
<Image
src='/images/default-banner-brand.jpg'
@@ -96,6 +108,7 @@ const Brand = ({ id }) => {
className='w-full h-auto'
/>
)}
+
{brand.data && (
<>
<Swiper
@@ -117,6 +130,7 @@ const Brand = ({ id }) => {
</SwiperSlide>
))}
</Swiper>
+
<div className='p-4'>
<div className='text-caption-1 text-gray_r-11 mb-2'>Produk dari brand:</div>
{brand?.data?.logo && (
@@ -136,8 +150,7 @@ const Brand = ({ id }) => {
</div>
</>
)}
- </div>
- <Divider />
+ </Skeleton>
</div>
</DesktopView>
</>
diff --git a/src/lib/brand/components/Breadcrumb.jsx b/src/lib/brand/components/Breadcrumb.jsx
new file mode 100644
index 00000000..0fec2dad
--- /dev/null
+++ b/src/lib/brand/components/Breadcrumb.jsx
@@ -0,0 +1,40 @@
+import { Breadcrumb as ChakraBreadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'
+import Link from 'next/link'
+import React from 'react'
+
+/**
+ * Renders a breadcrumb component with links to navigate through different pages.
+ *
+ * @param {Object} props - The props object containing the brand name.
+ * @param {string} props.brandName - The name of the brand to display in the breadcrumb.
+ * @return {JSX.Element} The rendered breadcrumb component.
+ */
+const Breadcrumb = ({ brandName }) => {
+ return (
+ <div className='container mx-auto py-4 md:py-6'>
+ <ChakraBreadcrumb>
+ <BreadcrumbItem>
+ <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'>
+ Home
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+
+ <BreadcrumbItem>
+ <BreadcrumbLink
+ as={Link}
+ href='/shop/brands'
+ className='!text-danger-500 whitespace-nowrap'
+ >
+ Brands
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+
+ <BreadcrumbItem isCurrentPage>
+ <BreadcrumbLink className='whitespace-nowrap'>{brandName}</BreadcrumbLink>
+ </BreadcrumbItem>
+ </ChakraBreadcrumb>
+ </div>
+ )
+}
+
+export default Breadcrumb
diff --git a/src/lib/cart/components/Cartheader.jsx b/src/lib/cart/components/Cartheader.jsx
index b3cf849b..580dfc8c 100644
--- a/src/lib/cart/components/Cartheader.jsx
+++ b/src/lib/cart/components/Cartheader.jsx
@@ -8,6 +8,7 @@ import { useRouter } from 'next/router'
import odooApi from '@/core/api/odooApi'
import { useProductCartContext } from '@/contexts/ProductCartContext'
import whatsappUrl from '@/core/utils/whatsappUrl'
+import { AnimatePresence, motion } from 'framer-motion'
const { ShoppingCartIcon, PhotoIcon } = require('@heroicons/react/24/outline')
const { default: Link } = require('next/link')
@@ -113,164 +114,182 @@ const Cardheader = (cartCount) => {
</span>
</Link>
</div>
- <div
- className={` ${
- isHovered ? 'block' : 'hidden'
- } fixed top-[155px] left-0 w-full h-full bg-black opacity-50 z-10`}
- ></div>
- <div
- className='hidden group-hover:block absolute z-10 left-0 w-96'
- onMouseEnter={handleMouseEnter}
- onMouseLeave={handleMouseLeave}
- >
- <div className='w-full max-w-md p-2 bg-white border border-gray-200 rounded-lg shadow'>
- <div className='p-2 flex justify-between items-center'>
- <h5 class='text-base font-semibold leading-none'>Keranjang Belanja</h5>
- <Link href='/shop/cart' class='text-sm font-medium text-red-600 underline'>
- Lihat Semua
- </Link>
- </div>
- <hr className='mt-3 mb-3 border border-gray-100' />
- <div className='flow-root max-h-[250px] overflow-y-auto'>
- {!auth && (
- <div className='justify-center p-4'>
- <p className='text-gray-500 text-center '>
- Silahkan{' '}
- <Link href='/login' className='text-red-600 underline leading-6'>
- Login
- </Link>{' '}
- Untuk Melihat Daftar Keranjang Belanja Anda
- </p>
- </div>
- )}
- {isLoading &&
- itemLoading.map((item) => (
- <div key={item} role='status' class='max-w-sm animate-pulse'>
- <div class='flex items-center space-x-4 mb- 2'>
- <div class='flex-shrink-0'>
- <PhotoIcon class='h-16 w-16 text-gray-500' />
+
+ <AnimatePresence>
+ {isHovered && (
+ <>
+ <motion.div
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ exit={{ opacity: 0 }}
+ transition={{ duration: 0.15 }}
+ className='fixed top-[155px] left-0 w-full h-full bg-black/50 z-10'
+ />
+
+ <motion.div
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1, transition: { duration: 0.2 } }}
+ exit={{ opacity: 0, transition: { duration: 0.3 } }}
+ className='absolute z-10 left-0 w-96'
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
+ >
+ <motion.div
+ initial={{ height: 0 }}
+ animate={{ height: 'auto' }}
+ exit={{ height: 0 }}
+ className='w-full max-w-md p-2 bg-white border border-gray-200 rounded-lg shadow overflow-hidden'
+ >
+ <div className='p-2 flex justify-between items-center'>
+ <h5 className='text-base font-semibold leading-none'>Keranjang Belanja</h5>
+ <Link href='/shop/cart' class='text-sm font-medium text-red-600 underline'>
+ Lihat Semua
+ </Link>
+ </div>
+ <hr className='mt-3 mb-3 border border-gray-100' />
+ <div className='flow-root max-h-[250px] overflow-y-auto'>
+ {!auth && (
+ <div className='justify-center p-4'>
+ <p className='text-gray-500 text-center '>
+ Silahkan{' '}
+ <Link href='/login' className='text-red-600 underline leading-6'>
+ Login
+ </Link>{' '}
+ Untuk Melihat Daftar Keranjang Belanja Anda
+ </p>
</div>
- <div class='flex-1 min-w-0'>
- <div class='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4'></div>
- <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5'></div>
- <div class='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div>
+ )}
+ {isLoading &&
+ itemLoading.map((item) => (
+ <div key={item} role='status' className='max-w-sm animate-pulse'>
+ <div className='flex items-center space-x-4 mb- 2'>
+ <div className='flex-shrink-0'>
+ <PhotoIcon className='h-16 w-16 text-gray-500' />
+ </div>
+ <div className='flex-1 min-w-0'>
+ <div className='h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4'></div>
+ <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px] mb-2.5'></div>
+ <div className='h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5'></div>
+ </div>
+ </div>
+ </div>
+ ))}
+ {auth && products.length === 0 && !isLoading && (
+ <div className='justify-center p-4'>
+ <p className='text-gray-500 text-center '>
+ Tidak Ada Produk di Keranjang Belanja Anda
+ </p>
</div>
- </div>
- </div>
- ))}
- {auth && products.length === 0 && !isLoading && (
- <div className='justify-center p-4'>
- <p className='text-gray-500 text-center '>
- Tidak Ada Produk di Keranjang Belanja Anda
- </p>
- </div>
- )}
- {auth && products.length > 0 && !isLoading && (
- <>
- <ul role='list' class='divide-y divide-gray-200 dark:divide-gray-700'>
- {products &&
- products?.map((product, index) => (
- <>
- <li class='py-1 sm:py-2'>
- <div class='flex items-center space-x-4'>
- <div class='flex-shrink-0'>
- <Link
- href={createSlug(
- '/shop/product/',
- product?.parent.name,
- product?.parent.id
- )}
- className='line-clamp-2 leading-6 !text-gray_r-12 font-normal'
- >
- <Image
- src={product?.parent?.image}
- alt={product?.name}
- className='object-contain object-center border border-gray_r-6 h-16 w-16 rounded-md'
- />
- </Link>
- </div>
- <div class='flex-1 min-w-0'>
- <Link
- href={createSlug(
- '/shop/product/',
- product?.parent.name,
- product?.parent.id
- )}
- className='line-clamp-2 leading-6 !text-gray_r-12 font-normal'
- >
- {' '}
- <p class='text-caption-2 font-medium text-gray-900 truncate dark:text-white'>
- {product.parent.name}
- </p>
- </Link>
-
- {product?.hasFlashsale && (
- <div className='flex gap-x-1 items-center mb-2 mt-1'>
- <div className='badge-solid-red'>
- {product?.price?.discountPercentage}%
+ )}
+ {auth && products.length > 0 && !isLoading && (
+ <>
+ <ul role='list' className='divide-y divide-gray-200 dark:divide-gray-700'>
+ {products &&
+ products?.map((product, index) => (
+ <>
+ <li className='py-1 sm:py-2'>
+ <div className='flex items-center space-x-4'>
+ <div className='flex-shrink-0'>
+ <Link
+ href={createSlug(
+ '/shop/product/',
+ product?.parent.name,
+ product?.parent.id
+ )}
+ className='line-clamp-2 leading-6 !text-gray_r-12 font-normal'
+ >
+ <Image
+ src={product?.parent?.image}
+ alt={product?.name}
+ className='object-contain object-center border border-gray_r-6 h-16 w-16 rounded-md'
+ />
+ </Link>
</div>
- <div className='text-gray_r-11 line-through text-caption-2'>
- {currencyFormat(product?.price?.price)}
+ <div className='flex-1 min-w-0'>
+ <Link
+ href={createSlug(
+ '/shop/product/',
+ product?.parent.name,
+ product?.parent.id
+ )}
+ className='line-clamp-2 leading-6 !text-gray_r-12 font-normal'
+ >
+ {' '}
+ <p className='text-caption-2 font-medium text-gray-900 truncate dark:text-white'>
+ {product.parent.name}
+ </p>
+ </Link>
+
+ {product?.hasFlashsale && (
+ <div className='flex gap-x-1 items-center mb-2 mt-1'>
+ <div className='badge-solid-red'>
+ {product?.price?.discountPercentage}%
+ </div>
+ <div className='text-gray_r-11 line-through text-caption-2'>
+ {currencyFormat(product?.price?.price)}
+ </div>
+ </div>
+ )}
+ <div className='flex justify-between items-center'>
+ <div className='font-semibold text-sm text-red-600'>
+ {product?.price?.priceDiscount > 0 ? (
+ currencyFormat(product?.price?.priceDiscount)
+ ) : (
+ <span className='text-gray_r-12/90 font-normal text-caption-1'>
+ <a
+ href={whatsappUrl('product', {
+ name: product.name,
+ manufacture: product.manufacture?.name,
+ url: createSlug(
+ '/shop/product/',
+ product.name,
+ product.id,
+ true
+ )
+ })}
+ className='text-danger-500 underline'
+ rel='noopener noreferrer'
+ target='_blank'
+ >
+ Call For Price
+ </a>
+ </span>
+ )}
+ </div>
+ </div>
</div>
</div>
- )}
- <div className='flex justify-between items-center'>
- <div className='font-semibold text-sm text-red-600'>
- {product?.price?.priceDiscount > 0 ? (
- currencyFormat(product?.price?.priceDiscount)
- ) : (
- <span className='text-gray_r-12/90 font-normal text-caption-1'>
- <a
- href={whatsappUrl('product', {
- name: product.name,
- manufacture: product.manufacture?.name,
- url: createSlug(
- '/shop/product/',
- product.name,
- product.id,
- true
- )
- })}
- className='text-danger-500 underline'
- rel='noopener noreferrer'
- target='_blank'
- >
- Call for Inquiry
- </a>
- </span>
- )}
- </div>
- </div>
- </div>
- </div>
- </li>
- </>
- ))}
- </ul>
- <hr />
- </>
- )}
- </div>
- {auth && products.length > 0 && !isLoading && (
- <>
- <div className='mt-3'>
- <span className='text-gray-400 text-caption-2'>Subtotal Sebelum PPN : </span>
- <span className='font-semibold text-red-600'>{currencyFormat(subTotal)}</span>
- </div>
- <div className='mt-5 mb-2'>
- <button
- type='button'
- className='btn-solid-red rounded-lg w-full'
- onClick={handleCheckout}
- disabled={buttonLoading}
- >
- {buttonLoading ? 'Loading...' : 'Lanjutkan Ke Pembayaran'}
- </button>
- </div>
- </>
- )}
- </div>
- </div>
+ </li>
+ </>
+ ))}
+ </ul>
+ <hr />
+ </>
+ )}
+ </div>
+ {auth && products.length > 0 && !isLoading && (
+ <>
+ <div className='mt-3'>
+ <span className='text-gray-400 text-caption-2'>Subtotal Sebelum PPN : </span>
+ <span className='font-semibold text-red-600'>{currencyFormat(subTotal)}</span>
+ </div>
+ <div className='mt-5 mb-2'>
+ <button
+ type='button'
+ className='btn-solid-red rounded-lg w-full'
+ onClick={handleCheckout}
+ disabled={buttonLoading}
+ >
+ {buttonLoading ? 'Loading...' : 'Lanjutkan Ke Pembayaran'}
+ </button>
+ </div>
+ </>
+ )}
+ </motion.div>
+ </motion.div>
+ </>
+ )}
+ </AnimatePresence>
</div>
)
}
diff --git a/src/lib/category/components/Breadcrumb.jsx b/src/lib/category/components/Breadcrumb.jsx
new file mode 100644
index 00000000..127904ee
--- /dev/null
+++ b/src/lib/category/components/Breadcrumb.jsx
@@ -0,0 +1,56 @@
+import odooApi from '@/core/api/odooApi'
+import { createSlug } from '@/core/utils/slug'
+import {
+ Breadcrumb as ChakraBreadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ Skeleton
+} from '@chakra-ui/react'
+import Link from 'next/link'
+import React from 'react'
+import { useQuery } from 'react-query'
+
+/**
+ * Render a breadcrumb component.
+ *
+ * @param {object} categoryId - The ID of the category.
+ * @return {JSX.Element} The breadcrumb component.
+ */
+const Breadcrumb = ({ categoryId }) => {
+ const breadcrumbs = useQuery(
+ `category-breadcrumbs/${categoryId}`,
+ async () => await odooApi('GET', `/api/v1/category/${categoryId}/category-breadcrumb`)
+ )
+
+ return (
+ <div className='container mx-auto py-4 md:py-6'>
+ <Skeleton isLoaded={!breadcrumbs.isLoading} className='w-2/3'>
+ <ChakraBreadcrumb>
+ <BreadcrumbItem>
+ <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'>
+ Home
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+
+ {breadcrumbs.data?.map((category, index) => (
+ <BreadcrumbItem key={index} isCurrentPage={index === breadcrumbs.data.length - 1}>
+ {index === breadcrumbs.data.length - 1 ? (
+ <BreadcrumbLink className='whitespace-nowrap'>{category.name}</BreadcrumbLink>
+ ) : (
+ <BreadcrumbLink
+ as={Link}
+ href={createSlug('/shop/category/', category.name, category.id)}
+ className='!text-danger-500 whitespace-nowrap'
+ >
+ {category.name}
+ </BreadcrumbLink>
+ )}
+ </BreadcrumbItem>
+ ))}
+ </ChakraBreadcrumb>
+ </Skeleton>
+ </div>
+ )
+}
+
+export default Breadcrumb
diff --git a/src/lib/category/components/Category.jsx b/src/lib/category/components/Category.jsx
index af696d42..e6ea5acf 100644
--- a/src/lib/category/components/Category.jsx
+++ b/src/lib/category/components/Category.jsx
@@ -30,7 +30,7 @@ const Category = () => {
return (
<DesktopView>
<div className='category-mega-box'>
- {categories.map((category) => (
+ {categories?.map((category) => (
<div key={category.id}>
<Link
href={createSlug('/shop/category/', category.name, category.id)}
diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx
index 85221e5d..9a799010 100644
--- a/src/lib/checkout/components/Checkout.jsx
+++ b/src/lib/checkout/components/Checkout.jsx
@@ -5,7 +5,6 @@ import useAuth from '@/core/hooks/useAuth'
import { getItemAddress } from '@/core/utils/address'
import addressesApi from '@/lib/address/api/addressesApi'
import {
- ArrowLongLeftIcon,
BanknotesIcon,
ChevronLeftIcon,
ClockIcon,
@@ -17,22 +16,21 @@ import { deleteItemCart, getCartApi } from '@/core/utils/cart'
import currencyFormat from '@/core/utils/currencyFormat'
import { toast } from 'react-hot-toast'
import getFileBase64 from '@/core/utils/getFileBase64'
-// import checkoutApi from '../api/checkoutApi'
import { useRouter } from 'next/router'
import VariantGroupCard from '@/lib/variant/components/VariantGroupCard'
import axios from 'axios'
import Image from '@/core/components/elements/Image/Image'
-import imageNext from 'next/image'
import MobileView from '@/core/components/views/MobileView'
import DesktopView from '@/core/components/views/DesktopView'
import ExpedisiList from '../api/ExpedisiList'
import whatsappUrl from '@/core/utils/whatsappUrl'
-import { createSlug } from '@/core/utils/slug'
import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
import { useQuery } from 'react-query'
import { gtagPurchase } from '@/core/utils/googleTag'
import { findVoucher, getVoucher } from '../api/getVoucher'
import CardProdcuctsList from '@/core/components/elements/Product/cartProductsList'
+import { Spinner } from '@chakra-ui/react'
+import { AnimatePresence, motion } from 'framer-motion'
const SELF_PICKUP_ID = 32
@@ -110,6 +108,7 @@ const Checkout = () => {
const [buttonTerapkan, SetButtonTerapkan] = useState(false)
const [checkoutValidation, setCheckoutValidation] = useState(false)
const [loadingVoucher, setLoadingVoucher] = useState(true)
+ const [loadingRajaOngkir, setLoadingRajaOngkir] = useState(false)
const expedisiValidation = useRef(null)
@@ -169,6 +168,16 @@ const Checkout = () => {
setExpedisi(dataExpedisi)
}
loadExpedisi()
+
+ const handlePopState = () => {
+ router.push('/shop/cart')
+ }
+
+ window.onpopstate = handlePopState
+
+ return () => {
+ window.onpopstate = null
+ }
// voucher()
}, [])
@@ -211,6 +220,7 @@ const Checkout = () => {
useEffect(() => {
setCheckoutValidation(false)
const loadServiceRajaOngkir = async () => {
+ setLoadingRajaOngkir(true)
const body = {
origin: 2127,
destination: selectedAddress.shipping.rajaongkirCityId,
@@ -221,6 +231,7 @@ const Checkout = () => {
}
setBiayaKirim(0)
const dataService = await axios('/api/rajaongkir-service?body=' + JSON.stringify(body))
+ setLoadingRajaOngkir(false)
setListServiceExpedisi(dataService.data[0].costs)
if (dataService.data[0].costs[0]) {
setBiayaKirim(dataService.data[0].costs[0]?.cost[0].value)
@@ -259,9 +270,8 @@ const Checkout = () => {
useEffect(() => {
if (selectedExpedisi) {
let serviceType = selectedExpedisi.split(',')
- if (serviceType[0] === 0) {
- setSelectedExpedisi(0)
- }
+ if (serviceType[0] === 0) return
+
setselectedCarrier(serviceType[0])
setselectedCarrierId(serviceType[1])
setListServiceExpedisi([])
@@ -723,6 +733,7 @@ const Checkout = () => {
checkWeigth={checkWeigth}
checkoutValidation={checkoutValidation}
expedisiValidation={expedisiValidation}
+ loadingRajaOngkir={loadingRajaOngkir}
/>
<Divider />
<SectionListService
@@ -980,6 +991,7 @@ const Checkout = () => {
checkWeigth={checkWeigth}
checkoutValidation={checkoutValidation}
expedisiValidation={expedisiValidation}
+ loadingRajaOngkir={loadingRajaOngkir}
/>
<Divider />
<SectionListService
@@ -990,7 +1002,7 @@ const Checkout = () => {
<div className='p-4'>
<div className='font-medium'>Detail Pesanan</div>
<CardProdcuctsList isLoading={isLoading} products={products} />
-
+
{/* <table className='table-checkout'>
<thead>
<tr>
@@ -1158,7 +1170,9 @@ const Checkout = () => {
<div className='sticky top-48 border border-gray_r-6 bg-white rounded p-4'>
<div className='flex justify-between items-center'>
<div className='font-medium'>Ringkasan Pesanan</div>
- <div className='text-gray_r-11 text-caption-1'>{products?.length} Barang</div>
+ <div className='text-gray_r-11 text-caption-1'>
+ {products?.length} Barang - {cartCheckout?.totalWeight.kg} Kg
+ </div>
</div>
<hr className='my-4 border-gray_r-6' />
@@ -1403,32 +1417,51 @@ const SectionExpedisi = ({
setSelectedExpedisi,
checkWeigth,
checkoutValidation,
- expedisiValidation
+ expedisiValidation,
+ loadingRajaOngkir
}) =>
address?.rajaongkirCityId > 0 && (
<div className='p-4' ref={expedisiValidation}>
<div className='flex justify-between items-center'>
- <div className='font-medium'>Pilih Expedisi : </div>
+ <div className='font-medium'>Pilih Ekspedisi: </div>
<div className='w-[250px]'>
- <select
- className={`form-input ${checkoutValidation ? 'border-red-500 shake' : ''}`}
- onChange={(e) => setSelectedExpedisi(e.target.value)}
- required
- >
- <option value='0,0'>Pilih Pengiriman</option>
- <option value='1,32'>SELF PICKUP</option>
- {checkWeigth != true &&
- listExpedisi.map((expedisi) => (
- <option
- disabled={checkWeigth}
- value={expedisi.label + ',' + expedisi.carrierId}
- key={expedisi.value}
+ <div className='flex items-center gap-x-4'>
+ <select
+ className={`form-input ${checkoutValidation ? 'border-red-500 shake' : ''}`}
+ onChange={(e) => setSelectedExpedisi(e.target.value)}
+ required
+ >
+ <option value='0,0'>Pilih Pengiriman</option>
+ <option value='1,32'>SELF PICKUP</option>
+ {checkWeigth != true &&
+ listExpedisi.map((expedisi) => (
+ <option
+ disabled={checkWeigth}
+ value={expedisi.label + ',' + expedisi.carrierId}
+ key={expedisi.value}
+ >
+ {' '}
+ {expedisi.label.toUpperCase()}{' '}
+ </option>
+ ))}
+ </select>
+
+ <AnimatePresence>
+ {loadingRajaOngkir && (
+ <motion.div
+ initial={{ opacity: 0, width: 0 }}
+ animate={{ opacity: 1, width: '28px' }}
+ exit={{ opacity: 0, width: 0 }}
+ transition={{
+ duration: 0.25
+ }}
+ className='overflow-hidden'
>
- {' '}
- {expedisi.label.toUpperCase()}{' '}
- </option>
- ))}
- </select>
+ <Spinner thickness='3px' speed='0.5s' color='red.500' />
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </div>
{checkoutValidation && (
<span className='text-sm text-red-500'>*silahkan pilih expedisi</span>
)}
@@ -1459,7 +1492,7 @@ const SectionListService = ({ listserviceExpedisi, setSelectedServiceType }) =>
<>
<div className='p-4'>
<div className='flex justify-between items-center'>
- <div className='font-medium'>Service Type Expedisi : </div>
+ <div className='font-medium'>Tipe Layanan Ekspedisi: </div>
<div>
<select className='form-input' onChange={(e) => setSelectedServiceType(e.target.value)}>
{listserviceExpedisi.map((service) => (
diff --git a/src/lib/flashSale/components/FlashSale.jsx b/src/lib/flashSale/components/FlashSale.jsx
index 87545d8d..3d5c4e0e 100644
--- a/src/lib/flashSale/components/FlashSale.jsx
+++ b/src/lib/flashSale/components/FlashSale.jsx
@@ -63,7 +63,7 @@ const FlashSaleProduct = ({ flashSaleId }) => {
useEffect(() => {
const loadProducts = async () => {
const dataProducts = await productSearchApi({
- query: `fq=flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500`,
+ query: `fq=flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500&orderBy=flashsale-price-asc`,
operation: 'AND'
})
setProducts(dataProducts.response)
diff --git a/src/lib/product/components/Product/Breadcrumb.jsx b/src/lib/product/components/Product/Breadcrumb.jsx
new file mode 100644
index 00000000..0554dba5
--- /dev/null
+++ b/src/lib/product/components/Product/Breadcrumb.jsx
@@ -0,0 +1,69 @@
+import odooApi from '@/core/api/odooApi'
+import { createSlug } from '@/core/utils/slug'
+import {
+ Breadcrumb as ChakraBreadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ Skeleton
+} from '@chakra-ui/react'
+import classNames from 'classnames'
+import Link from 'next/link'
+import { useQuery } from 'react-query'
+
+/**
+ * Renders a breadcrumb component based on the provided `productId`.
+ *
+ * @param {Object} props - The properties passed to the component.
+ * @param {number} props.productId - The ID of the product.
+ * @param {string} props.productName - The ID of the product.
+ * @return {ReactElement} The rendered breadcrumb component.
+ */
+const Breadcrumb = ({ productId, productName }) => {
+ const categories = useQuery(
+ `detail/categories/${productId}`,
+ async () => await odooApi('GET', `/api/v1/product/${productId}/category-breadcrumb`),
+ {
+ enabled: !!productId
+ }
+ )
+
+ return (
+ <Skeleton
+ isLoaded={!categories.isLoading}
+ className={classNames({
+ 'w-2/3': categories.isLoading,
+ 'w-full': !categories.isLoading
+ })}
+ >
+ <ChakraBreadcrumb
+ mb={10}
+ overflowX={'auto'}
+ className='text-caption-2 md:text-body-2 p-4 md:p-0'
+ >
+ <BreadcrumbItem>
+ <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'>
+ Home
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+
+ {categories.data?.map((category) => (
+ <BreadcrumbItem key={category.id}>
+ <BreadcrumbLink
+ as={Link}
+ href={createSlug('/shop/category/', category.name, category.id)}
+ className='!text-danger-500 whitespace-nowrap'
+ >
+ {category.name}
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+ ))}
+
+ <BreadcrumbItem isCurrentPage>
+ <BreadcrumbLink className='whitespace-nowrap'>{productName}</BreadcrumbLink>
+ </BreadcrumbItem>
+ </ChakraBreadcrumb>
+ </Skeleton>
+ )
+}
+
+export default Breadcrumb
diff --git a/src/lib/product/components/Product/Product.jsx b/src/lib/product/components/Product/Product.jsx
index 54490c26..6e983c2e 100644
--- a/src/lib/product/components/Product/Product.jsx
+++ b/src/lib/product/components/Product/Product.jsx
@@ -36,29 +36,21 @@ const Product = ({ product, isVariant = false }) => {
}
}, [product, isVariant])
- if (isVariant == true) {
- return (
- <>
- <ProductDesktopVariant
- product={product}
- wishlist={wishlist}
- toggleWishlist={toggleWishlist}
- />
- <ProductMobileVariant
- product={product}
- wishlist={wishlist}
- toggleWishlist={toggleWishlist}
- />
- </>
- )
- } else {
- return (
- <>
- <ProductMobile product={product} wishlist={wishlist} toggleWishlist={toggleWishlist} />
- <ProductDesktop products={product} wishlist={wishlist} toggleWishlist={toggleWishlist} />
- </>
- )
- }
+ return isVariant == true ? (
+ <>
+ <ProductDesktopVariant
+ product={product}
+ wishlist={wishlist}
+ toggleWishlist={toggleWishlist}
+ />
+ <ProductMobileVariant product={product} wishlist={wishlist} toggleWishlist={toggleWishlist} />
+ </>
+ ) : (
+ <>
+ <ProductMobile product={product} wishlist={wishlist} toggleWishlist={toggleWishlist} />
+ <ProductDesktop products={product} wishlist={wishlist} toggleWishlist={toggleWishlist} />
+ </>
+ )
}
export default Product
diff --git a/src/lib/product/components/Product/ProductDesktop.jsx b/src/lib/product/components/Product/ProductDesktop.jsx
index aa771eab..11c34009 100644
--- a/src/lib/product/components/Product/ProductDesktop.jsx
+++ b/src/lib/product/components/Product/ProductDesktop.jsx
@@ -24,6 +24,8 @@ import ColumnsSLA from './ColumnsSLA'
import { useProductCartContext } from '@/contexts/ProductCartContext'
import { Box, Skeleton, Tooltip } from '@chakra-ui/react'
import { Info } from 'lucide-react'
+import Breadcrumb from './Breadcrumb'
+import { sellingProductFormat } from '@/core/utils/formatValue'
const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
const router = useRouter()
@@ -89,38 +91,38 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
}
const updateCart = (variantId, quantity, source) => {
- let dataUpdate
+ let dataUpdate = {
+ productId: variantId,
+ quantity,
+ selected: true,
+ source: source === 'buy' ? 'buy' : null
+ }
+
if (product.variants.length > 1) {
let variantIndex = product.variants.findIndex((varian) => varian.id == variantId)
- dataUpdate = {
- productId: variantId,
- quantity,
- programLineId: product.variants[variantIndex].programActive,
- selected: true,
- source: source === 'buy' ? 'buy' : null
- }
+ dataUpdate['programLineId'] = product.variants[variantIndex].programActive
} else {
- dataUpdate = {
- productId: variantId,
- quantity,
- programLineId: promotionActiveId,
- selected: true,
- source: source === 'buy' ? 'buy' : null
- }
+ dataUpdate['programLineId'] = promotionActiveId
}
+
updateItemCart(dataUpdate)
}
- const handleAddToCart = (variantId) => {
- if (!auth) {
- router.push(`/login?next=/shop/product/${slug}`)
- return
- }
+ const redirectToLogin = (action, variantId, quantity) => {
+ const nextURL = `/shop/product/${slug}?action=${action}&variantId=${variantId}&qty=${quantity}`
+ router.push(`/login?next=${encodeURIComponent(nextURL)}`)
+ return true
+ }
+ const handleAddToCart = (variantId) => {
const quantity = variantQuantityRefs.current[variantId].value
if (!validQuantity(quantity)) return
+ if (!auth) {
+ return redirectToLogin('add_to_cart', variantId, quantity)
+ }
+
let source = 'cart'
updateCart(variantId, quantity, source)
setRefreshCart(true)
@@ -140,6 +142,10 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
const quantity = variantQuantityRefs.current[variant].value
if (!validQuantity(quantity)) return
+ if (!auth) {
+ return redirectToLogin('buy', variant, quantity)
+ }
+
let source = 'buy'
updateCart(variant, quantity, source)
router.push(`/shop/checkout?source=buy`)
@@ -199,47 +205,49 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
return (
<DesktopView>
<div className='container mx-auto pt-10'>
+ <Breadcrumb productId={product.id} productName={product.name} />
<div className='flex'>
<div className='w-full flex flex-wrap'>
<div className='w-5/12'>
<div className='relative mb-2'>
- {product?.flashSale?.remainingTime > 0 && lowestPrice?.price.discountPercentage > 0 && (
- <div className={`absolute bottom-0 w-full`}>
- <div className='absolute bottom-0 w-full h-full'>
- <ImageNext
- src={backgorundFlashSale || '/images/GAMBAR-BG-FLASH-SALE.jpg'}
- width={1000}
- height={100}
- />
- </div>
- <div className='relative'>
- <div className='flex gap-x-2 items-center p-2'>
- <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '>
- <span className='text-lg font-bold'>
- {Math.floor(product.lowestPrice.discountPercentage)}%
- </span>
- </div>
- <div
- className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-4`}
- >
- <ImageNext
- src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg'
- width={17}
- height={10}
- />
- <span className='text-white text-lg font-semibold'>
- {product?.flashSale?.tag != 'false' || product?.flashSale?.tag
- ? product?.flashSale?.tag
- : 'FLASH SALE'}
- </span>
- </div>
- <div>
- <CountDown2 initialTime={product.flashSale.remainingTime} />
+ {product?.flashSale?.remainingTime > 0 &&
+ lowestPrice?.price.discountPercentage > 0 && (
+ <div className={`absolute bottom-0 w-full`}>
+ <div className='absolute bottom-0 w-full h-full'>
+ <ImageNext
+ src={backgorundFlashSale || '/images/GAMBAR-BG-FLASH-SALE.jpg'}
+ width={1000}
+ height={100}
+ />
+ </div>
+ <div className='relative'>
+ <div className='flex gap-x-2 items-center p-2'>
+ <div className='bg-yellow-400 rounded-full p-1 h-9 w-20 flex items-center justify-center '>
+ <span className='text-lg font-bold'>
+ {Math.floor(product.lowestPrice.discountPercentage)}%
+ </span>
+ </div>
+ <div
+ className={`bg-red-600 border border-solid border-yellow-400 rounded-full h-9 p-2 flex w-[50%] items-center justify-center gap-x-4`}
+ >
+ <ImageNext
+ src='/images/ICON_FLASH_SALE_WEBSITE_INDOTEKNIK.svg'
+ width={17}
+ height={10}
+ />
+ <span className='text-white text-lg font-semibold'>
+ {product?.flashSale?.tag != 'false' || product?.flashSale?.tag
+ ? product?.flashSale?.tag
+ : 'FLASH SALE'}
+ </span>
+ </div>
+ <div>
+ <CountDown2 initialTime={product.flashSale.remainingTime} />
+ </div>
</div>
</div>
</div>
- </div>
- )}
+ )}
<Image
src={product.image}
alt={product.name}
@@ -390,7 +398,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
))}
</div>
<div className='flex'>
- <div className='w-3/4 leading-7 product__description'>
+ <div className='w-3/4 leading-8 product__description'>
<TabContent active={informationTab == 'description'}>
<span
dangerouslySetInnerHTML={{
@@ -411,7 +419,7 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
</div>
</div>
- <div className='w-[25%]'>
+ <div className='w-[30%]'>
{product.variants.length > 1 && product.lowestPrice.priceDiscount > 0 && (
<div className='text-gray_r-12/80'>Harga mulai dari: </div>
)}
@@ -440,7 +448,11 @@ const ProductDesktop = ({ products, wishlist, toggleWishlist }) => {
)}
</div>
)} */}
-
+ {product?.qtySold > 0 && (
+ <div className='text-gray_r-9'>
+ {sellingProductFormat(product?.qtySold) + ' Terjual'}
+ </div>
+ )}
{lowestPrice?.isFlashsale && lowestPrice?.price.discountPercentage > 0 ? (
<>
<div className='flex gap-x-1 items-center mt-2'>
diff --git a/src/lib/product/components/Product/ProductMobile.jsx b/src/lib/product/components/Product/ProductMobile.jsx
index ffa75f72..ef2c0002 100644
--- a/src/lib/product/components/Product/ProductMobile.jsx
+++ b/src/lib/product/components/Product/ProductMobile.jsx
@@ -19,9 +19,14 @@ import { gtagAddToCart } from '@/core/utils/googleTag'
import odooApi from '@/core/api/odooApi'
import ImageNext from 'next/image'
import CountDown2 from '@/core/components/elements/CountDown/CountDown2'
+import Breadcrumb from './Breadcrumb'
+import useAuth from '@/core/hooks/useAuth'
+import { sellingProductFormat } from '@/core/utils/formatValue'
const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
const router = useRouter()
+ const auth = useAuth()
+ const { slug } = router.query
const [quantity, setQuantity] = useState('1')
const [selectedVariant, setSelectedVariant] = useState(null)
@@ -58,7 +63,8 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
price: getLowestPrice(),
stock: product.stockTotal,
weight: product.weight,
- hasProgram: false
+ hasProgram: false,
+ qtySold: product.qtySold
})
const variantOptions = product.variants?.map((variant) => {
@@ -101,7 +107,8 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
stock: variant.stock,
weight: variant.weight,
hasProgram: variant.hasProgram,
- isFlashsale: variant.isFlashsale
+ isFlashsale: variant.isFlashsale,
+ qtySold: variant.qtySold
}
setActiveVariant(newActiveVariant)
@@ -127,9 +134,20 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
return isValid
}
+ const redirectToLogin = (action) => {
+ const nextURL = `/shop/product/${slug}?action=${action}&variantId=${activeVariant.id}&qty=${quantity}`
+ router.push(`/login?next=${encodeURIComponent(nextURL)}`)
+ return true
+ }
+
const handleClickCart = () => {
if (!validAction()) return
gtagAddToCart(activeVariant, quantity)
+
+ if (!auth) {
+ return redirectToLogin('add_to_cart')
+ }
+
updateItemCart({
productId: activeVariant.id,
quantity,
@@ -142,6 +160,10 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
const handleClickBuy = () => {
if (!validAction()) return
+ if (!auth) {
+ return redirectToLogin('buy')
+ }
+
updateItemCart({
productId: activeVariant.id,
quantity,
@@ -160,6 +182,7 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
return (
<MobileView>
+ <Breadcrumb productId={product.id} productName={product.name} />
<div className='relative'>
{product?.flashSale?.remainingTime > 0 && activeVariant?.price.discountPercentage > 0 && (
<div className={`absolute bottom-0 w-full`}>
@@ -225,7 +248,9 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
</button>
</div>
<h1 className='leading-6 font-medium mb-3'>{activeVariant?.name}</h1>
-
+ {product?.qtySold > 0 && (
+ <div className='text-gray_r-9'>{sellingProductFormat(activeVariant?.qtySold) + ' Terjual'}</div>
+ )}
{product.variants.length > 1 &&
activeVariant.price.priceDiscount > 0 &&
!selectedVariant && (
diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx
index bc2174e4..9500a3fd 100644
--- a/src/lib/product/components/ProductCard.jsx
+++ b/src/lib/product/components/ProductCard.jsx
@@ -1,6 +1,7 @@
import Image from '@/core/components/elements/Image/Image'
import Link from '@/core/components/elements/Link/Link'
import currencyFormat from '@/core/utils/currencyFormat'
+import { sellingProductFormat } from '@/core/utils/formatValue'
import { createSlug } from '@/core/utils/slug'
import whatsappUrl from '@/core/utils/whatsappUrl'
import ImageNext from 'next/image'
@@ -82,9 +83,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
)}
<Link
href={createSlug('/shop/product/', product?.name, product?.id)}
- className={`mb-3 !text-gray_r-12 leading-6 block ${
- simpleTitle ? 'line-clamp-2 h-12' : 'line-clamp-3 h-[64px]'
- }`}
+ className={`mb-2 !text-gray_r-12 leading-6 block line-clamp-3 h-[64px]`}
title={product?.name}
>
{product?.name}
@@ -123,12 +122,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
</div>
)}
- {product?.stockTotal > 0 && (
- <div className='flex gap-x-1'>
- <div className='badge-solid-red'>Ready Stock</div>
- <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div>
- </div>
- )}
+ <div className='flex w-full items-center gap-x-1 '>
+ {product?.stockTotal > 0 && <div className='badge-solid-red'>Ready Stock</div>}
+ {/* <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div> */}
+ {product?.qtySold > 0 && <div className='text-gray_r-9 text-[11px]'>{sellingProductFormat(product?.qtySold) + ' Terjual'}</div>}
+ </div>
</div>
</div>
)
@@ -186,9 +184,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
)}
<Link
href={createSlug('/shop/product/', product?.name, product?.id)}
- className={`mb-3 !text-gray_r-12 leading-6 ${
- simpleTitle ? 'line-clamp-2' : 'line-clamp-3'
- }`}
+ className={`mb-3 !text-gray_r-12 leading-6 line-clamp-3`}
>
{product?.name}
</Link>
@@ -229,12 +225,11 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => {
</div>
)}
- {product?.stockTotal > 0 && (
- <div className='flex gap-x-1'>
- <div className='badge-solid-red'>Ready Stock</div>
- <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div>
- </div>
- )}
+ <div className='flex w-full items-center gap-x-1 '>
+ {product?.stockTotal > 0 && <div className='badge-solid-red'>Ready Stock</div>}
+ {/* <div className='badge-gray'>{product?.stockTotal > 5 ? '> 5' : '< 5'}</div> */}
+ {product?.qtySold > 0 && <div className='text-gray_r-9 text-[11px]'>{sellingProductFormat(product?.qtySold) + ' Terjual'}</div>}
+ </div>
</div>
</div>
)
diff --git a/src/lib/product/components/ProductFilter.jsx b/src/lib/product/components/ProductFilter.jsx
index 34357526..d52fcb90 100644
--- a/src/lib/product/components/ProductFilter.jsx
+++ b/src/lib/product/components/ProductFilter.jsx
@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
import { useState } from 'react'
import _ from 'lodash'
import { toQuery } from 'lodash-contrib'
+import { Checkbox } from '@chakra-ui/react'
const orderOptions = [
{ value: 'price-asc', label: 'Harga Terendah' },
@@ -14,12 +15,51 @@ const orderOptions = [
const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBrand = null }) => {
const router = useRouter()
const { query } = router
- const [order, setOrder] = useState(query?.orderBy)
+ const [order, setOrder] = useState(query?.orderBy || 'popular')
const [brand, setBrand] = useState(query?.brand)
const [category, setCategory] = useState(query?.category)
const [priceFrom, setPriceFrom] = useState(query?.priceFrom)
const [priceTo, setPriceTo] = useState(query?.priceTo)
+ const [stock, setStock] = useState(query?.stock)
+
+ const [activeRange, setActiveRange] = useState(null)
+
+ const priceRange = [
+ {
+ priceFrom: 100000,
+ priceTo: 200000
+ },
+ {
+ priceFrom: 200000,
+ priceTo: 300000
+ },
+ {
+ priceFrom: 300000,
+ priceTo: 400000
+ },
+ {
+ priceFrom: 400000,
+ priceTo: 500000
+ }
+ ]
+
+ const handlePriceFromChange = async (priceFromr, priceTor, index) => {
+ await setPriceFrom(priceFromr)
+ await setPriceTo(priceTor)
+ setActiveRange(index)
+ }
+
+ const handleReadyStockChange = (event) => {
+ const value = event.target.value
+ const isChecked = event.target.checked
+ if (isChecked) {
+ setStock(value)
+ } else {
+ setStock(null)
+ }
+ }
+
const handleSubmit = () => {
let params = {
q: router.query.q,
@@ -27,17 +67,27 @@ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBr
brand,
category,
priceFrom,
- priceTo
+ priceTo,
+ stock: stock
}
params = _.pickBy(params, _.identity)
params = toQuery(params)
router.push(`${prefixUrl}?${params}`)
}
+ const formatCurrency = (value) => {
+ if (value >= 1000) {
+ const thousands = Math.floor(value / 1000) // Menghitung ribuan
+ return `Rp${thousands}k`
+ } else {
+ return `Rp${value}`
+ }
+ }
+
return (
<BottomPopup active={active} close={close} title='Filter Produk'>
<div className='flex flex-col gap-y-4'>
- {!defaultBrand && (
+ {!router.pathname.includes('brands') && !defaultBrand && (
<div>
<label>Brand</label>
<select
@@ -46,15 +96,22 @@ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBr
value={brand}
onChange={(e) => setBrand(e.target.value)}
>
- <option value=''>Pilih Brand...</option>
- {brands.map((brand, index) => (
- <option value={brand} key={index}>
- {brand}
- </option>
- ))}
+ {brands.length > 0 ? (
+ <>
+ <option value=''>Pilih Brand...</option>
+ {brands.map((brand, index) => (
+ <option value={brand.brand} key={index}>
+ {brand.brand} <span className='text-sm text-gray-200'>({brand.qty})</span>
+ </option>
+ ))}
+ </>
+ ) : (
+ <option value=''>Brands tidak tersedia</option>
+ )}
</select>
</div>
)}
+
<div>
<label>Kategori</label>
<select
@@ -63,12 +120,18 @@ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBr
value={category}
onChange={(e) => setCategory(e.target.value)}
>
- <option value=''>Pilih Kategori...</option>
- {categories.map((category, index) => (
- <option value={category} key={index}>
- {category}
- </option>
- ))}
+ {categories.length > 0 ? (
+ <>
+ <option value=''>Pilih Kategori...</option>
+ {categories.map((category, index) => (
+ <option value={category.name} key={index}>
+ {category.name} <span className='text-sm text-gray-200'>({category.qty})</span>
+ </option>
+ ))}
+ </>
+ ) : (
+ <option value=''>Kategori tidak tersedia</option>
+ )}
</select>
</div>
<div>
@@ -106,7 +169,34 @@ const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBr
onChange={(e) => setPriceTo(e.target.value)}
/>
</div>
+ <div className='grid grid-cols-2 gap-x-3 gap-y-2 mt-2'>
+ {priceRange.map((price, i) => (
+ <button
+ key={i}
+ onClick={() => handlePriceFromChange(price.priceFrom, price.priceTo, i)}
+ className={`w-full border ${
+ i === activeRange ? 'border-red-600' : 'border-gray-400'
+ }
+ py-2 p-3 rounded-full text-sm whitespace-nowrap`}
+ >
+ {formatCurrency(price.priceFrom)} - {formatCurrency(price.priceTo)}
+ </button>
+ ))}
+ </div>
</div>
+ {/* <div>
+ <label>Ketersedian Stok</label>
+ <div className='mt-2'>
+ <Checkbox
+ isChecked={stock === 'ready stock'}
+ onChange={handleReadyStockChange}
+ value={'ready stock'}
+ size='md'
+ >
+ Ketersedian Stok
+ </Checkbox>
+ </div>
+ </div> */}
<button type='button' className='btn-solid-red w-full mt-2' onClick={handleSubmit}>
Terapkan Filter
</button>
diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx
index b64349c7..6118ed6b 100644
--- a/src/lib/product/components/ProductFilterDesktop.jsx
+++ b/src/lib/product/components/ProductFilterDesktop.jsx
@@ -1,5 +1,5 @@
import { useRouter } from 'next/router'
-import { useState } from 'react'
+import { useEffect, useState } from 'react'
import _ from 'lodash'
import { toQuery } from 'lodash-contrib'
import {
@@ -17,6 +17,7 @@ import {
Stack,
VStack
} from '@chakra-ui/react'
+import Image from '@/core/components/elements/Image/Image'
const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = null }) => {
const router = useRouter()
@@ -26,6 +27,32 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu
const [categoryValues, setCategory] = useState(query?.category?.split(',') || [])
const [priceFrom, setPriceFrom] = useState(query?.priceFrom)
const [priceTo, setPriceTo] = useState(query?.priceTo)
+ const [stock, setStock] = useState(query?.stock)
+ const [activeRange, setActiveRange] = useState(null)
+ const [activeIndeces, setActiveIndeces] = useState([])
+
+ const priceRange = [
+ {
+ priceFrom: 100000,
+ priceTo: 200000
+ },
+ {
+ priceFrom: 200000,
+ priceTo: 300000
+ },
+ {
+ priceFrom: 300000,
+ priceTo: 400000
+ },
+ {
+ priceFrom: 400000,
+ priceTo: 500000
+ }
+ ]
+
+ const indexRange = priceRange.findIndex((range) => {
+ return range.priceFrom === parseInt(priceFrom) && range.priceTo == parseInt(priceTo)
+ })
const handleCategoriesChange = (event) => {
const value = event.target.value
@@ -46,6 +73,22 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu
}
}
+ const handleReadyStockChange = (event) => {
+ const value = event.target.value
+ const isChecked = event.target.checked
+ if (isChecked) {
+ setStock(value)
+ } else {
+ setStock(null)
+ }
+ }
+
+ const handlePriceFromChange = async (priceFromr, priceTor, index) => {
+ await setPriceFrom(priceFromr)
+ await setPriceTo(priceTor)
+ setActiveRange(index)
+ }
+
const handleSubmit = () => {
let params = {
q: router.query.q,
@@ -53,41 +96,80 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu
brand: brandValues.join(','),
category: categoryValues.join(','),
priceFrom,
- priceTo
+ priceTo,
+ stock: stock
}
params = _.pickBy(params, _.identity)
params = toQuery(params)
router.push(`${prefixUrl}?${params}`)
}
+ const formatCurrency = (value) => {
+ if (value >= 1000) {
+ const thousands = Math.floor(value / 1000) // Menghitung ribuan
+ return `Rp${thousands}k`
+ } else {
+ return `Rp${value}`
+ }
+ }
+
+ /*const handleIndexAccordion = async () => {
+ if (brandValues) {
+ await setActiveIndeces([...activeIndeces, 0])
+ }
+ if (categoryValues) {
+ await setActiveIndeces([...activeIndeces, !router.pathname.includes('brands') ? 1 : 0])
+ }
+ if (priceRange) {
+ await setActiveIndeces([...activeIndeces, !router.pathname.includes('brands') ? 2 : 1])
+ }
+ if (stock) {
+ await setActiveIndeces([...activeIndeces, !router.pathname.includes('brands') ? 3 : 2])
+ }
+ }*/
+
+ useEffect(() => {
+ setActiveRange(indexRange)
+ }, [])
+
+
return (
<>
<Accordion defaultIndex={[0]} allowMultiple>
- <AccordionItem>
- <AccordionButton padding={[2, 4]}>
- <Box as='span' flex='1' textAlign='left' fontWeight='semibold'>
- Brand
- </Box>
- <AccordionIcon />
- </AccordionButton>
+ {!router.pathname.includes('brands') && (
+ <AccordionItem>
+ <AccordionButton padding={[2, 4]}>
+ <Box as='span' flex='1' textAlign='left' fontWeight='semibold'>
+ Brand
+ </Box>
+ <AccordionIcon />
+ </AccordionButton>
- <AccordionPanel>
- <Stack gap={3} direction='column' maxH={'240px'} overflow='auto'>
- {brands.map((brand, index) => (
- <div className='flex items-center gap-2' key={index}>
- <Checkbox
- isChecked={brandValues.includes(brand)}
- onChange={handleBrandsChange}
- value={brand}
- size='md'
- >
- {brand}
- </Checkbox>
- </div>
- ))}
- </Stack>
- </AccordionPanel>
- </AccordionItem>
+ <AccordionPanel>
+ <Stack gap={3} direction='column' maxH={'240px'} overflow='auto'>
+ {brands && brands.length > 0 ? (
+ brands.map((brand, index) => (
+ <div className='flex items-center gap-2 ' key={index}>
+ <Checkbox
+ isChecked={brandValues.includes(brand.brand)}
+ onChange={handleBrandsChange}
+ value={brand.brand}
+ size='md'
+ >
+ <div className='flex items-center gap-2'>
+ <span>{brand.brand} </span>
+ <span className='text-sm text-gray-600'>({brand.qty})</span>
+ </div>
+ </Checkbox>
+ </div>
+ ))
+ ) : (
+ <div className='flex items-center gap-2'>Brands tidak tersedia</div>
+ )}
+ </Stack>
+ </AccordionPanel>
+ </AccordionItem>
+ )}
<AccordionItem>
<AccordionButton padding={[2, 4]}>
@@ -99,18 +181,25 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu
<AccordionPanel>
<Stack gap={3} direction='column' maxH={'240px'} overflow='auto'>
- {categories.map((category, index) => (
- <div className='flex items-center gap-2' key={index}>
- <Checkbox
- isChecked={categoryValues.includes(category)}
- onChange={handleCategoriesChange}
- value={category}
- size='md'
- >
- {category}
- </Checkbox>
- </div>
- ))}
+ {categories && categories.length > 0 ? (
+ categories.map((category, index) => (
+ <div className='flex items-center gap-2' key={index}>
+ <Checkbox
+ isChecked={categoryValues.includes(category.name)}
+ onChange={handleCategoriesChange}
+ value={category.name}
+ size='md'
+ >
+ <div className='flex items-center gap-2'>
+ <span>{category.name} </span>
+ <span className='text-sm text-gray-600'>({category.qty})</span>
+ </div>
+ </Checkbox>
+ </div>
+ ))
+ ) : (
+ <div className='flex items-center gap-2'>Kategori tidak tersedia</div>
+ )}
</Stack>
</AccordionPanel>
</AccordionItem>
@@ -143,9 +232,43 @@ const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = nu
onChange={(e) => setPriceTo(e.target.value)}
/>
</InputGroup>
+ <div className='grid grid-cols-2 gap-x-3 gap-y-2'>
+ {priceRange.map((price, i) => (
+ <button
+ key={i}
+ onClick={() => handlePriceFromChange(price.priceFrom, price.priceTo, i)}
+ className={`w-full border ${
+ i === activeRange ? 'border-red-600' : 'border-gray-400'
+ }
+ py-2 p-3 rounded-full text-sm whitespace-nowrap`}
+ >
+ {formatCurrency(price.priceFrom)} - {formatCurrency(price.priceTo)}
+ </button>
+ ))}
+ </div>
</VStack>
</AccordionPanel>
</AccordionItem>
+
+ {/* <AccordionItem>
+ <AccordionButton padding={[2, 4]}>
+ <Box as='span' flex='1' textAlign='left' fontWeight='semibold'>
+ Ketersedian Stok
+ </Box>
+ <AccordionIcon />
+ </AccordionButton>
+
+ <AccordionPanel paddingY={4}>
+ <Checkbox
+ isChecked={stock === 'ready stock'}
+ onChange={handleReadyStockChange}
+ value={'ready stock'}
+ size='md'
+ >
+ Ketersedian Stock
+ </Checkbox>
+ </AccordionPanel>
+ </AccordionItem> */}
</Accordion>
<Button className='w-full mt-6' colorScheme='red' onClick={handleSubmit}>
diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx
index df9aa91b..9d59b305 100644
--- a/src/lib/product/components/ProductSearch.jsx
+++ b/src/lib/product/components/ProductSearch.jsx
@@ -15,22 +15,27 @@ import { useRouter } from 'next/router'
import searchSpellApi from '@/core/api/searchSpellApi'
import Link from '@/core/components/elements/Link/Link'
import whatsappUrl from '@/core/utils/whatsappUrl'
+import { Image } from '@chakra-ui/react'
+import odooApi from '@/core/api/odooApi'
const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
const router = useRouter()
const { page = 1 } = query
const [q, setQ] = useState(query?.q || '*')
+ const [limit, setLimit] = useState(query?.limit || 30)
+ const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular')
if (defaultBrand) query.brand = defaultBrand.toLowerCase()
- const { productSearch } = useProductSearch({ query: { ...query, q } })
+ const { productSearch } = useProductSearch({ query: { ...query, q, limit, orderBy } })
const [products, setProducts] = useState(null)
const [spellings, setSpellings] = useState(null)
+ const [bannerPromotionHeader, setBannerPromotionHeader] = useState(null)
+ const [bannerPromotionFooter, setBannerPromotionFooter] = useState(null)
const popup = useActive()
+ const numRows = [30, 50, 80, 100]
- const pageCount = Math.ceil(
- productSearch.data?.response.numFound / productSearch.data?.responseHeader.params.rows
- )
+ const pageCount = Math.ceil(productSearch.data?.response.numFound / limit)
const productStart = productSearch.data?.responseHeader.params.start
- const productRows = productSearch.data?.responseHeader.params.rows
+ const productRows = limit
const productFound = productSearch.data?.response.numFound
useEffect(() => {
@@ -63,20 +68,44 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
}
}, [productFound, query, spellings])
- const brands = productSearch.data?.facetCounts?.facetFields?.manufactureName?.filter(
+ const brands = []
+ for (
+ let i = 0;
+ i < productSearch.data?.facetCounts?.facetFields?.manufactureNameS.length;
+ i += 2
+ ) {
+ const brand = productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i]
+ const qty = productSearch.data?.facetCounts?.facetFields?.manufactureNameS[i + 1]
+ if (qty > 0) {
+ brands.push({ brand, qty })
+ }
+ }
+ /*const brandsList = productSearch.data?.facetCounts?.facetFields?.manufactureName?.filter(
(value, index) => {
if (index % 2 === 0) {
- return true
+ const brand = value
+ const qty = index + 1
+ brands.push({ brand, qty })
}
}
- )
- const categories = productSearch.data?.facetCounts?.facetFields?.categoryName?.filter(
+ )*/
+
+ const categories = []
+ for (let i = 0; i < productSearch.data?.facetCounts?.facetFields?.categoryName.length; i += 2) {
+ const name = productSearch.data?.facetCounts?.facetFields?.categoryName[i]
+ const qty = productSearch.data?.facetCounts?.facetFields?.categoryName[i + 1]
+ if (qty > 0) {
+ categories.push({ name, qty })
+ }
+ }
+
+ /*const categories = productSearch.data?.facetCounts?.facetFields?.categoryName?.filter(
(value, index) => {
if (index % 2 === 0) {
return true
}
}
- )
+ )*/
const orderOptions = [
{ value: 'price-asc', label: 'Harga Terendah' },
@@ -95,6 +124,30 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
router.push(`${prefixUrl}?${params}`)
}
+ const handleLimit = (e) => {
+ let params = {
+ ...router.query,
+ limit: e.target.value
+ }
+ params = _.pickBy(params, _.identity)
+ params = toQuery(params)
+ router.push(`${prefixUrl}?${params}`)
+ }
+ const getBanner = async () => {
+ if (router.pathname.includes('search')) {
+ const getBannerHeader = await odooApi('GET', '/api/v1/banner?type=promotion-header')
+ const getBannerFooter = await odooApi('GET', '/api/v1/banner?type=promotion-footer')
+ var randomIndex = Math.floor(Math.random() * getBannerHeader.length)
+ var randomIndexFooter = Math.floor(Math.random() * getBannerFooter.length)
+ setBannerPromotionHeader(getBannerHeader[randomIndex])
+ setBannerPromotionFooter(getBannerFooter[randomIndexFooter])
+ }
+ }
+
+ useEffect(() => {
+ getBanner()
+ }, [])
+
useEffect(() => {
setProducts(productSearch.data?.response?.products)
}, [productSearch])
@@ -117,7 +170,7 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
<>
<MobileView>
{productSearch.isLoading && <ProductSearchSkeleton />}
- <div className='p-4'>
+ <div className='p-4 pt-0'>
<h1 className='mb-2 font-semibold text-h-sm'>Produk</h1>
<div className='mb-2 leading-6 text-gray_r-11'>
@@ -127,9 +180,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
{pageCount > 1 ? (
<>
{productStart + 1}-
- {productStart + productRows > productFound
+ {parseInt(productStart) + parseInt(productRows) > productFound
? productFound
- : productStart + productRows}
+ : parseInt(productStart) + parseInt(productRows)}
&nbsp;dari&nbsp;
</>
) : (
@@ -149,9 +202,28 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
</div>
{productFound > 0 && (
- <button className='btn-light mb-6 py-2 px-5' onClick={popup.activate}>
- Filter
- </button>
+ <div className='flex items-center gap-x-2 mb-5 justify-between'>
+ <div>
+ <button className='btn-light py-2 px-5 h-[40px]' onClick={popup.activate}>
+ Filter
+ </button>
+ </div>
+ <div className=''>
+ <select
+ name='limit'
+ className='form-input w-24'
+ value={router.query?.limit || ''}
+ onChange={(e) => handleLimit(e)}
+ >
+ {numRows.map((option, index) => (
+ <option key={index} value={option}>
+ {' '}
+ {option}{' '}
+ </option>
+ ))}
+ </select>
+ </div>
+ </div>
)}
<div className='grid grid-cols-2 gap-3'>
@@ -178,7 +250,7 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
</MobileView>
<DesktopView>
- <div className='container mx-auto mt-10 flex mb-3'>
+ <div className='container mx-auto flex mb-3'>
<div className='w-3/12'>
<ProductFilterDesktop
brands={brands || []}
@@ -188,6 +260,16 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
/>
</div>
<div className='w-9/12 pl-6'>
+ {bannerPromotionHeader && bannerPromotionHeader?.image && (
+ <div className='mb-3'>
+ <Image
+ src={bannerPromotionHeader?.image}
+ alt=''
+ className='object-cover object-center h-full mx-auto'
+ />
+ </div>
+ )}
+
<h1 className='text-2xl mb-2 font-semibold'>Hasil Pencarian</h1>
<div className='flex justify-between items-center mb-2'>
<div className='mb-2 leading-6 text-gray_r-11'>
@@ -197,9 +279,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
{pageCount > 1 ? (
<>
{productStart + 1}-
- {productStart + productRows > productFound
+ {parseInt(productStart) + parseInt(productRows) > productFound
? productFound
- : productStart + productRows}
+ : parseInt(productStart) + parseInt(productRows)}
&nbsp;dari&nbsp;
</>
) : (
@@ -222,10 +304,9 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
<select
name='urutan'
className='form-input mt-2'
- value={router.query?.orderBy || ''}
+ value={orderBy}
onChange={(e) => handleOrderBy(e)}
>
- <option value=''>Urutkan</option>
{orderOptions.map((option, index) => (
<option key={index} value={option.value}>
{' '}
@@ -234,6 +315,21 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
))}
</select>
</div>
+ <div className='ml-3'>
+ <select
+ name='limit'
+ className='form-input mt-2'
+ value={router.query?.limit || ''}
+ onChange={(e) => handleLimit(e)}
+ >
+ {numRows.map((option, index) => (
+ <option key={index} value={option}>
+ {' '}
+ {option}{' '}
+ </option>
+ ))}
+ </select>
+ </div>
</div>
</div>
{productSearch.isLoading && <ProductSearchSkeleton />}
@@ -275,6 +371,15 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
className='!justify-end'
/>
</div>
+ {bannerPromotionFooter && bannerPromotionFooter?.image && (
+ <div className='mb-3'>
+ <Image
+ src={bannerPromotionFooter?.image}
+ alt=''
+ className='object-cover object-center h-full mx-auto'
+ />
+ </div>
+ )}
</div>
</div>
</DesktopView>
diff --git a/src/lib/promotinProgram/components/PromotionType.jsx b/src/lib/promotinProgram/components/PromotionType.jsx
index ad7185e3..51f2622a 100644
--- a/src/lib/promotinProgram/components/PromotionType.jsx
+++ b/src/lib/promotinProgram/components/PromotionType.jsx
@@ -24,7 +24,7 @@ const PromotionType = ({
const id = variantId
const listProgram = async () => {
const programs = await getPromotionProgram({ id })
- if (programs.length > 0) {
+ if (programs?.length > 0) {
setPromotionList(programs)
setActiveTitle(programs?.[0].type.value)
}
diff --git a/src/lib/variant/components/VariantCard.jsx b/src/lib/variant/components/VariantCard.jsx
index 6b06bc31..9f1b5733 100644
--- a/src/lib/variant/components/VariantCard.jsx
+++ b/src/lib/variant/components/VariantCard.jsx
@@ -40,7 +40,7 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => {
{product.attributes.length > 0 ? ` ・ ${product.attributes.join(', ')}` : ''}
</p>
<p className='text-caption-2 text-gray_r-11 mt-1'>
- Berat Item : {product?.weight} Kg
+ Berat Item : {product?.weight} Kg x {product?.quantity} Barang
</p>
<div className='flex flex-wrap gap-x-1 items-center mt-auto'>
{product.hasFlashsale && (
diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js
index b5161365..b4d67c5d 100644
--- a/src/pages/api/shop/search.js
+++ b/src/pages/api/shop/search.js
@@ -13,19 +13,20 @@ export default async function handler(req, res) {
orderBy = '',
operation = 'AND',
fq = '',
- limit = 30
+ limit = 30,
+ stock = ''
} = req.query
let paramOrderBy = ''
switch (orderBy) {
case 'price-asc':
- paramOrderBy += 'price_discount_f ASC'
+ paramOrderBy += 'price_tier1_v2_f ASC'
break
case 'price-desc':
- paramOrderBy += 'price_discount_f DESC'
+ paramOrderBy += 'price_tier1_v2_f DESC'
break
case 'popular':
- paramOrderBy += 'search_rank_i DESC'
+ paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,'
break
case 'popular-weekly':
paramOrderBy += 'search_rank_weekly_i DESC'
@@ -33,6 +34,9 @@ export default async function handler(req, res) {
case 'stock':
paramOrderBy += 'stock_total_f DESC'
break
+ case 'flashsale-price-asc':
+ paramOrderBy += 'flashsale_price_f ASC'
+ break
default:
paramOrderBy += 'product_rating_f DESC, price_discount_f DESC'
break
@@ -40,7 +44,7 @@ export default async function handler(req, res) {
let offset = (page - 1) * limit
let parameter = [
- 'facet.field=manufacture_name',
+ 'facet.field=manufacture_name_s',
'facet.field=category_name',
'facet=true',
'indent=true',
@@ -48,7 +52,7 @@ export default async function handler(req, res) {
`q.op=${operation}`,
`q=${escapeSolrQuery(q)}`,
'qf=name_s',
- `start=${offset}`,
+ `start=${parseInt(offset)}`,
`rows=${limit}`,
`sort=${paramOrderBy}`,
`fq=-publish_b:false`
@@ -62,8 +66,9 @@ export default async function handler(req, res) {
)
}
- if (brand) parameter.push(`fq=manufacture_name:${brand}`)
+ if (brand) parameter.push(`fq=manufacture_name:${brand.replace(/,/g, ' OR ')}`)
if (category) parameter.push(`fq=category_name:${category}`)
+ if (stock) parameter.push(`fq=stock_total_f:{1 TO *}`)
// Single fq in url params
if (typeof fq === 'string') parameter.push(`fq=${fq}`)
diff --git a/src/pages/shop/brands/[slug].jsx b/src/pages/shop/brands/[slug].jsx
index 88f19bc0..d75475b7 100644
--- a/src/pages/shop/brands/[slug].jsx
+++ b/src/pages/shop/brands/[slug].jsx
@@ -3,6 +3,7 @@ import { getIdFromSlug, getNameFromSlug } from '@/core/utils/slug'
import { useRouter } from 'next/router'
import _ from 'lodash'
import Seo from '@/core/components/Seo'
+import Breadcrumb from '@/lib/brand/components/Breadcrumb'
const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'))
const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch'))
@@ -26,6 +27,8 @@ export default function BrandDetail() {
]}
/>
+ <Breadcrumb brandName={brandName} />
+
<Brand id={getIdFromSlug(slug)} />
{!_.isEmpty(router.query) && (
<ProductSearch
diff --git a/src/pages/shop/cart.jsx b/src/pages/shop/cart.jsx
index a7f2037b..2da58c96 100644
--- a/src/pages/shop/cart.jsx
+++ b/src/pages/shop/cart.jsx
@@ -3,7 +3,9 @@ import BasicLayout from '@/core/components/layouts/BasicLayout'
import DesktopView from '@/core/components/views/DesktopView'
import MobileView from '@/core/components/views/MobileView'
import IsAuth from '@/lib/auth/components/IsAuth'
+import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'
import dynamic from 'next/dynamic'
+import Link from 'next/link'
const AppLayout = dynamic(() => import('@/core/components/layouts/AppLayout'))
const CartComponent = dynamic(() => import('@/lib/cart/components/Cart'))
@@ -22,6 +24,19 @@ export default function Cart() {
<DesktopView>
<BasicLayout>
+ <div className='container mx-auto py-4 md:py-6 pb-0'>
+ <Breadcrumb>
+ <BreadcrumbItem>
+ <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'>
+ Home
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+
+ <BreadcrumbItem isCurrentPage>
+ <BreadcrumbLink className='whitespace-nowrap'>Keranjang</BreadcrumbLink>
+ </BreadcrumbItem>
+ </Breadcrumb>
+ </div>
<CartComponent />
</BasicLayout>
</DesktopView>
diff --git a/src/pages/shop/category/[slug].jsx b/src/pages/shop/category/[slug].jsx
index dbc17c06..6d3985a8 100644
--- a/src/pages/shop/category/[slug].jsx
+++ b/src/pages/shop/category/[slug].jsx
@@ -3,6 +3,7 @@ import { getIdFromSlug, getNameFromSlug } from '@/core/utils/slug'
import { useRouter } from 'next/router'
import _ from 'lodash'
import Seo from '@/core/components/Seo'
+import Breadcrumb from '@/lib/category/components/Breadcrumb'
const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'))
const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch'))
@@ -13,9 +14,14 @@ export default function CategoryDetail() {
const categoryName = getNameFromSlug(slug)
const categoryId = getIdFromSlug(slug)
+ const q = router?.query.q || null
const query = {
- fq: `manufacture_id_i:${categoryId}`
+ fq: `category_id_i:${categoryId}`
}
+ if (q) {
+ query.q = q
+ }
+
return (
<BasicLayout>
<Seo
@@ -29,6 +35,8 @@ export default function CategoryDetail() {
]}
/>
+ <Breadcrumb categoryId={categoryId} />
+
{!_.isEmpty(router.query) && (
<ProductSearch query={query} prefixUrl={`/shop/category/${slug}`} />
)}
diff --git a/src/pages/shop/product/[slug].jsx b/src/pages/shop/product/[slug].jsx
index 63fb2e7e..d8366d3c 100644
--- a/src/pages/shop/product/[slug].jsx
+++ b/src/pages/shop/product/[slug].jsx
@@ -9,6 +9,7 @@ import cookie from 'cookie'
import axios from 'axios'
import { useProductContext } from '@/contexts/ProductContext'
import { useEffect } from 'react'
+import { updateItemCart } from '@/core/utils/cart'
const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'))
const Product = dynamic(() => import('@/lib/product/components/Product/Product'))
@@ -22,19 +23,16 @@ export async function getServerSideProps(context) {
const authToken = auth?.token || ''
let response = await axios(
- `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-detail?id=` + getIdFromSlug(slug) +'&auth=' + tier
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-detail?id=` +
+ getIdFromSlug(slug) +
+ '&auth=' +
+ tier
)
let product = response.data
// let productSolr = await productApi({ id: getIdFromSlug(slug), headers: { Token: authToken } })
// let productSolr = null
if (product?.length == 1) {
product = product[0]
- const regexHtmlTags = /(<([^>]+)>)/gi
- const regexHtmlTagsExceptP = /<\/?(?!p\b)[^>]*>/g
- product.description = product.description
- .replace(regexHtmlTagsExceptP, ' ')
- .replace(regexHtmlTags, ' ')
- .trim()
} else {
product = null
}
@@ -54,6 +52,27 @@ export default function ProductDetail({ product }) {
}
}, [product, setProduct])
+ useEffect(() => {
+ const { action, variantId, qty } = router.query
+ const addToCart = async () => {
+ const data = {
+ productId: variantId,
+ quantity: qty,
+ selected: true,
+ programLineId: null,
+ source: action
+ }
+ console.log('data dr test', data)
+ await updateItemCart(data)
+ const redirectURL = action === 'buy' ? '/shop/checkout?source=buy' : '/shop/cart'
+ router.push(redirectURL)
+ }
+
+ if (action && variantId && qty) {
+ addToCart()
+ }
+ }, [router])
+
if (!product) return <PageNotFound />
return (
diff --git a/src/pages/shop/search.jsx b/src/pages/shop/search.jsx
index 907465b7..db3b449a 100644
--- a/src/pages/shop/search.jsx
+++ b/src/pages/shop/search.jsx
@@ -2,6 +2,8 @@ import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import _ from 'lodash-contrib'
import Seo from '@/core/components/Seo'
+import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'
+import Link from 'next/link'
const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'))
const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch'))
@@ -13,6 +15,24 @@ export default function Search() {
<BasicLayout>
<Seo title={`Cari produk ${router.query.q || ''} di Indoteknik.com`} />
+ <div className='container mx-auto py-4 md:py-6'>
+ {router.query?.q && (
+ <Breadcrumb>
+ <BreadcrumbItem>
+ <BreadcrumbLink as={Link} href='/' className='!text-danger-500 whitespace-nowrap'>
+ Home
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+
+ <BreadcrumbItem isCurrentPage>
+ <BreadcrumbLink className='whitespace-nowrap'>
+ Cari: {router.query.q || ''}
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+ </Breadcrumb>
+ )}
+ </div>
+
{!_.isEmpty(router.query) && <ProductSearch query={router.query} prefixUrl='/shop/search' />}
</BasicLayout>
)
diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js
index 61816cf8..41d24b53 100644
--- a/src/utils/solrMapping.js
+++ b/src/utils/solrMapping.js
@@ -32,7 +32,8 @@ export const productMappingSolr = (products, pricelist) => {
remainingTime: flashsaleTime(product?.flashsale_end_date_s)?.remainingTime,
name: product?.product?.flashsale_name_s,
tag: product?.flashsale_tag_s || 'FLASH SALE'
- }
+ },
+ qtySold : product?.qty_sold_f || 0
}
if (product.manufacture_id_i && product.manufacture_name_s) {
@@ -85,7 +86,8 @@ export const variantsMappingSolr = (parent, products, pricelist) => {
stockTotal: product.stock_total_f || 0,
weight: product.weight_f || 0,
manufacture: {},
- parent: {}
+ parent: {},
+ qtySold : product?.qty_sold_f || 0
}
if (product.manufacture_id_i && product.manufacture_name_s) {