diff options
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | src/lib/brand/components/Brand.jsx | 128 | ||||
| -rw-r--r-- | src/lib/product/api/productSearchApi.js | 2 | ||||
| -rw-r--r-- | src/lib/product/components/ProductFilterDesktop.jsx | 147 | ||||
| -rw-r--r-- | src/lib/product/components/ProductSearch.jsx | 258 | ||||
| -rw-r--r-- | tailwind.config.js | 12 |
6 files changed, 447 insertions, 102 deletions
diff --git a/package.json b/package.json index 3f9ef1a7..0d51e896 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "axios": "^1.1.3", "camelcase-object-deep": "^1.1.7", "cookies-next": "^2.1.1", + "flowbite": "^1.6.4", + "flowbite-react": "^0.4.2", "framer-motion": "^7.6.7", "lodash-contrib": "^4.1200.1", "midtrans-client": "^1.3.1", diff --git a/src/lib/brand/components/Brand.jsx b/src/lib/brand/components/Brand.jsx index c338c4c4..db4e81da 100644 --- a/src/lib/brand/components/Brand.jsx +++ b/src/lib/brand/components/Brand.jsx @@ -8,6 +8,8 @@ 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' const swiperBanner = { pagination: { dynamicBullets: true }, @@ -23,46 +25,94 @@ const Brand = ({ id }) => { return ( <> - <div className='min-h-[150px]'> - {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'> - <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} + <MobileView> + <> + <div className='min-h-[150px]'> + {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'> + <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> - </> - )} - </div> - <Divider /> + </> + )} + </div> + <Divider /> + </> + </MobileView> + <DesktopView> + <div className='container mx-auto mt-10 mb-3'> + <div className='min-h-[150px]'> + {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'> + <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 /> + </div> + </DesktopView> </> ) } diff --git a/src/lib/product/api/productSearchApi.js b/src/lib/product/api/productSearchApi.js index e7ad49a6..f626e8cc 100644 --- a/src/lib/product/api/productSearchApi.js +++ b/src/lib/product/api/productSearchApi.js @@ -2,7 +2,7 @@ import _ from 'lodash-contrib' import axios from 'axios' const productSearchApi = async ({ query }) => { - const dataProductSearch = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}`) + const dataProductSearch = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}&operation=OR`) return dataProductSearch.data } diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx new file mode 100644 index 00000000..276a7cc9 --- /dev/null +++ b/src/lib/product/components/ProductFilterDesktop.jsx @@ -0,0 +1,147 @@ +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' +import { Accordion, Badge, Checkbox, Label, TextInput } from 'flowbite-react' + +const ProductFilterDesktop = ({ brands, categories, prefixUrl, defaultBrand = null }) => { + const router = useRouter() + const { query } = router + const [order, setOrder] = useState(query?.orderBy) + const [brandValues, setBrand] = useState(query?.brand?.split(',') || []) + const [categoryValues, setCategory] = useState(query?.category?.split(',') || []) + const [priceFrom, setPriceFrom] = useState(query?.priceFrom) + const [priceTo, setPriceTo] = useState(query?.priceTo) + + const handleCategorysChange = (event) => { + const value = event.target.value + const isChecked = event.target.checked + if (isChecked) { + setCategory([...categoryValues, value]) + } else { + setCategory(categoryValues.filter((val) => val !== value)) + } + } + const handleBrandsChange = (event) => { + const value = event.target.value + const isChecked = event.target.checked + if (isChecked) { + setBrand([...brandValues, value]) + } else { + setBrand(brandValues.filter((val) => val !== value)) + } + } + + console.log('branddddd', defaultBrand) + + const handleSubmit = () => { + let params = { + q: router.query.q, + orderBy: order, + brand: brandValues.join(','), + category: categoryValues.join(','), + priceFrom, + priceTo + } + params = _.pickBy(params, _.identity) + params = toQuery(params) + router.push(`${prefixUrl}?${params}`) + } + + return ( + <> + <Accordion + flush={true} + alwaysOpen={true} + > + <Accordion.Panel> + <Accordion.Title>Kategori</Accordion.Title> + <Accordion.Content className='overflow-auto max-h-[150px]'> + <div + className='flex flex-col gap-4 scroll-snap' + id='checkbox' + > + {categories.map((category, index) => ( + <div + className='flex items-center gap-2' + key={index} + > + <Checkbox + checked={categoryValues.includes(category)} + onChange={handleCategorysChange} + value={category} + /> + <Label htmlFor='accept'> {category} </Label> + {/* <div className='badge-solid-red'>250</div> */} + </div> + ))} + </div> + </Accordion.Content> + </Accordion.Panel> + <Accordion.Panel> + {!defaultBrand && ( + <> + <Accordion.Title >Brand</Accordion.Title> + <Accordion.Content className='overflow-auto max-h-[150px]'> + <div + className='flex flex-col gap-4 scroll-snap' + id='checkbox' + > + {brands.map((brand, index) => ( + <div + className='flex items-center gap-2' + key={index} + > + <Checkbox + checked={brandValues.includes(brand)} + onChange={handleBrandsChange} + value={brand} + /> + <Label htmlFor='accept'> {brand} </Label> + {/* <div className='badge-solid-red'>250</div> */} + </div> + ))} + </div> + </Accordion.Content> + </> + )} + </Accordion.Panel> + <Accordion.Panel> + <Accordion.Title> Harga </Accordion.Title> + <Accordion.Content> + <div className='mb-3'> + <TextInput + placeholder='Harga Minimum' + addon='Rp' + type='number' + value={priceFrom} + onChange={(e) => setPriceFrom(e.target.value)} + /> + </div> + <div className='mb-3'> + <TextInput + placeholder='Harga Maximum' + addon='Rp' + type='number' + value={priceTo} + onChange={(e) => setPriceTo(e.target.value)} + /> + </div> + </Accordion.Content> + </Accordion.Panel> + </Accordion> + <div className='p-5'> + <button + type='button' + className='btn-solid-red w-full mt-6' + onClick={handleSubmit} + > + Terapkan + </button> + </div> + </> + ) +} + +export default ProductFilterDesktop diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index 52bd5119..3078eac5 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -7,8 +7,15 @@ import _ from 'lodash' import ProductSearchSkeleton from './Skeleton/ProductSearchSkeleton' import ProductFilter from './ProductFilter' import useActive from '@/core/hooks/useActive' +import MobileView from '@/core/components/views/MobileView' +import DesktopView from '@/core/components/views/DesktopView' +import NextImage from 'next/image' +import { ChevronDownIcon } from '@heroicons/react/24/outline' +import ProductFilterDesktop from './ProductFilterDesktop' +import { useRouter } from 'next/router' const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { + const router = useRouter() const { page = 1 } = query if (defaultBrand) query.brand = defaultBrand.toLowerCase() const { productSearch } = useProductSearch({ query }) @@ -35,6 +42,29 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { } ) + const [open, setOpen] = useState(1) + const [order, setOrder] = useState(query?.orderBy) + + const handleOpen = (value) => { + setOpen(open === value ? 0 : value) + } + const orderOptions = [ + { value: 'price-asc', label: 'Harga Terendah' }, + { value: 'price-desc', label: 'Harga Tertinggi' }, + { value: 'popular', label: 'Populer' }, + { value: 'stock', label: 'Ready Stock' } + ] + + const handleOrderBy = (e) => { + let params = { + ...router.query, + orderBy: e.target.value + } + params = _.pickBy(params, _.identity) + params = toQuery(params) + router.push(`${prefixUrl}?${params}`) + } + useEffect(() => { if (!products) { setProducts(productSearch.data?.response?.products) @@ -46,70 +76,180 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { } 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 - {pageCount > 1 ? ( + <> + <MobileView> + <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 ? ( <> - {productStart + 1}- - {productStart + productRows > productFound - ? productFound - : productStart + productRows} - dari + Menampilkan + {pageCount > 1 ? ( + <> + {productStart + 1}- + {productStart + productRows > productFound + ? productFound + : productStart + productRows} + dari + </> + ) : ( + '' + )} + {productFound} + produk{' '} + {query.q && ( + <> + untuk pencarian <span className='font-semibold'>{query.q}</span> + </> + )} </> ) : ( - '' + 'Mungkin yang anda cari' )} - {productFound} - 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> + + <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> + </MobileView> + <DesktopView> + <div className='container mx-auto mt-10 flex mb-3'> + <div className='w-3/12'> + <ProductFilterDesktop + brands={brands || []} + categories={categories || []} + prefixUrl={prefixUrl} + defaultBrand={defaultBrand} /> - ))} - </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> + </div> + <div className='w-9/12 p-3'> + <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'> + {productFound > 0 ? ( + <> + Menampilkan + {pageCount > 1 ? ( + <> + {productStart + 1}- + {productStart + productRows > productFound + ? productFound + : productStart + productRows} + dari + </> + ) : ( + '' + )} + {productFound} + produk{' '} + {query.q && ( + <> + untuk pencarian <span className='font-semibold'>{query.q}</span> + </> + )} + </> + ) : ( + 'Mungkin yang anda cari' + )} + </div> + <div className='justify-end flex '> + {/* <div> + <select + name='jumlah-baris' + className='form-input mt-2' + > + <option value=''>Jumlah Baris</option> + {orderOptions.map((option, index) => ( + <option value={option.value}> {option.label} </option> + ))} + </select> + </div> */} + <div className='ml-3'> + <select + name='urutan' + className='form-input mt-2' + onChange={(e) => handleOrderBy(e)} + > + <option value=''>Urutkan</option> + {orderOptions.map((option, index) => ( + <option value={option.value}> {option.label} </option> + ))} + </select> + </div> + </div> + </div> + <div className='grid grid-cols-5 gap-x-3 gap-y-6'> + {products && + products.map((product) => ( + <ProductCard + product={product} + key={product.id} + /> + ))} + </div> + <div className='flex justify-between items-center mt-6 mb-2'> + <div className='pt-2 pb-6 flex items-center gap-x-3'> + <NextImage + src='/images/logo-question.png' + alt='Logo Question Indoteknik' + width={60} + height={60} + /> + <div className='text-gray_r-12/90'> + <span> + Barang yang anda cari tidak ada?{' '} + <a + href='#' + className='text-red_r-9' + > + Hubungi Kami + </a> + </span> + </div> + </div> + + <Pagination + pageCount={pageCount} + currentPage={parseInt(page)} + url={`${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} + className='!justify-end' + /> + </div> + </div> + </div> + </DesktopView> + </> ) } diff --git a/tailwind.config.js b/tailwind.config.js index 95570311..f4335eb8 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,12 @@ /** @type {import('tailwindcss').Config} */ -module.exports = { - content: ['./src/**/*.{js,ts,jsx,tsx}'], +const withMT = require("@material-tailwind/react/utils/withMT"); +module.exports = withMT({ + content: [ + "./node_modules/flowbite-react/**/*.js", + './src/**/*.{js,ts,jsx,tsx}'], + plugins: [ + require("flowbite/plugin") + ], theme: { extend: { container: { @@ -103,4 +109,4 @@ module.exports = { } }, plugins: [require('@tailwindcss/line-clamp'), require('@tailwindcss/typography')] -} +}) |
