summaryrefslogtreecommitdiff
path: root/src/lib/product
diff options
context:
space:
mode:
authorIT Fixcomart <it@fixcomart.co.id>2023-03-01 09:18:52 +0000
committerIT Fixcomart <it@fixcomart.co.id>2023-03-01 09:18:52 +0000
commita7abbf4ddc70068620e9f44b74dc162ce2e16ee2 (patch)
tree74f66253717515d364ce74bd8275015c1f829cbc /src/lib/product
parent90e1edab9b6a8ccc09a49fed3addbec2cbc4e4c3 (diff)
parenta1b9b647a6c4bda1f5db63879639d44543f9557e (diff)
Merged in refactor (pull request #1)
Refactor
Diffstat (limited to 'src/lib/product')
-rw-r--r--src/lib/product/api/productApi.js9
-rw-r--r--src/lib/product/api/productSearchApi.js9
-rw-r--r--src/lib/product/api/productSimilarApi.js10
-rw-r--r--src/lib/product/components/Product.jsx315
-rw-r--r--src/lib/product/components/ProductCard.jsx76
-rw-r--r--src/lib/product/components/ProductFilter.jsx132
-rw-r--r--src/lib/product/components/ProductSearch.jsx116
-rw-r--r--src/lib/product/components/ProductSimilar.jsx15
-rw-r--r--src/lib/product/components/ProductSlider.jsx64
-rw-r--r--src/lib/product/components/Skeleton/ProductSearchSkeleton.jsx14
-rw-r--r--src/lib/product/hooks/useProductSearch.js15
-rw-r--r--src/lib/product/hooks/useProductSimilar.js13
12 files changed, 788 insertions, 0 deletions
diff --git a/src/lib/product/api/productApi.js b/src/lib/product/api/productApi.js
new file mode 100644
index 00000000..4fe4cd7d
--- /dev/null
+++ b/src/lib/product/api/productApi.js
@@ -0,0 +1,9 @@
+import odooApi from '@/core/api/odooApi'
+
+const productApi = async ({ id }) => {
+ if (!id) return
+ const dataProduct = await odooApi('GET', `/api/v1/product/${id}`)
+ return dataProduct
+}
+
+export default productApi
diff --git a/src/lib/product/api/productSearchApi.js b/src/lib/product/api/productSearchApi.js
new file mode 100644
index 00000000..b9acd94b
--- /dev/null
+++ b/src/lib/product/api/productSearchApi.js
@@ -0,0 +1,9 @@
+import _ from 'lodash-contrib'
+import axios from 'axios'
+
+const productSearchApi = async ({ query }) => {
+ const dataProductSearch = await axios(`${process.env.SELF_HOST}/api/shop/search?${query}`)
+ return dataProductSearch.data
+}
+
+export default productSearchApi
diff --git a/src/lib/product/api/productSimilarApi.js b/src/lib/product/api/productSimilarApi.js
new file mode 100644
index 00000000..7142fab4
--- /dev/null
+++ b/src/lib/product/api/productSimilarApi.js
@@ -0,0 +1,10 @@
+import axios from 'axios'
+
+const productSimilarApi = async ({ query }) => {
+ const dataProductSimilar = await axios(
+ `${process.env.SELF_HOST}/api/shop/search?q=${query}&page=1&orderBy=popular`
+ )
+ return dataProductSimilar.data.response
+}
+
+export default productSimilarApi
diff --git a/src/lib/product/components/Product.jsx b/src/lib/product/components/Product.jsx
new file mode 100644
index 00000000..9e33316c
--- /dev/null
+++ b/src/lib/product/components/Product.jsx
@@ -0,0 +1,315 @@
+import Badge from '@/core/components/elements/Badge/Badge'
+import Divider from '@/core/components/elements/Divider/Divider'
+import Image from '@/core/components/elements/Image/Image'
+import Link from '@/core/components/elements/Link/Link'
+import currencyFormat from '@/core/utils/currencyFormat'
+import { useEffect, useState } from 'react'
+import Select from 'react-select'
+import ProductSimilar from './ProductSimilar'
+import LazyLoad from 'react-lazy-load'
+import { toast } from 'react-hot-toast'
+import { updateItemCart } from '@/core/utils/cart'
+import useWishlist from '@/lib/wishlist/hooks/useWishlist'
+import { HeartIcon } from '@heroicons/react/24/outline'
+import useAuth from '@/core/hooks/useAuth'
+import { useRouter } from 'next/router'
+import createOrDeleteWishlistApi from '@/lib/wishlist/api/createOrDeleteWishlistApi'
+
+const informationTabOptions = [
+ { value: 'specification', label: 'Spesifikasi' },
+ { value: 'description', label: 'Deskripsi' },
+ { value: 'important', label: 'Info Penting' }
+]
+
+const Product = ({ product }) => {
+ const auth = useAuth()
+ const router = useRouter()
+ const { wishlist } = useWishlist({ productId: product?.id })
+ const [quantity, setQuantity] = useState('1')
+ const [selectedVariant, setSelectedVariant] = useState(null)
+ const [informationTab, setInformationTab] = useState(null)
+
+ const [activeVariant, setActiveVariant] = useState({
+ id: product.id,
+ code: product.code,
+ name: product.name,
+ price: product.lowestPrice,
+ stock: product.stockTotal,
+ weight: product.weight
+ })
+
+ const variantOptions = product.variants?.map((variant) => ({
+ value: variant.id,
+ label:
+ (variant.code ? `[${variant.code}] ` : '') +
+ (variant.attributes.length > 0 ? variant.attributes.join(', ') : product.name)
+ }))
+
+ useEffect(() => {
+ if (!selectedVariant && variantOptions.length == 1) {
+ setSelectedVariant(variantOptions[0])
+ }
+ }, [selectedVariant, variantOptions])
+
+ useEffect(() => {
+ if (selectedVariant) {
+ const variant = product.variants.find((variant) => variant.id == selectedVariant.value)
+ const variantAttributes =
+ variant.attributes.length > 0 ? ' - ' + variant.attributes.join(', ') : ''
+
+ setActiveVariant({
+ id: variant.id,
+ code: variant.code,
+ name: variant.parent.name + variantAttributes,
+ price: variant.price,
+ stock: variant.stock,
+ weight: variant.weight
+ })
+ }
+ }, [selectedVariant, product])
+
+ useEffect(() => {
+ if (!informationTab) {
+ setInformationTab(informationTabOptions[0].value)
+ }
+ }, [informationTab])
+
+ const handleClickCart = () => {
+ if (!selectedVariant) {
+ toast.error('Pilih varian terlebih dahulu')
+ return
+ }
+ if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) {
+ toast.error('Jumlah barang minimal 1')
+ return
+ }
+ updateItemCart({
+ productId: activeVariant.id,
+ quantity
+ })
+ toast.success('Berhasil menambahkan ke keranjang')
+ }
+
+ const toggleWishlist = async () => {
+ if (!auth) {
+ router.push('/login')
+ return
+ }
+ const data = { product_id: product.id }
+ await createOrDeleteWishlistApi({ data })
+ if (wishlist.data.productTotal > 0) {
+ toast.success('Berhasil menghapus dari wishlist')
+ } else {
+ toast.success('Berhasil menambahkan ke wishlist')
+ }
+ wishlist.refetch()
+ }
+
+ return (
+ <>
+ <Image
+ src={product.image}
+ alt={product.name}
+ className='h-72 object-contain object-center w-full border-b border-gray_r-4'
+ />
+
+ <div className='p-4'>
+ <div className='flex items-end mb-2'>
+ {product.manufacture?.name ? (
+ <Link href='/'>{product.manufacture?.name}</Link>
+ ) : (
+ <div>-</div>
+ )}
+ <button
+ type='button'
+ className='ml-auto'
+ onClick={toggleWishlist}
+ >
+ {wishlist.data?.productTotal > 0 ? (
+ <HeartIcon className='w-6 fill-red_r-11 text-red_r-11' />
+ ) : (
+ <HeartIcon className='w-6' />
+ )}
+ </button>
+ </div>
+ <h1 className='leading-6 font-medium'>{activeVariant?.name}</h1>
+ {activeVariant?.price?.discountPercentage > 0 && (
+ <div className='flex gap-x-1 items-center mt-2'>
+ <div className='text-gray_r-11 line-through text-caption-1'>
+ {currencyFormat(activeVariant?.price?.price)}
+ </div>
+ <Badge type='solid-red'>{activeVariant?.price?.discountPercentage}%</Badge>
+ </div>
+ )}
+ <h3 className='text-red_r-11 font-semibold mt-1'>
+ {activeVariant?.price?.priceDiscount > 0 ? (
+ currencyFormat(activeVariant?.price?.priceDiscount)
+ ) : (
+ <span className='text-gray_r-11 leading-6 font-normal'>
+ Hubungi kami untuk dapatkan harga terbaik,&nbsp;
+ <a
+ href='https://wa.me/'
+ className='text-red_r-11 underline'
+ >
+ klik disini
+ </a>
+ </span>
+ )}
+ </h3>
+ </div>
+
+ <Divider />
+
+ <div className='p-4'>
+ <div>
+ <label className='flex justify-between'>
+ Pilih Varian:
+ <span className='text-gray_r-11'>{product?.variantTotal} Varian</span>
+ </label>
+ <Select
+ name='variant'
+ classNamePrefix='form-select'
+ options={variantOptions}
+ className='mt-2'
+ value={selectedVariant}
+ onChange={(option) => setSelectedVariant(option)}
+ isSearchable={product.variantTotal > 10}
+ />
+ </div>
+ <div className='mt-4 mb-2'>Jumlah</div>
+ <div className='flex gap-x-3'>
+ <div className='w-2/12'>
+ <input
+ name='quantity'
+ type='number'
+ className='form-input'
+ value={quantity}
+ onChange={(e) => setQuantity(e.target.value)}
+ />
+ </div>
+ <button
+ type='button'
+ className='btn-yellow flex-1'
+ onClick={handleClickCart}
+ >
+ Keranjang
+ </button>
+ <button
+ type='button'
+ className='btn-solid-red flex-1'
+ >
+ Beli
+ </button>
+ </div>
+ </div>
+
+ <Divider />
+
+ <div className='p-4'>
+ <h2 className='font-semibold'>Informasi Produk</h2>
+ <div className='flex gap-x-4 mt-4 mb-3'>
+ {informationTabOptions.map((option) => (
+ <TabButton
+ value={option.value}
+ key={option.value}
+ active={informationTab == option.value}
+ onClick={() => setInformationTab(option.value)}
+ >
+ {option.label}
+ </TabButton>
+ ))}
+ </div>
+
+ <TabContent
+ active={informationTab == 'specification'}
+ className='rounded border border-gray_r-6 divide-y divide-gray_r-6'
+ >
+ <SpecificationContent label='Jumlah Varian'>
+ <span>{product?.variantTotal} Varian</span>
+ </SpecificationContent>
+ <SpecificationContent label='Nomor SKU'>
+ <span>SKU-{product?.id}</span>
+ </SpecificationContent>
+ <SpecificationContent label='Part Number'>
+ <span>{activeVariant?.code || '-'}</span>
+ </SpecificationContent>
+ <SpecificationContent label='Stok'>
+ {activeVariant?.stock > 0 && (
+ <span className='flex gap-x-1.5'>
+ <div className='badge-solid-red'>Ready Stock</div>
+ <div className='badge-gray'>{activeVariant?.stock > 5 ? '> 5' : '< 5'}</div>
+ </span>
+ )}
+ {activeVariant?.stock == 0 && (
+ <a
+ href='https://wa.me'
+ className='text-red_r-11 font-medium'
+ >
+ Tanya Stok
+ </a>
+ )}
+ </SpecificationContent>
+ <SpecificationContent label='Berat Barang'>
+ {activeVariant?.weight > 0 && <span>{activeVariant?.weight} KG</span>}
+ {activeVariant?.weight == 0 && (
+ <a
+ href='https://wa.me'
+ className='text-red_r-11 font-medium'
+ >
+ Tanya Berat
+ </a>
+ )}
+ </SpecificationContent>
+ </TabContent>
+
+ <TabContent
+ active={informationTab == 'description'}
+ className='leading-6 text-gray_r-11'
+ dangerouslySetInnerHTML={{
+ __html: product.description != '' ? product.description : 'Belum ada deskripsi produk.'
+ }}
+ />
+ </div>
+
+ <Divider />
+
+ <div className='p-4'>
+ <h2 className='font-semibold mb-4'>Kamu Mungkin Juga Suka</h2>
+ <LazyLoad>
+ <ProductSimilar query={product?.name.split(' ').slice(1, 3).join(' ')} />
+ </LazyLoad>
+ </div>
+ </>
+ )
+}
+
+const TabButton = ({ children, active, ...props }) => {
+ const activeClassName = active ? 'text-red_r-11 underline underline-offset-4' : 'text-gray_r-11'
+ return (
+ <button
+ {...props}
+ type='button'
+ className={`font-medium pb-1 ${activeClassName}`}
+ >
+ {children}
+ </button>
+ )
+}
+
+const TabContent = ({ children, active, className, ...props }) => (
+ <div
+ {...props}
+ className={`${active ? 'block' : 'hidden'} ${className}`}
+ >
+ {children}
+ </div>
+)
+
+const SpecificationContent = ({ children, label }) => (
+ <div className='flex justify-between p-3'>
+ <span className='text-gray_r-11'>{label}</span>
+ {children}
+ </div>
+)
+
+export default Product
diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx
new file mode 100644
index 00000000..6b88a3bd
--- /dev/null
+++ b/src/lib/product/components/ProductCard.jsx
@@ -0,0 +1,76 @@
+import Image from '@/core/components/elements/Image/Image'
+import Link from '@/core/components/elements/Link/Link'
+import currencyFormat from '@/core/utils/currencyFormat'
+import { createSlug } from '@/core/utils/slug'
+
+const ProductCard = ({ product, simpleTitle }) => {
+ return (
+ <>
+ <div className='rounded shadow-sm border border-gray_r-4 h-full bg-white'>
+ <Link
+ href={createSlug('/shop/product/', product?.name, product?.id)}
+ className='border-b border-gray_r-4 relative'
+ >
+ <Image
+ src={product?.image}
+ alt={product?.name}
+ className='w-full object-contain object-center h-36'
+ />
+ {product.variantTotal > 1 && (
+ <div className='absolute badge-gray bottom-1.5 left-1.5'>
+ {product.variantTotal} Varian
+ </div>
+ )}
+ </Link>
+ <div className='p-2 pb-3 text-caption-2 leading-5'>
+ {product?.manufacture?.name ? (
+ <Link
+ href={createSlug(
+ '/shop/brands/',
+ product?.manufacture?.name,
+ product?.manufacture.id
+ )}
+ className='mb-1'
+ >
+ {product.manufacture.name}
+ </Link>
+ ) : (
+ <div>-</div>
+ )}
+ <Link
+ href={createSlug('/shop/product/', product?.name, product?.id)}
+ className={`font-medium mb-2 !text-gray_r-12 ${
+ simpleTitle ? 'line-clamp-2' : 'line-clamp-3'
+ }`}
+ >
+ {product?.name}
+ </Link>
+ {product?.lowestPrice?.discountPercentage > 0 && (
+ <div className='flex gap-x-1 mb-1 items-center'>
+ <div className='text-gray_r-11 line-through text-[11px]'>
+ {currencyFormat(product?.lowestPrice?.price)}
+ </div>
+ <div className='badge-solid-red'>{product?.lowestPrice?.discountPercentage}%</div>
+ </div>
+ )}
+
+ <div className='text-red_r-11 font-semibold mb-2'>
+ {product?.lowestPrice?.priceDiscount > 0 ? (
+ currencyFormat(product?.lowestPrice?.priceDiscount)
+ ) : (
+ <a href='https://wa.me/'>Call for price</a>
+ )}
+ </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>
+ </div>
+ </>
+ )
+}
+
+export default ProductCard
diff --git a/src/lib/product/components/ProductFilter.jsx b/src/lib/product/components/ProductFilter.jsx
new file mode 100644
index 00000000..eca95f74
--- /dev/null
+++ b/src/lib/product/components/ProductFilter.jsx
@@ -0,0 +1,132 @@
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup'
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+import _ from 'lodash'
+import { toQuery } from 'lodash-contrib'
+
+const orderOptions = [
+ { value: 'price-asc', label: 'Harga Terendah' },
+ { value: 'price-desc', label: 'Harga Tertinggi' },
+ { value: 'popular', label: 'Populer' },
+ { value: 'stock', label: 'Ready Stock' }
+]
+
+const ProductFilter = ({ active, close, brands, categories, prefixUrl, defaultBrand = null }) => {
+ const router = useRouter()
+ const { query } = router
+ const [order, setOrder] = useState(query?.orderBy)
+ 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 handleSubmit = () => {
+ let params = {
+ q: router.query.q,
+ orderBy: order,
+ brand,
+ category,
+ priceFrom,
+ priceTo
+ }
+ params = _.pickBy(params, _.identity)
+ params = toQuery(params)
+ router.push(`${prefixUrl}?${params}`)
+ }
+
+ return (
+ <BottomPopup
+ active={active}
+ close={close}
+ title='Filter Produk'
+ >
+ <div className='flex flex-col gap-y-4'>
+ {!defaultBrand && (
+ <div>
+ <label>Brand</label>
+ <select
+ name='brand'
+ className='form-input mt-2'
+ value={brand}
+ onChange={(e) => setBrand(e.target.value)}
+ >
+ <option value=''>Pilih Brand...</option>
+ {brands.map((brand, index) => (
+ <option
+ value={brand}
+ key={index}
+ >
+ {brand}
+ </option>
+ ))}
+ </select>
+ </div>
+ )}
+ <div>
+ <label>Kategori</label>
+ <select
+ name='category'
+ className='form-input mt-2'
+ value={category}
+ onChange={(e) => setCategory(e.target.value)}
+ >
+ <option value=''>Pilih Kategori...</option>
+ {categories.map((category, index) => (
+ <option
+ value={category}
+ key={index}
+ >
+ {category}
+ </option>
+ ))}
+ </select>
+ </div>
+ <div>
+ <label>Urutkan</label>
+ <div className='flex mt-2 gap-x-2 overflow-x-auto'>
+ {orderOptions.map((orderOption) => (
+ <button
+ key={orderOption.value}
+ className={`btn-light px-3 font-normal flex-shrink-0 ${
+ order == orderOption.value ? 'bg-yellow_r-10' : 'bg-transparent'
+ }`}
+ onClick={() => setOrder(orderOption.value)}
+ >
+ {orderOption.label}
+ </button>
+ ))}
+ </div>
+ </div>
+ <div>
+ <label>Harga</label>
+ <div className='flex mt-2 gap-x-4 items-center'>
+ <input
+ type='number'
+ className='form-input'
+ placeholder='Dari'
+ value={priceFrom}
+ onChange={(e) => setPriceFrom(e.target.value)}
+ />
+ <span>—</span>
+ <input
+ type='number'
+ className='form-input'
+ placeholder='Sampai'
+ value={priceTo}
+ onChange={(e) => setPriceTo(e.target.value)}
+ />
+ </div>
+ </div>
+ <button
+ type='button'
+ className='btn-solid-red w-full mt-2'
+ onClick={handleSubmit}
+ >
+ Terapkan Filter
+ </button>
+ </div>
+ </BottomPopup>
+ )
+}
+
+export default ProductFilter
diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx
new file mode 100644
index 00000000..52bd5119
--- /dev/null
+++ b/src/lib/product/components/ProductSearch.jsx
@@ -0,0 +1,116 @@
+import { useEffect, useState } from 'react'
+import useProductSearch from '../hooks/useProductSearch'
+import ProductCard from './ProductCard'
+import Pagination from '@/core/components/elements/Pagination/Pagination'
+import { toQuery } from 'lodash-contrib'
+import _ from 'lodash'
+import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton'
+import ProductFilter from './ProductFilter'
+import useActive from '@/core/hooks/useActive'
+
+const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
+ const { page = 1 } = query
+ if (defaultBrand) query.brand = defaultBrand.toLowerCase()
+ const { productSearch } = useProductSearch({ query })
+ const [products, setProducts] = useState(null)
+ const popup = useActive()
+
+ const pageCount = Math.ceil(
+ productSearch.data?.response.numFound / productSearch.data?.responseHeader.params.rows
+ )
+ const productStart = productSearch.data?.responseHeader.params.start
+ const productRows = productSearch.data?.responseHeader.params.rows
+ const productFound = productSearch.data?.response.numFound
+
+ const brands = productSearch.data?.facetCounts?.facetFields?.brandStr?.filter((value, index) => {
+ if (index % 2 === 0) {
+ return true
+ }
+ })
+ const categories = productSearch.data?.facetCounts?.facetFields?.categoryNameStr?.filter(
+ (value, index) => {
+ if (index % 2 === 0) {
+ return true
+ }
+ }
+ )
+
+ useEffect(() => {
+ if (!products) {
+ setProducts(productSearch.data?.response?.products)
+ }
+ }, [query, products, productSearch])
+
+ if (productSearch.isLoading) {
+ return <ProductSearchSkeleton />
+ }
+
+ return (
+ <div className='p-4'>
+ <h1 className='mb-2 font-semibold text-h-sm'>Produk</h1>
+
+ <div className='mb-2 leading-6 text-gray_r-11'>
+ {productFound > 0 ? (
+ <>
+ Menampilkan&nbsp;
+ {pageCount > 1 ? (
+ <>
+ {productStart + 1}-
+ {productStart + productRows > productFound
+ ? productFound
+ : productStart + productRows}
+ &nbsp;dari&nbsp;
+ </>
+ ) : (
+ ''
+ )}
+ {productFound}
+ &nbsp;produk{' '}
+ {query.q && (
+ <>
+ untuk pencarian <span className='font-semibold'>{query.q}</span>
+ </>
+ )}
+ </>
+ ) : (
+ 'Mungkin yang anda cari'
+ )}
+ </div>
+
+ <button
+ className='btn-light mb-6 py-2 px-5'
+ onClick={popup.activate}
+ >
+ Filter
+ </button>
+
+ <div className='grid grid-cols-2 gap-3'>
+ {products &&
+ products.map((product) => (
+ <ProductCard
+ product={product}
+ key={product.id}
+ />
+ ))}
+ </div>
+
+ <Pagination
+ pageCount={pageCount}
+ currentPage={parseInt(page)}
+ url={`${prefixUrl}?${toQuery(_.omit(query, ['page']))}`}
+ className='mt-6 mb-2'
+ />
+
+ <ProductFilter
+ active={popup.active}
+ close={popup.deactivate}
+ brands={brands || []}
+ categories={categories || []}
+ prefixUrl={prefixUrl}
+ defaultBrand={defaultBrand}
+ />
+ </div>
+ )
+}
+
+export default ProductSearch
diff --git a/src/lib/product/components/ProductSimilar.jsx b/src/lib/product/components/ProductSimilar.jsx
new file mode 100644
index 00000000..63a33089
--- /dev/null
+++ b/src/lib/product/components/ProductSimilar.jsx
@@ -0,0 +1,15 @@
+import PopularProductSkeleton from '@/lib/home/components/Skeleton/PopularProductSkeleton'
+import useProductSimilar from '../hooks/useProductSimilar'
+import ProductSlider from './ProductSlider'
+
+const ProductSimilar = ({ query }) => {
+ const { productSimilar } = useProductSimilar({ query })
+
+ if (productSimilar.isLoading) {
+ return <PopularProductSkeleton />
+ }
+
+ return <ProductSlider products={productSimilar.data} />
+}
+
+export default ProductSimilar
diff --git a/src/lib/product/components/ProductSlider.jsx b/src/lib/product/components/ProductSlider.jsx
new file mode 100644
index 00000000..060d4638
--- /dev/null
+++ b/src/lib/product/components/ProductSlider.jsx
@@ -0,0 +1,64 @@
+import { Swiper, SwiperSlide } from 'swiper/react'
+import { FreeMode } from 'swiper'
+import ProductCard from './ProductCard'
+import 'swiper/css'
+import Image from '@/core/components/elements/Image/Image'
+import Link from '@/core/components/elements/Link/Link'
+import { useRef } from 'react'
+
+const bannerClassName =
+ 'absolute rounded-r top-0 left-0 h-full max-w-[52%] idt-transition border border-gray_r-6'
+
+const ProductSlider = ({ products, simpleTitle = false, bannerMode = false }) => {
+ const bannerRef = useRef('')
+
+ const changeBannerOpacity = (swiper) => {
+ if (!bannerMode) return
+ const calculateOpacity = (132 + swiper.translate) / 100
+ bannerRef.current.style = `opacity: ${calculateOpacity > 0 ? calculateOpacity : 0}`
+ }
+
+ return (
+ <>
+ {bannerMode && (
+ <div ref={bannerRef}>
+ <Image
+ src={products.banner.image}
+ alt={products.banner.name}
+ style={{ opacity: 1 }}
+ className={bannerClassName}
+ />
+ </div>
+ )}
+ <Swiper
+ freeMode={{ enabled: true, sticky: false }}
+ slidesPerView={2.2}
+ spaceBetween={8}
+ onSliderMove={changeBannerOpacity}
+ onSlideChangeTransitionEnd={changeBannerOpacity}
+ onSlideChangeTransitionStart={changeBannerOpacity}
+ prefix='product'
+ modules={[FreeMode]}
+ >
+ {bannerMode && (
+ <SwiperSlide>
+ <Link
+ href={products.banner.url}
+ className='w-full h-full block'
+ ></Link>
+ </SwiperSlide>
+ )}
+ {products?.products?.map((product, index) => (
+ <SwiperSlide key={index}>
+ <ProductCard
+ product={product}
+ simpleTitle={simpleTitle}
+ />
+ </SwiperSlide>
+ ))}
+ </Swiper>
+ </>
+ )
+}
+
+export default ProductSlider
diff --git a/src/lib/product/components/Skeleton/ProductSearchSkeleton.jsx b/src/lib/product/components/Skeleton/ProductSearchSkeleton.jsx
new file mode 100644
index 00000000..fa1e175d
--- /dev/null
+++ b/src/lib/product/components/Skeleton/ProductSearchSkeleton.jsx
@@ -0,0 +1,14 @@
+import ProductCardSkeleton from '@/core/components/elements/Skeleton/ProductCardSkeleton'
+
+const ProductSearchSkeleton = () => (
+ <div className='p-4 grid grid-cols-2 gap-4'>
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ </div>
+)
+
+export default ProductSearchSkeleton
diff --git a/src/lib/product/hooks/useProductSearch.js b/src/lib/product/hooks/useProductSearch.js
new file mode 100644
index 00000000..0396caec
--- /dev/null
+++ b/src/lib/product/hooks/useProductSearch.js
@@ -0,0 +1,15 @@
+import { useQuery } from 'react-query'
+import productSearchApi from '../api/productSearchApi'
+import _ from 'lodash-contrib'
+
+const useProductSearch = ({ query }) => {
+ const queryString = _.toQuery(query)
+ const fetchProductSearch = async () => await productSearchApi({ query: queryString })
+ const { data, isLoading } = useQuery(`productSearch-${queryString}`, fetchProductSearch)
+
+ return {
+ productSearch: { data, isLoading }
+ }
+}
+
+export default useProductSearch
diff --git a/src/lib/product/hooks/useProductSimilar.js b/src/lib/product/hooks/useProductSimilar.js
new file mode 100644
index 00000000..d16e4c58
--- /dev/null
+++ b/src/lib/product/hooks/useProductSimilar.js
@@ -0,0 +1,13 @@
+import productSimilarApi from '../api/productSimilarApi'
+import { useQuery } from 'react-query'
+
+const useProductSimilar = ({ query }) => {
+ const fetchProductSimilar = async () => await productSimilarApi({ query })
+ const { data, isLoading } = useQuery(`productSimilar-${query}`, fetchProductSimilar)
+
+ return {
+ productSimilar: { data, isLoading }
+ }
+}
+
+export default useProductSimilar