summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/brand/api/BrandApi.js8
-rw-r--r--src/lib/brand/components/Brand.jsx70
-rw-r--r--src/lib/brand/components/BrandCard.jsx20
-rw-r--r--src/lib/brand/hooks/useBrand.js13
-rw-r--r--src/lib/cart/api/CartApi.js11
-rw-r--r--src/lib/cart/components/Cart.jsx30
-rw-r--r--src/lib/cart/hooks/useCart.js17
-rw-r--r--src/lib/elements/hooks/useBottomPopup.js40
-rw-r--r--src/lib/elements/hooks/useConfirmAlert.js49
-rw-r--r--src/lib/home/api/categoryHomeApi.js8
-rw-r--r--src/lib/home/api/categoryHomeIdApi.js8
-rw-r--r--src/lib/home/api/heroBannerApi.js8
-rw-r--r--src/lib/home/api/popularProductApi.js8
-rw-r--r--src/lib/home/api/preferredBrandApi.js8
-rw-r--r--src/lib/home/components/CategoryHome.jsx28
-rw-r--r--src/lib/home/components/CategoryHomeId.jsx19
-rw-r--r--src/lib/home/components/HeroBanner.jsx50
-rw-r--r--src/lib/home/components/PopularProduct.jsx24
-rw-r--r--src/lib/home/components/PreferredBrand.jsx30
-rw-r--r--src/lib/home/components/Skeleton/PopularProductSkeleton.jsx10
-rw-r--r--src/lib/home/components/Skeleton/PreferredBrandSkeleton.jsx12
-rw-r--r--src/lib/home/hooks/useCategoryHome.js13
-rw-r--r--src/lib/home/hooks/useCategoryHomeId.js13
-rw-r--r--src/lib/home/hooks/useHeroBanner.js13
-rw-r--r--src/lib/home/hooks/usePopularProduct.js13
-rw-r--r--src/lib/home/hooks/usePreferredBrand.js13
-rw-r--r--src/lib/product/api/productApi.js8
-rw-r--r--src/lib/product/api/productSearchApi.js9
-rw-r--r--src/lib/product/api/productSimilarApi.js8
-rw-r--r--src/lib/product/components/Product.jsx276
-rw-r--r--src/lib/product/components/ProductCard.jsx68
-rw-r--r--src/lib/product/components/ProductFilter.jsx131
-rw-r--r--src/lib/product/components/ProductSearch.jsx95
-rw-r--r--src/lib/product/components/ProductSimilar.jsx15
-rw-r--r--src/lib/product/components/ProductSlider.jsx51
-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
38 files changed, 1150 insertions, 89 deletions
diff --git a/src/lib/brand/api/BrandApi.js b/src/lib/brand/api/BrandApi.js
new file mode 100644
index 00000000..15634cc4
--- /dev/null
+++ b/src/lib/brand/api/BrandApi.js
@@ -0,0 +1,8 @@
+import odooApi from "@/core/api/odooApi"
+
+const BrandApi = async ({ id }) => {
+ const dataBrand = await odooApi('GET', `/api/v1/manufacture/${id}`)
+ return dataBrand
+}
+
+export default BrandApi \ No newline at end of file
diff --git a/src/lib/brand/components/Brand.jsx b/src/lib/brand/components/Brand.jsx
new file mode 100644
index 00000000..a42f4c81
--- /dev/null
+++ b/src/lib/brand/components/Brand.jsx
@@ -0,0 +1,70 @@
+import useBrand from "../hooks/useBrand"
+import Image from "@/core/components/elements/Image/Image"
+
+import { Swiper, SwiperSlide } from "swiper/react"
+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"
+
+const swiperBanner = {
+ pagination: { dynamicBullets: true },
+ autoplay: {
+ delay: 6000,
+ disableOnInteraction: false
+ },
+ modules: [Pagination, Autoplay]
+}
+
+const Brand = ({ id }) => {
+ const { brand } = useBrand({ id })
+
+ return (
+ <>
+ <div className="min-h-[200px]">
+ { brand.isLoading && <ImageSkeleton /> }
+ { 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`}
+ className="w-full h-auto"
+ />
+ </SwiperSlide>
+ )) }
+ </Swiper>
+ <div className="p-4 pt-2">
+ <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"
+ />
+ ) }
+ { !brand?.data?.logo && (
+ <div className="bg-red_r-10 text-white text-center text-caption-1 py-2 px-4 rounded w-fit">
+ { brand?.data?.name }
+ </div>
+ ) }
+ </div>
+ </>
+ ) }
+ </div>
+ <Divider />
+ </>
+ )
+}
+
+export default Brand \ No newline at end of file
diff --git a/src/lib/brand/components/BrandCard.jsx b/src/lib/brand/components/BrandCard.jsx
new file mode 100644
index 00000000..8783010e
--- /dev/null
+++ b/src/lib/brand/components/BrandCard.jsx
@@ -0,0 +1,20 @@
+import Image from "@/core/components/elements/Image/Image"
+import Link from "@/core/components/elements/Link/Link"
+import { createSlug } from "@/core/utils/slug"
+
+const BrandCard = ({ brand }) => {
+ return (
+ <Link
+ href={createSlug('/shop/brands/', brand.name, brand.id)}
+ className="py-1 px-2 rounded border border-gray_r-6"
+ >
+ <Image
+ src={brand.logo}
+ alt={brand.name}
+ className="h-10 object-contain object-center"
+ />
+ </Link>
+ )
+}
+
+export default BrandCard \ No newline at end of file
diff --git a/src/lib/brand/hooks/useBrand.js b/src/lib/brand/hooks/useBrand.js
new file mode 100644
index 00000000..be42a44c
--- /dev/null
+++ b/src/lib/brand/hooks/useBrand.js
@@ -0,0 +1,13 @@
+import { useQuery } from "react-query"
+import BrandApi from "../api/BrandApi"
+
+const useBrand = ({ id }) => {
+ const fetchBrand = async () => await BrandApi({ id })
+ const { data, isLoading } = useQuery(`brand-${id}`, fetchBrand)
+
+ return {
+ brand: { data, isLoading }
+ }
+}
+
+export default useBrand \ No newline at end of file
diff --git a/src/lib/cart/api/CartApi.js b/src/lib/cart/api/CartApi.js
new file mode 100644
index 00000000..9a5b5053
--- /dev/null
+++ b/src/lib/cart/api/CartApi.js
@@ -0,0 +1,11 @@
+import odooApi from "@/core/api/odooApi"
+
+const CartApi = async ({ variantIds }) => {
+ if (variantIds) {
+ const dataCart = await odooApi('GET', `/api/v1/product_variant/${variantIds}`)
+ return dataCart
+ }
+ return null
+}
+
+export default CartApi \ No newline at end of file
diff --git a/src/lib/cart/components/Cart.jsx b/src/lib/cart/components/Cart.jsx
new file mode 100644
index 00000000..5f9ae1c0
--- /dev/null
+++ b/src/lib/cart/components/Cart.jsx
@@ -0,0 +1,30 @@
+import Link from "@/core/components/elements/Link/Link"
+import useCart from "../hooks/useCart"
+import Image from "@/core/components/elements/Image/Image"
+
+const Cart = () => {
+ const { cart } = useCart()
+
+ return (
+ <div className="p-4">
+ <div className="flex justify-between mb-4">
+ <h1 className="font-semibold">Daftar Produk Belanja</h1>
+ <Link href="/">Cari Produk Lain</Link>
+ </div>
+ <div className="flex flex-col gap-y-4">
+ { cart.data?.map((product) => (
+ <div key={product.id} className="flex">
+ <div className="w-4/12">
+ <Image src={product?.parent?.image} alt={product?.name} className="object-contain object-center border border-gray_r-6 h-32 w-full rounded-md" />
+ </div>
+ <div className="flex-1 px-2">
+ <div>{ product?.parent?.name }</div>
+ </div>
+ </div>
+ )) }
+ </div>
+ </div>
+ )
+}
+
+export default Cart \ No newline at end of file
diff --git a/src/lib/cart/hooks/useCart.js b/src/lib/cart/hooks/useCart.js
new file mode 100644
index 00000000..44931b8a
--- /dev/null
+++ b/src/lib/cart/hooks/useCart.js
@@ -0,0 +1,17 @@
+import { getCart } from "@/core/utils/cart"
+import { useQuery } from "react-query"
+import _ from "lodash"
+import CartApi from "../api/CartApi"
+
+const useCart = () => {
+ const cart = getCart()
+ const variantIds = _.keys(cart).join(',')
+ const fetchCart = async () => CartApi({ variantIds })
+ const { data, isLoading } = useQuery('cart', fetchCart)
+
+ return {
+ cart: { data, isLoading }
+ }
+}
+
+export default useCart \ No newline at end of file
diff --git a/src/lib/elements/hooks/useBottomPopup.js b/src/lib/elements/hooks/useBottomPopup.js
deleted file mode 100644
index 88b72316..00000000
--- a/src/lib/elements/hooks/useBottomPopup.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { useState } from "react";
-import dynamic from "next/dynamic";
-
-const DynamicBottomPopup = dynamic(() => import('@/components/elements/BottomPopup'));
-
-const useBottomPopup = ({
- title,
- children
-}) => {
- const [ isOpen, setIsOpen ] = useState(false);
- const [ dataPopup, setDataPopup ] = useState(null);
-
- const closePopup = () => {
- setIsOpen(false);
- setDataPopup(null);
- };
- const openPopup = ( data = null ) => {
- setIsOpen(true);
- setDataPopup(data);
- };
-
- const BottomPopup = (
- <DynamicBottomPopup
- title={title}
- active={isOpen}
- closePopup={closePopup}
- >
- { children(dataPopup) }
- </DynamicBottomPopup>
- );
-
- return {
- dataPopup,
- BottomPopup,
- closePopup,
- openPopup
- }
-}
-
-export default useBottomPopup; \ No newline at end of file
diff --git a/src/lib/elements/hooks/useConfirmAlert.js b/src/lib/elements/hooks/useConfirmAlert.js
deleted file mode 100644
index 4975c57d..00000000
--- a/src/lib/elements/hooks/useConfirmAlert.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { useState } from "react";
-import dynamic from "next/dynamic";
-
-const DynamicConfirmAlert = dynamic(() => import('@/components/elements/ConfirmAlert'));
-
-const useConfirmAlert = ({
- title,
- caption,
- closeText,
- submitText,
- onSubmit,
-}) => {
- const [ isOpen, setIsOpen ] = useState(false);
- const [ data, setData ] = useState(null);
-
- const closeConfirmAlert = () => {
- setIsOpen(false);
- setData(null);
- };
- const openConfirmAlert = ( data = null ) => {
- setIsOpen(true);
- setData(data);
- };
- const handleSubmit = async () => {
- await onSubmit(data);
- closeConfirmAlert();
- };
-
- const ConfirmAlert = (
- <DynamicConfirmAlert
- title={title}
- caption={caption}
- closeText={closeText}
- submitText={submitText}
- onClose={closeConfirmAlert}
- onSubmit={handleSubmit}
- show={isOpen}
- />
- );
-
- return {
- isOpen,
- closeConfirmAlert,
- openConfirmAlert,
- ConfirmAlert
- };
-}
-
-export default useConfirmAlert; \ No newline at end of file
diff --git a/src/lib/home/api/categoryHomeApi.js b/src/lib/home/api/categoryHomeApi.js
new file mode 100644
index 00000000..efb31240
--- /dev/null
+++ b/src/lib/home/api/categoryHomeApi.js
@@ -0,0 +1,8 @@
+import odooApi from "@/core/api/odooApi"
+
+const categoryHomeIdApi = async ({ id }) => {
+ const dataCategoryHomeId = await odooApi('GET', `/api/v1/categories_homepage?id=${id}`)
+ return dataCategoryHomeId
+}
+
+export default categoryHomeIdApi \ No newline at end of file
diff --git a/src/lib/home/api/categoryHomeIdApi.js b/src/lib/home/api/categoryHomeIdApi.js
new file mode 100644
index 00000000..d5612195
--- /dev/null
+++ b/src/lib/home/api/categoryHomeIdApi.js
@@ -0,0 +1,8 @@
+import odooApi from "@/core/api/odooApi"
+
+const categoryHomeIdApi = async () => {
+ const dataCategoryHomeIds = await odooApi('GET', '/api/v1/categories_homepage/ids')
+ return dataCategoryHomeIds
+}
+
+export default categoryHomeIdApi \ No newline at end of file
diff --git a/src/lib/home/api/heroBannerApi.js b/src/lib/home/api/heroBannerApi.js
new file mode 100644
index 00000000..7ba84bc6
--- /dev/null
+++ b/src/lib/home/api/heroBannerApi.js
@@ -0,0 +1,8 @@
+import odooApi from "@/core/api/odooApi"
+
+const heroBannerApi = async () => {
+ const dataHeroBanners = await odooApi('GET', '/api/v1/banner?type=index-a-1')
+ return dataHeroBanners
+}
+
+export default heroBannerApi \ No newline at end of file
diff --git a/src/lib/home/api/popularProductApi.js b/src/lib/home/api/popularProductApi.js
new file mode 100644
index 00000000..d7adca83
--- /dev/null
+++ b/src/lib/home/api/popularProductApi.js
@@ -0,0 +1,8 @@
+import axios from "axios"
+
+const popularProductApi = async () => {
+ const dataPopularProducts = await axios(`${process.env.SELF_HOST}/api/shop/search?q=*&page=1&orderBy=popular`)
+ return dataPopularProducts.data.response
+}
+
+export default popularProductApi \ No newline at end of file
diff --git a/src/lib/home/api/preferredBrandApi.js b/src/lib/home/api/preferredBrandApi.js
new file mode 100644
index 00000000..f289f387
--- /dev/null
+++ b/src/lib/home/api/preferredBrandApi.js
@@ -0,0 +1,8 @@
+import odooApi from "@/core/api/odooApi"
+
+const preferredBrandApi = async () => {
+ const dataPreferredBrands = await odooApi('GET', '/api/v1/manufacture?level=prioritas')
+ return dataPreferredBrands
+}
+
+export default preferredBrandApi \ No newline at end of file
diff --git a/src/lib/home/components/CategoryHome.jsx b/src/lib/home/components/CategoryHome.jsx
new file mode 100644
index 00000000..0bca9846
--- /dev/null
+++ b/src/lib/home/components/CategoryHome.jsx
@@ -0,0 +1,28 @@
+import ProductSlider from "@/lib/product/components/ProductSlider"
+import useCategoryHome from "../hooks/useCategoryHome"
+import PopularProductSkeleton from "./Skeleton/PopularProductSkeleton"
+
+const CategoryHome = ({ id }) => {
+ const { categoryHome } = useCategoryHome({ id })
+
+ return (
+ <div className="p-4 relative bg-yellow_r-2">
+ { categoryHome.data ? (
+ <ProductSlider
+ products={{
+ products: categoryHome.data?.[0].products,
+ banner: {
+ image: categoryHome.data?.[0].image,
+ name: categoryHome.data?.[0].name,
+ url: `/shop/search?category=${categoryHome.data?.[0].name}`
+ }
+ }}
+ simpleTitle
+ bannerMode
+ />
+ ) : <PopularProductSkeleton /> }
+ </div>
+ )
+}
+
+export default CategoryHome \ No newline at end of file
diff --git a/src/lib/home/components/CategoryHomeId.jsx b/src/lib/home/components/CategoryHomeId.jsx
new file mode 100644
index 00000000..4cbbd1fc
--- /dev/null
+++ b/src/lib/home/components/CategoryHomeId.jsx
@@ -0,0 +1,19 @@
+import { LazyLoadComponent } from "react-lazy-load-image-component"
+import useCategoryHomeId from "../hooks/useCategoryHomeId"
+import CategoryHome from "./CategoryHome"
+
+const CategoryHomeId = () => {
+ const { categoryHomeIds } = useCategoryHomeId()
+
+ return (
+ <div className="flex flex-col gap-y-6">
+ { categoryHomeIds.data?.map((id) => (
+ <LazyLoadComponent key={id}>
+ <CategoryHome id={id} />
+ </LazyLoadComponent>
+ )) }
+ </div>
+ )
+}
+
+export default CategoryHomeId \ No newline at end of file
diff --git a/src/lib/home/components/HeroBanner.jsx b/src/lib/home/components/HeroBanner.jsx
new file mode 100644
index 00000000..604ca8ac
--- /dev/null
+++ b/src/lib/home/components/HeroBanner.jsx
@@ -0,0 +1,50 @@
+import ImageSkeleton from "@/core/components/elements/Skeleton/ImageSkeleton"
+import useHeroBanner from "../hooks/useHeroBanner"
+import Image from "@/core/components/elements/Image/Image"
+
+// Swiper
+import { Swiper, SwiperSlide } from "swiper/react"
+import { Pagination, Autoplay } from "swiper"
+import "swiper/css"
+import "swiper/css/pagination"
+import "swiper/css/autoplay"
+
+const swiperBanner = {
+ pagination: { dynamicBullets: true },
+ autoplay: {
+ delay: 6000,
+ disableOnInteraction: false
+ },
+ modules: [Pagination, Autoplay]
+}
+
+const HeroBanner = () => {
+ const { heroBanners } = useHeroBanner()
+
+ return (
+ <div className="min-h-[200px]">
+ { heroBanners.isLoading && <ImageSkeleton /> }
+ { !heroBanners.isLoading && (
+ <Swiper
+ slidesPerView={1}
+ pagination={swiperBanner.pagination}
+ modules={swiperBanner.modules}
+ autoplay={swiperBanner.autoplay}
+ className="border-b border-gray_r-6"
+ >
+ { heroBanners.data?.map((banner, index) => (
+ <SwiperSlide key={index}>
+ <Image
+ src={banner.image}
+ alt={banner.name}
+ className="w-full h-auto"
+ />
+ </SwiperSlide>
+ )) }
+ </Swiper>
+ ) }
+ </div>
+ )
+}
+
+export default HeroBanner \ No newline at end of file
diff --git a/src/lib/home/components/PopularProduct.jsx b/src/lib/home/components/PopularProduct.jsx
new file mode 100644
index 00000000..87e47218
--- /dev/null
+++ b/src/lib/home/components/PopularProduct.jsx
@@ -0,0 +1,24 @@
+import { Swiper, SwiperSlide } from "swiper/react"
+import usePopularProduct from "../hooks/usePopularProduct"
+import ProductCard from "@/lib/product/components/ProductCard"
+import PopularProductSkeleton from "./Skeleton/PopularProductSkeleton"
+import ProductSlider from "@/lib/product/components/ProductSlider"
+
+const PopularProduct = () => {
+ const { popularProducts } = usePopularProduct()
+
+ return (
+ <div className="px-4">
+ <div className="font-medium mb-4">Produk Populer</div>
+ { popularProducts.isLoading && <PopularProductSkeleton /> }
+ { !popularProducts.isLoading && (
+ <ProductSlider
+ products={popularProducts.data}
+ simpleTitle
+ />
+ ) }
+ </div>
+ )
+}
+
+export default PopularProduct \ No newline at end of file
diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx
new file mode 100644
index 00000000..3d3b1b69
--- /dev/null
+++ b/src/lib/home/components/PreferredBrand.jsx
@@ -0,0 +1,30 @@
+import { Swiper, SwiperSlide } from "swiper/react"
+import usePreferredBrand from "../hooks/usePreferredBrand"
+import PreferredBrandSkeleton from "./Skeleton/PreferredBrandSkeleton"
+import BrandCard from "@/lib/brand/components/BrandCard"
+
+const PreferredBrand = () => {
+ const { preferredBrands } = usePreferredBrand()
+
+ return (
+ <div className="px-4">
+ <div className="font-medium mb-4">Brand Pilihan</div>
+ { preferredBrands.isLoading && <PreferredBrandSkeleton /> }
+ { !preferredBrands.isLoading && (
+ <Swiper
+ slidesPerView={3.5}
+ spaceBetween={8}
+ freeMode
+ >
+ { preferredBrands.data?.manufactures.map((brand) => (
+ <SwiperSlide key={brand.id}>
+ <BrandCard brand={brand} />
+ </SwiperSlide>
+ )) }
+ </Swiper>
+ ) }
+ </div>
+ )
+}
+
+export default PreferredBrand \ No newline at end of file
diff --git a/src/lib/home/components/Skeleton/PopularProductSkeleton.jsx b/src/lib/home/components/Skeleton/PopularProductSkeleton.jsx
new file mode 100644
index 00000000..c5b0fcaa
--- /dev/null
+++ b/src/lib/home/components/Skeleton/PopularProductSkeleton.jsx
@@ -0,0 +1,10 @@
+import ProductCardSkeleton from "@/core/components/elements/Skeleton/ProductCardSkeleton"
+
+const PopularProductSkeleton = () => (
+ <div className="grid grid-cols-2 gap-x-3">
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ </div>
+)
+
+export default PopularProductSkeleton \ No newline at end of file
diff --git a/src/lib/home/components/Skeleton/PreferredBrandSkeleton.jsx b/src/lib/home/components/Skeleton/PreferredBrandSkeleton.jsx
new file mode 100644
index 00000000..6bdd3c82
--- /dev/null
+++ b/src/lib/home/components/Skeleton/PreferredBrandSkeleton.jsx
@@ -0,0 +1,12 @@
+import BrandSkeleton from "@/core/components/elements/Skeleton/BrandSkeleton"
+
+const PreferredBrandSkeleton = () => (
+ <div className="grid grid-cols-4 gap-x-3">
+ <BrandSkeleton />
+ <BrandSkeleton />
+ <BrandSkeleton />
+ <BrandSkeleton />
+ </div>
+)
+
+export default PreferredBrandSkeleton \ No newline at end of file
diff --git a/src/lib/home/hooks/useCategoryHome.js b/src/lib/home/hooks/useCategoryHome.js
new file mode 100644
index 00000000..14ef2a0f
--- /dev/null
+++ b/src/lib/home/hooks/useCategoryHome.js
@@ -0,0 +1,13 @@
+import categoryHomeApi from "../api/categoryHomeApi"
+import { useQuery } from "react-query"
+
+const useCategoryHome = ({ id }) => {
+ const fetchCategoryHome = async () => await categoryHomeApi({ id })
+ const { isLoading, data } = useQuery(`categoryHome-${id}`, fetchCategoryHome)
+
+ return {
+ categoryHome: { data, isLoading }
+ }
+}
+
+export default useCategoryHome \ No newline at end of file
diff --git a/src/lib/home/hooks/useCategoryHomeId.js b/src/lib/home/hooks/useCategoryHomeId.js
new file mode 100644
index 00000000..bb61b655
--- /dev/null
+++ b/src/lib/home/hooks/useCategoryHomeId.js
@@ -0,0 +1,13 @@
+import categoryHomeIdApi from "../api/categoryHomeIdApi"
+import { useQuery } from "react-query"
+
+const useCategoryHomeId = () => {
+ const fetchCategoryHomeId = async () => await categoryHomeIdApi()
+ const { isLoading, data } = useQuery("categoryHomeId", fetchCategoryHomeId)
+
+ return {
+ categoryHomeIds: { data, isLoading }
+ }
+}
+
+export default useCategoryHomeId \ No newline at end of file
diff --git a/src/lib/home/hooks/useHeroBanner.js b/src/lib/home/hooks/useHeroBanner.js
new file mode 100644
index 00000000..a15dda60
--- /dev/null
+++ b/src/lib/home/hooks/useHeroBanner.js
@@ -0,0 +1,13 @@
+import heroBannerApi from "../api/heroBannerApi"
+import { useQuery } from "react-query"
+
+const useHeroBanner = () => {
+ const fetchHeroBanner = async () => await heroBannerApi()
+ const { isLoading, data } = useQuery("heroBanner", fetchHeroBanner)
+
+ return {
+ heroBanners: { data, isLoading }
+ }
+}
+
+export default useHeroBanner \ No newline at end of file
diff --git a/src/lib/home/hooks/usePopularProduct.js b/src/lib/home/hooks/usePopularProduct.js
new file mode 100644
index 00000000..f69c2f71
--- /dev/null
+++ b/src/lib/home/hooks/usePopularProduct.js
@@ -0,0 +1,13 @@
+import popularProductApi from "../api/popularProductApi"
+import { useQuery } from "react-query"
+
+const usePopularProduct = () => {
+ const fetchPopularProduct = async () => await popularProductApi()
+ const { data, isLoading } = useQuery('popularProduct', fetchPopularProduct)
+
+ return {
+ popularProducts: { data, isLoading }
+ }
+}
+
+export default usePopularProduct \ No newline at end of file
diff --git a/src/lib/home/hooks/usePreferredBrand.js b/src/lib/home/hooks/usePreferredBrand.js
new file mode 100644
index 00000000..4be9793e
--- /dev/null
+++ b/src/lib/home/hooks/usePreferredBrand.js
@@ -0,0 +1,13 @@
+import preferredBrandApi from "../api/preferredBrandApi"
+import { useQuery } from "react-query"
+
+const usePreferredBrand = () => {
+ const fetchPreferredBrand = async () => await preferredBrandApi()
+ const { data, isLoading } = useQuery('preferredBrand', fetchPreferredBrand)
+
+ return {
+ preferredBrands: { data, isLoading }
+ }
+}
+
+export default usePreferredBrand \ No newline at end of file
diff --git a/src/lib/product/api/productApi.js b/src/lib/product/api/productApi.js
new file mode 100644
index 00000000..a543f086
--- /dev/null
+++ b/src/lib/product/api/productApi.js
@@ -0,0 +1,8 @@
+import odooApi from "@/core/api/odooApi"
+
+const productApi = async ({ id }) => {
+ const dataProduct = await odooApi('GET', `/api/v1/product/${id}`)
+ return dataProduct
+}
+
+export default productApi \ No newline at end of file
diff --git a/src/lib/product/api/productSearchApi.js b/src/lib/product/api/productSearchApi.js
new file mode 100644
index 00000000..86b2914f
--- /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 \ No newline at end of file
diff --git a/src/lib/product/api/productSimilarApi.js b/src/lib/product/api/productSimilarApi.js
new file mode 100644
index 00000000..1449d9ca
--- /dev/null
+++ b/src/lib/product/api/productSimilarApi.js
@@ -0,0 +1,8 @@
+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 \ No newline at end of file
diff --git a/src/lib/product/components/Product.jsx b/src/lib/product/components/Product.jsx
new file mode 100644
index 00000000..2a3624e7
--- /dev/null
+++ b/src/lib/product/components/Product.jsx
@@ -0,0 +1,276 @@
+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 { addItemCart } from "@/core/utils/cart"
+
+const informationTabOptions = [
+ { value: 'specification', label: 'Spesifikasi' },
+ { value: 'description', label: 'Deskripsi' },
+ { value: 'important', label: 'Info Penting' },
+]
+
+const Product = ({ product }) => {
+ 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(', ') : ''
+ console.log(variant);
+ 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
+ }
+ addItemCart({
+ productId: activeVariant.id,
+ quantity
+ })
+ toast.success('Berhasil menambahkan ke keranjang')
+ }
+
+ return (
+ <>
+ <Image
+ src={product.image}
+ alt={product.name}
+ className="h-72 object-contain object-center w-full border-b border-gray_r-4 bg-white"
+ />
+
+ <div className="p-4">
+ <Link href="/" className="mb-2">{ product.manufacture?.name }</Link>
+ <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?.priceDiscount)}
+ </div>
+ <Badge type="solid-red">
+ {activeVariant?.price?.discountPercentage}%
+ </Badge>
+ </div>
+ ) }
+ <h3 className="text-red_r-11 font-semibold mt-1">
+ { activeVariant?.price?.price > 0 ? currencyFormat(activeVariant?.price?.price) : (
+ <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 border-b border-red_r-11' : '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 \ No newline at end of file
diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx
new file mode 100644
index 00000000..86ac3a64
--- /dev/null
+++ b/src/lib/product/components/ProductCard.jsx
@@ -0,0 +1,68 @@
+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">
+ <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 bg-white"
+ />
+ { 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 bg-white">
+ <Link
+ href={createSlug('/shop/brands/', product?.manufacture?.name, product?.manufacture.id)}
+ className="mb-1"
+ >
+ {product?.manufacture?.name}
+ </Link>
+ <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 \ No newline at end of file
diff --git a/src/lib/product/components/ProductFilter.jsx b/src/lib/product/components/ProductFilter.jsx
new file mode 100644
index 00000000..023b6a8b
--- /dev/null
+++ b/src/lib/product/components/ProductFilter.jsx
@@ -0,0 +1,131 @@
+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 \ No newline at end of file
diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx
new file mode 100644
index 00000000..14df9864
--- /dev/null
+++ b/src/lib/product/components/ProductSearch.jsx
@@ -0,0 +1,95 @@
+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 \ No newline at end of file
diff --git a/src/lib/product/components/ProductSimilar.jsx b/src/lib/product/components/ProductSimilar.jsx
new file mode 100644
index 00000000..89cab536
--- /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 \ No newline at end of file
diff --git a/src/lib/product/components/ProductSlider.jsx b/src/lib/product/components/ProductSlider.jsx
new file mode 100644
index 00000000..8d677547
--- /dev/null
+++ b/src/lib/product/components/ProductSlider.jsx
@@ -0,0 +1,51 @@
+import { Swiper, SwiperSlide } from "swiper/react"
+import ProductCard from "./ProductCard"
+import "swiper/css"
+import Image from "@/core/components/elements/Image/Image"
+import Link from "@/core/components/elements/Link/Link"
+import { useState } 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 [ activeIndex, setActiveIndex ] = useState(0)
+ const swiperSliderFirstMove = (swiper) => {
+ setActiveIndex(swiper.activeIndex)
+ }
+
+ return (
+ <>
+ { bannerMode && (
+ <Image
+ src={products.banner.image}
+ alt={products.banner.name}
+ className={`${bannerClassName} ${activeIndex > 0 ? 'opacity-0' : 'opacity-100'}`}
+ />
+ ) }
+ <Swiper
+ freeMode={true}
+ slidesPerView={2.2}
+ spaceBetween={8}
+ onSlideChange={swiperSliderFirstMove}
+ prefix="product"
+ >
+ { 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 \ No newline at end of file
diff --git a/src/lib/product/components/Skeleton/ProductSearchSkeleton.jsx b/src/lib/product/components/Skeleton/ProductSearchSkeleton.jsx
new file mode 100644
index 00000000..e51a565c
--- /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 \ No newline at end of file
diff --git a/src/lib/product/hooks/useProductSearch.js b/src/lib/product/hooks/useProductSearch.js
new file mode 100644
index 00000000..d23a8098
--- /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 \ No newline at end of file
diff --git a/src/lib/product/hooks/useProductSimilar.js b/src/lib/product/hooks/useProductSimilar.js
new file mode 100644
index 00000000..444fec0b
--- /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 \ No newline at end of file