diff options
Diffstat (limited to 'src/lib/product')
| -rw-r--r-- | src/lib/product/components/Product/ProductDesktopVariant.jsx | 136 | ||||
| -rw-r--r-- | src/lib/product/components/Product/ProductMobileVariant.jsx | 188 | ||||
| -rw-r--r-- | src/lib/product/components/ProductCard.jsx | 2 | ||||
| -rw-r--r-- | src/lib/product/components/ProductFilterDesktop.jsx | 348 | ||||
| -rw-r--r-- | src/lib/product/components/ProductSearch.jsx | 78 |
5 files changed, 477 insertions, 275 deletions
diff --git a/src/lib/product/components/Product/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx index de88e5bb..59fa2032 100644 --- a/src/lib/product/components/Product/ProductDesktopVariant.jsx +++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx @@ -103,15 +103,6 @@ const ProductDesktopVariant = ({ variantQuantityRefs.current[variantId] = element; }; - const validQuantity = (quantity) => { - let isValid = true; - if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) { - toast.error('Jumlah barang minimal 1'); - isValid = false; - } - return isValid; - }; - const handleAddToCart = (variant) => { if (!auth) { router.push(`/login?next=/shop/product/${slug}?srsltid=${srsltid}`); @@ -132,29 +123,36 @@ const ProductDesktopVariant = ({ setAddCartAlert(true); }; + const toInt = (v) => { + const n = parseInt(String(v ?? '').trim(), 10); + return Number.isFinite(n) ? n : 0; + }; + + const validQuantity = (q) => { + if (!Number.isInteger(q) || q < 1) { + toast.error('Jumlah barang minimal 1'); + return false; + } + return true; + }; + const handleBuy = async (variant) => { - const quantity = variantQuantityRefs?.current[product.id]?.value; - let isLoggedIn = typeof auth === 'object'; + const quantity = Math.max(1, toInt(quantityInput)); // clamp min 1 + let isLoggedIn = typeof auth === 'object'; if (!isLoggedIn) { const currentUrl = encodeURIComponent(router.asPath); await router.push(`/login?next=${currentUrl}`); - // Tunggu login berhasil, misalnya dengan memantau perubahan status auth. - const authCheckInterval = setInterval(() => { - const newAuth = getAuth(); - if (typeof newAuth === 'object') { - isLoggedIn = true; - auth = newAuth; // Update nilai auth setelah login - clearInterval(authCheckInterval); - } - }, 500); // Periksa status login setiap 500ms - + // tunggu sampai auth ada await new Promise((resolve) => { - const checkLogin = setInterval(() => { - if (isLoggedIn) { - clearInterval(checkLogin); - resolve(null); + const t = setInterval(() => { + const newAuth = getAuth(); + if (typeof newAuth === 'object') { + isLoggedIn = true; + auth = newAuth; + clearInterval(t); + resolve(); } }, 500); }); @@ -162,16 +160,57 @@ const ProductDesktopVariant = ({ if (!validQuantity(quantity)) return; - updateItemCart({ + await updateItemCart({ productId: variant, quantity, programLineId: null, selected: true, source: 'buy', }); - router.push(`/shop/checkout?source=buy`); + + router.push('/shop/checkout?source=buy'); }; + // const handleBuy = async (variant) => { + // const quantity = variantQuantityRefs?.current[product.id]?.value; + // let isLoggedIn = typeof auth === 'object'; + + // if (!isLoggedIn) { + // const currentUrl = encodeURIComponent(router.asPath); + // await router.push(`/login?next=${currentUrl}`); + + // // Tunggu login berhasil, misalnya dengan memantau perubahan status auth. + // const authCheckInterval = setInterval(() => { + // const newAuth = getAuth(); + // if (typeof newAuth === 'object') { + // isLoggedIn = true; + // auth = newAuth; // Update nilai auth setelah login + // clearInterval(authCheckInterval); + // } + // }, 500); // Periksa status login setiap 500ms + + // await new Promise((resolve) => { + // const checkLogin = setInterval(() => { + // if (isLoggedIn) { + // clearInterval(checkLogin); + // resolve(null); + // } + // }, 500); + // }); + // } + + // if (!validQuantity(quantity)) return; + + // updateItemCart({ + // productId: variant, + // quantity, + // programLineId: null, + // selected: true, + // source: 'buy', + // }); + // router.push(`/shop/checkout?source=buy`); + // }; + const handleButton = async (variant) => { const quantity = quantityInput; let isLoggedIn = typeof auth === 'object'; @@ -443,37 +482,53 @@ const ProductDesktopVariant = ({ )} </h3> )} - <div className='flex justify-between items-center py-5 px-3'> + <div className='flex gap-x-5 items-center py-5'> <div className='relative flex items-center'> <button type='button' className='absolute left-0 px-2 py-1 h-full text-gray-500' - onClick={() => - setQuantityInput( - String(Math.max(1, Number(quantityInput) - 1)) - ) - } + onClick={() => { + const n = parseInt(String(quantityInput), 10); + const next = Number.isFinite(n) ? Math.max(1, n - 1) : 1; + setQuantityInput(next); + }} > - </button> + <input type='number' id='quantity' min={1} + step={1} + inputMode='numeric' + pattern='[0-9]*' value={quantityInput} - onChange={(e) => setQuantityInput(e.target.value)} - className=' w-24 h-10 text-center border border-gray-300 rounded focus:outline-none' + onChange={(e) => { + const raw = e.target.value.trim(); + const n = parseInt(raw, 10); + setQuantityInput(Number.isFinite(n) && n > 0 ? n : 1); + }} + onKeyDown={(e) => { + if (['e', 'E', '+', '-', '.'].includes(e.key)) + e.preventDefault(); + }} + className='w-24 h-10 text-center border border-gray-300 rounded focus:outline-none' /> + <button type='button' className='absolute right-0 px-2 py-1 h-full text-gray-500' - onClick={() => - setQuantityInput(String(Number(quantityInput) + 1)) - } + onClick={() => { + const n = parseInt(String(quantityInput), 10); + const next = (Number.isFinite(n) ? n : 0) + 1; + setQuantityInput(next); + }} > + </button> </div> + <div> <Skeleton isLoaded={!isLoadingSLA} @@ -510,7 +565,8 @@ const ProductDesktopVariant = ({ <Button onClick={() => handleAddToCart(product.id)} className='w-full' - colorScheme='yellow' + colorScheme='red' + variant={'outline'} > Keranjang </Button> @@ -529,7 +585,7 @@ const ProductDesktopVariant = ({ className='w-full border-2 p-2 gap-1 mt-2 hover:bg-slate-100 flex items-center' > <ImageNext - src='/images/writing.png' + src='/images/doc_red.svg' alt='penawaran instan' className='' width={25} diff --git a/src/lib/product/components/Product/ProductMobileVariant.jsx b/src/lib/product/components/Product/ProductMobileVariant.jsx index de5c3f10..cab8e9be 100644 --- a/src/lib/product/components/Product/ProductMobileVariant.jsx +++ b/src/lib/product/components/Product/ProductMobileVariant.jsx @@ -27,6 +27,8 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { let auth = getAuth(); const [quantity, setQuantity] = useState('1'); const [selectedVariant, setSelectedVariant] = useState(product.id); + const [quantityInput, setQuantityInput] = useState(String(1)); + const [qtyPickUp, setQtyPickUp] = useState(0); const [informationTab, setInformationTab] = useState( informationTabOptions[0].value ); @@ -63,30 +65,33 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { } }, [selectedVariant, product]); - const validAction = () => { - let isValid = true; + const validAction = (q) => { if (!selectedVariant) { toast.error('Pilih varian terlebih dahulu'); - isValid = false; + return false; } - if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) { + if (!Number.isInteger(q) || q < 1) { toast.error('Jumlah barang minimal 1'); - isValid = false; + return false; } - return isValid; + return true; }; + const getQty = () => Math.max(1, toInt(quantityInput)); + const handleClickCart = async () => { + const q = getQty(); + if (!auth) { router.push(`/login?next=/shop/product/${slug}?srsltid=${srsltid}`); return; } + if (!validAction(q)) return; - if (!validAction()) return; - gtagAddToCart(activeVariant, quantity); + gtagAddToCart(activeVariant, q); updateItemCart({ productId: product.id, - quantity, + quantity: q, programLineId: null, selected: true, source: null, @@ -95,37 +100,29 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { }; const handleClickBuy = async () => { - let isLoggedIn = typeof auth === 'object'; + const q = getQty(); + let isLoggedIn = typeof auth === 'object'; if (!isLoggedIn) { const currentUrl = encodeURIComponent(router.asPath); await router.push(`/login?next=${currentUrl}`); - - // Tunggu login berhasil, misalnya dengan memantau perubahan status auth. - const authCheckInterval = setInterval(() => { - const newAuth = getAuth(); - if (typeof newAuth === 'object') { - isLoggedIn = true; - auth = newAuth; // Update nilai auth setelah login - clearInterval(authCheckInterval); - } - }, 500); // Periksa status login setiap 500ms - await new Promise((resolve) => { - const checkLogin = setInterval(() => { - if (isLoggedIn) { - clearInterval(checkLogin); + const t = setInterval(() => { + const newAuth = getAuth(); + if (typeof newAuth === 'object') { + auth = newAuth; + clearInterval(t); resolve(null); } }, 500); }); } - if (!validAction()) return; + if (!validAction(q)) return; updateItemCart({ productId: product.id, - quantity, + quantity: q, programLineId: null, selected: true, source: 'buy', @@ -133,8 +130,21 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { router.push(`/shop/checkout?source=buy`); }; + const toInt = (v) => { + const n = parseInt(String(v ?? '').trim(), 10); + return Number.isFinite(n) ? n : 0; + }; + + const validQuantity = (q) => { + if (!Number.isInteger(q) || q < 1) { + toast.error('Jumlah barang minimal 1'); + return false; + } + return true; + }; + const handleButton = (variant) => { - const quantity = quantityInput; + const quantity = Math.max(1, toInt(quantityInput)); // clamp min 1 if (!validQuantity(quantity)) return; updateItemCart({ @@ -168,9 +178,10 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { return ( <MobileView> - <div - className={`px-4 block md:sticky md:top-[150px] md:py-6 fixed bottom-0 left-0 right-0 bg-white p-2 z-10 pb-6 pt-6 rounded-lg shadow-[rgba(0,0,4,0.1)_0px_-4px_4px_0px] `} - > + {/* PRICE & ACTIONS: tetap punyamu, hanya hapus input number lama */} + {/* ===== BAR BAWAH (fixed) ===== */} + <div className='px-4 fixed bottom-0 left-0 right-0 bg-white z-10 pb-6 pt-4 rounded-t-2xl shadow-[rgba(0,0,4,0.1)_0px_-4px_4px_0px]'> + {/* HARGA & PPN (logikamu tetap) */} {activeVariant.isFlashSale && activeVariant?.price?.discountPercentage > 0 ? ( <> @@ -225,50 +236,105 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => { )} </div> )} - <div className=''> - <div className='mt-4 mb-2'>Jumlah</div> - <div className='flex gap-x-3'> - <div className='w-2/12'> + + {/* ⬇️ TAMBAHKAN BLOK INI DI DALAM BAR: STOK & STEPPER */} + <div className='grid grid-cols-12 items-center gap-3 mt-3'> + <div className='col-span-7'> + <div + className={`text-[14px] ${ + product?.sla?.qty < 10 ? 'text-red-600 font-medium' : '' + }`} + > + Stock : {activeVariant?.stock ?? 0} + </div> + {qtyPickUp > 0 && ( + <div className='text-[16px] mt-0.5 text-red-500 italic'> + * {qtyPickUp} barang bisa di pickup + </div> + )} + </div> + <div className='col-span-5 flex justify-end'> + <div className='inline-flex items-stretch border rounded-xl overflow-hidden'> + <button + type='button' + className='h-10 w-10 grid place-items-center text-gray-700 hover:bg-gray-100 active:scale-95' + onClick={() => + setQuantityInput( + String(Math.max(1, Number(quantityInput || 1) - 1)) + ) + } + aria-label='Kurangi' + > + <span className='text-2xl leading-none'>–</span> + </button> <input - name='quantity' type='number' - className='form-input' - value={quantity} - onChange={(e) => setQuantity(e.target.value)} + min={1} + value={quantityInput} + onChange={(e) => setQuantityInput(e.target.value)} + className='h-10 w-16 text-center text-lg outline-none border-x + [appearance:textfield] + [&::-webkit-outer-spin-button]:appearance-none + [&::-webkit-inner-spin-button]:appearance-none' /> + <button + type='button' + className='h-10 w-10 grid place-items-center text-gray-700 hover:bg-gray-100 active:scale-95' + onClick={() => + setQuantityInput(String(Number(quantityInput || 1) + 1)) + } + aria-label='Tambah' + > + <span className='text-2xl leading-none'>+</span> + </button> </div> - <button - type='button' - className='btn-yellow flex-1' - onClick={handleClickCart} - > - Keranjang - </button> - <button - type='button' - className='btn-solid-red flex-1' - onClick={handleClickBuy} - > - Beli - </button> </div> + </div> + + <div className='h4'/> + {/* TOMBOL AKSI */} + <div className='flex gap-2 mt-3'> + {/* Tombol Dokumen */} <Button onClick={() => handleButton(product.id)} - color={'red'} - colorScheme='white' - className='w-full border-2 p-2 gap-1 mt-2 hover:bg-slate-100 flex items-center' + className='flex items-center justify-center p-2 border-2 hover:bg-slate-100' + variant='outline' + title='Lihat Dokumen' > <ImageNext - src='/images/writing.png' - alt='penawaran instan' - className='' - width={25} - height={25} + src='/images/doc.svg' + width={18} + height={18} + alt='Dokumen' /> - Penawaran Harga Instan </Button> + {/* Container untuk tombol aksi utama */} + <div className='flex-1 flex gap-2'> + <Button + onClick={() => + handleClickCart(product.id, Number(quantityInput || 1)) + } + className='flex-1' + colorScheme='red' + variant='outline' + isDisabled={product.stock === 0} + > + Keranjang + </Button> + <Button + onClick={() => + handleClickBuy(product.id, Number(quantityInput || 1)) + } + className='flex-1' + colorScheme='red' + isDisabled={product.stock === 0} + > + Beli + </Button> + </div> </div> </div> + <Image src={product.image + '?variant=True'} alt={product.name} diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index a8ed90a4..f4f5882e 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -73,7 +73,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { if (variant == 'vertical') { return ( - <div className='rounded shadow-sm border border-gray_r-4 bg-white h-[330px] md:h-[380px]'> + <div className='rounded shadow-sm border border-gray_r-4 bg-white'> <Link href={URL.product} className='border-b border-gray_r-4 relative' aria-label='Produk'> <div className='relative'> <Image diff --git a/src/lib/product/components/ProductFilterDesktop.jsx b/src/lib/product/components/ProductFilterDesktop.jsx index d2ecb4d9..440e1795 100644 --- a/src/lib/product/components/ProductFilterDesktop.jsx +++ b/src/lib/product/components/ProductFilterDesktop.jsx @@ -1,7 +1,6 @@ import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import _ from 'lodash'; -import { toQuery } from 'lodash-contrib'; import { Accordion, AccordionButton, @@ -9,7 +8,6 @@ import { AccordionItem, AccordionPanel, Box, - Button, Checkbox, Input, InputGroup, @@ -17,136 +15,200 @@ import { Stack, VStack, } from '@chakra-ui/react'; -import Image from '@/core/components/elements/Image/Image'; import { formatCurrency } from '@/core/utils/formatValue'; const ProductFilterDesktop = ({ - brands, - categories, + brands, // bisa [{id,name,qty}] atau [{brand,qty}] + categories, // [{name, qty}] prefixUrl, - defaultBrand = null, }) => { const router = useRouter(); - const { query } = router; - const [order, setOrder] = useState(query?.orderBy); - const [brandValues, setBrand] = useState(query?.brand?.split(',') || []); + + const [order, setOrder] = useState(router.query?.orderBy); + const [brandValues, setBrand] = useState( + typeof router.query?.brand === 'string' && router.query.brand + ? router.query.brand.split(',').filter(Boolean) + : [] + ); const [categoryValues, setCategory] = useState( - query?.category?.split(',') || [] + typeof router.query?.category === 'string' && router.query.category + ? router.query.category.split(',').filter(Boolean) + : [] ); - const [priceFrom, setPriceFrom] = useState(query?.priceFrom); - const [priceTo, setPriceTo] = useState(query?.priceTo); - const [stock, setStock] = useState(query?.stock); + const [priceFrom, setPriceFrom] = useState(router.query?.priceFrom ?? ''); + const [priceTo, setPriceTo] = useState(router.query?.priceTo ?? ''); + const [stock, setStock] = useState(router.query?.stock ?? null); const [activeRange, setActiveRange] = useState(null); - const [activeIndeces, setActiveIndeces] = useState([]); + + const handlePriceKeyDown = (e) => { + if (e.key !== 'Enter') return; + e.preventDefault(); + // keluar dari preset kalau user input manual + setActiveRange(null); + + // pakai state terkini untuk apply + const fromVal = priceFrom === '' ? '' : String(priceFrom); + const toVal = priceTo === '' ? '' : String(priceTo); + + applyFilters({ priceFrom: fromVal, priceTo: toVal }); + }; + + // --- normalisasi data brand agar tahan banting --- + const normBrands = useMemo(() => { + return (brands ?? []) + .map((b, i) => ({ + id: String(b.id ?? b.val ?? b.brand ?? i), + name: String(b.name ?? b.brand ?? b.label ?? b.val ?? '').trim(), + qty: b.qty ?? b.count, + })) + .filter((b) => b.name); + }, [brands]); const priceRange = [ - { - priceFrom: 100000, - priceTo: 200000, - }, - { - priceFrom: 200000, - priceTo: 300000, - }, - { - priceFrom: 300000, - priceTo: 400000, - }, - { - priceFrom: 400000, - priceTo: 500000, - }, + { 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; - 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)); + const indexRange = priceRange.findIndex( + (r) => r.priceFrom === parseInt(priceFrom) && r.priceTo == parseInt(priceTo) + ); + + const applyFilters = (changes = {}) => { + const params = new URLSearchParams(); + + // 1) salin SEMUA param yang ada sekarang (jangan hilangkan apapun) + Object.entries(router.query).forEach(([k, v]) => { + if (v == null) return; + if (Array.isArray(v)) { + // penting: fq bisa multi-value; gunakan append, bukan join + v.forEach((item) => params.append(k, String(item))); + } else { + params.set(k, String(v)); + } + }); + + // 2) baca nilai dasar langsung dari URL (hindari state stale) + const arr = (val) => + typeof val === 'string' && val ? val.split(',').filter(Boolean) : []; + + const nextBrand = + 'brandValues' in changes ? changes.brandValues : arr(router.query.brand); + const nextCategory = + 'categoryValues' in changes + ? changes.categoryValues + : arr(router.query.category); + const nextPriceFrom = + 'priceFrom' in changes ? changes.priceFrom : router.query.priceFrom ?? ''; + const nextPriceTo = + 'priceTo' in changes ? changes.priceTo : router.query.priceTo ?? ''; + const nextStock = + 'stock' in changes ? changes.stock : router.query.stock ?? null; + const nextOrder = + 'order' in changes ? changes.order : router.query.orderBy ?? ''; + + const setOrDel = (key, val) => { + const empty = + val == null || val === '' || (Array.isArray(val) && val.length === 0); + if (empty) params.delete(key); + else params.set(key, Array.isArray(val) ? val.join(',') : String(val)); + }; + + setOrDel('brand', nextBrand); + setOrDel('category', nextCategory); + setOrDel('priceFrom', nextPriceFrom); + setOrDel('priceTo', nextPriceTo); + setOrDel('stock', nextStock); + setOrDel('orderBy', nextOrder); + + // 3) kalau ada perubahan filter utama → reset page ke 1 + const changedKeys = Object.keys(changes); + const touched = [ + 'brandValues', + 'categoryValues', + 'priceFrom', + 'priceTo', + 'stock', + 'order', + ]; + if (changedKeys.some((k) => touched.includes(k))) { + params.set('page', '1'); } + + // 4) shallow replace (tanpa reload penuh) + const base = router.asPath.split('?')[0]; + router.replace(`${base}?${params.toString()}`, undefined, { + shallow: true, + scroll: false, + }); }; - const handleReadyStockChange = (event) => { - const value = event.target.value; - const isChecked = event.target.checked; - if (isChecked) { - setStock(value); - } else { - setStock(null); - } + // debounce untuk input harga (biar nggak spam) + const debouncedApply = useMemo(() => _.debounce(applyFilters, 350), []); // eslint-disable-line + useEffect(() => () => debouncedApply.cancel(), [debouncedApply]); + + // === handlers === + const handleCategoriesChange = (e) => { + const { value, checked } = e.target; + const next = checked + ? [...categoryValues, value] + : categoryValues.filter((v) => v !== value); + setCategory(next); + applyFilters({ categoryValues: next }); }; - const handlePriceFromChange = async (priceFromr, priceTor, index) => { - await setPriceFrom(priceFromr); - await setPriceTo(priceTor); - setActiveRange(index); + const handleBrandsChange = (e) => { + const { value, checked } = e.target; // value = brand ID/name (string) + const next = checked + ? [...brandValues, value] + : brandValues.filter((v) => v !== value); + setBrand(next); + applyFilters({ brandValues: next }); }; - const handleSubmit = () => { - let params = { - penawaran: router.query.penawaran, - q: router.query.q, - orderBy: order, - brand: brandValues.join(','), - category: categoryValues.join(','), - priceFrom, - priceTo, - stock: stock, - }; - params = _.pickBy(params, _.identity); - params = toQuery(params); + const handleReadyStockChange = (e) => { + const { checked, value } = e.target; + const next = checked ? value : null; + setStock(next); + applyFilters({ stock: next }); + }; - const slug = Array.isArray(router.query.slug) - ? router.query.slug[0] - : router.query.slug; + const handlePriceRangeClick = async (pf, pt, idx) => { + await setPriceFrom(pf); + await setPriceTo(pt); + setActiveRange(idx); + applyFilters({ priceFrom: pf, priceTo: pt }); + }; - if (slug) { - if (prefixUrl.includes('category') || prefixUrl.includes('lob')) { - router.push(`${prefixUrl}?${params}`); - } else { - router.push(`${prefixUrl}/${slug}?${params}`); - } - } else { - router.push(`${prefixUrl}?${params}`); - } + const onPriceFromInput = (e) => { + setPriceFrom(e.target.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]) - } - }*/ + const onPriceToInput = (e) => { + setPriceTo(e.target.value); + }; useEffect(() => { setActiveRange(indexRange); - }, []); + }, []); // init active range + + useEffect(() => { + setBrand( + router.query?.brand + ? String(router.query.brand).split(',').filter(Boolean) + : [] + ); + setCategory( + router.query?.category + ? String(router.query.category).split(',').filter(Boolean) + : [] + ); + setPriceFrom(router.query?.priceFrom ?? ''); + setPriceTo(router.query?.priceTo ?? ''); + setStock(router.query?.stock ?? null); + setOrder(router.query?.orderBy ?? ''); + }, [router.query]); return ( <> @@ -159,23 +221,24 @@ const ProductFilterDesktop = ({ </Box> <AccordionIcon /> </AccordionButton> - <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}> + <Stack gap={3} direction='column' maxH='240px' overflow='auto'> + {normBrands.length > 0 ? ( + normBrands.map((b) => ( + <div className='flex items-center gap-2' key={b.id}> <Checkbox - isChecked={brandValues.includes(brand.brand)} + isChecked={brandValues.includes(String(b.id))} onChange={handleBrandsChange} - value={brand.brand} + value={String(b.id)} // idealnya ID brand size='md' > <div className='flex items-center gap-2'> - <span>{brand.brand} </span> - <span className='text-sm text-gray-600'> - ({brand.qty}) - </span> + <span>{b.name}</span> + {b.qty !== undefined && ( + <span className='text-sm text-gray-600'> + ({b.qty}) + </span> + )} </div> </Checkbox> </div> @@ -197,23 +260,20 @@ const ProductFilterDesktop = ({ </Box> <AccordionIcon /> </AccordionButton> - <AccordionPanel> - <Stack gap={3} direction='column' maxH={'240px'} overflow='auto'> - {categories && categories.length > 0 ? ( - categories.map((category, index) => ( - <div className='flex items-center gap-2' key={index}> + <Stack gap={3} direction='column' maxH='240px' overflow='auto'> + {(categories ?? []).length > 0 ? ( + categories.map((c, i) => ( + <div className='flex items-center gap-2' key={i}> <Checkbox - isChecked={categoryValues.includes(category.name)} + isChecked={categoryValues.includes(c.name)} onChange={handleCategoriesChange} - value={category.name} + value={c.name} size='md' > <div className='flex items-center gap-2'> - <span>{category.name} </span> - <span className='text-sm text-gray-600'> - ({category.qty}) - </span> + <span>{c.name}</span> + <span className='text-sm text-gray-600'>({c.qty})</span> </div> </Checkbox> </div> @@ -234,7 +294,6 @@ const ProductFilterDesktop = ({ </Box> <AccordionIcon /> </AccordionButton> - <AccordionPanel paddingY={4}> <VStack gap={4}> <InputGroup> @@ -243,32 +302,34 @@ const ProductFilterDesktop = ({ type='number' placeholder='Harga minimum' value={priceFrom} - onChange={(e) => setPriceFrom(e.target.value)} + onChange={onPriceFromInput} + onKeyDown={handlePriceKeyDown} // ⟵ apply saat Enter /> </InputGroup> + <InputGroup> <InputLeftAddon>Rp</InputLeftAddon> <Input type='number' - placeholder='Harga maximum' + placeholder='Harga maksimum' value={priceTo} - onChange={(e) => setPriceTo(e.target.value)} + onChange={onPriceToInput} + onKeyDown={handlePriceKeyDown} // ⟵ apply saat Enter /> </InputGroup> + <div className='grid grid-cols-2 gap-x-3 gap-y-2'> - {priceRange.map((price, i) => ( + {priceRange.map((p, i) => ( <button key={i} onClick={() => - handlePriceFromChange(price.priceFrom, price.priceTo, i) + handlePriceRangeClick(p.priceFrom, p.priceTo, i) } className={`w-full border ${ i === activeRange ? 'border-red-600' : 'border-gray-400' - } - py-2 p-3 rounded-full text-sm whitespace-nowrap`} + } py-2 p-3 rounded-full text-sm whitespace-nowrap`} > - {formatCurrency(price.priceFrom)} -{' '} - {formatCurrency(price.priceTo)} + {formatCurrency(p.priceFrom)} - {formatCurrency(p.priceTo)} </button> ))} </div> @@ -279,27 +340,22 @@ const ProductFilterDesktop = ({ {/* <AccordionItem> <AccordionButton padding={[2, 4]}> <Box as='span' flex='1' textAlign='left' fontWeight='semibold'> - Ketersedian Stok + Ketersediaan Stok </Box> <AccordionIcon /> </AccordionButton> - <AccordionPanel paddingY={4}> <Checkbox isChecked={stock === 'ready stock'} onChange={handleReadyStockChange} - value={'ready stock'} + value='ready stock' size='md' > - Ketersedian Stock + Ready Stock </Checkbox> </AccordionPanel> </AccordionItem> */} </Accordion> - - <Button className='w-full mt-6' colorScheme='red' onClick={handleSubmit}> - Terapkan - </Button> </> ); }; diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index 2fb3138a..850d00cc 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -1,12 +1,12 @@ import NextImage from 'next/image'; import { useRouter } from 'next/router'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState, useRef } from 'react'; import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react'; import axios from 'axios'; import _ from 'lodash'; import { toQuery } from 'lodash-contrib'; - +import { FunnelIcon, AdjustmentsHorizontalIcon } from '@heroicons/react/24/outline'; import odooApi from '@/core/api/odooApi'; import searchSpellApi from '@/core/api/searchSpellApi'; import Link from '@/core/components/elements/Link/Link'; @@ -50,7 +50,33 @@ const ProductSearch = ({ const categoryId = getIdFromSlug(prefixUrl); const [data, setData] = useState([]); const [dataLob, setDataLob] = useState([]); + const appliedDefaultBrandOrder = useRef(false); + if (defaultBrand) query.brand = defaultBrand.toLowerCase(); + useEffect(() => { + if (!router.isReady) return; + + const onBrandsPage = router.pathname.includes('brands'); + const hasOrder = typeof router.query?.orderBy === 'string' && router.query.orderBy !== ''; + + if (onBrandsPage && !hasOrder && !appliedDefaultBrandOrder.current) { + let params = { + ...router.query, + orderBy: 'popular', + }; + params = _.pickBy(params, _.identity); + const qs = toQuery(params); + + // ganti URL tanpa nambah history & tanpa full reload + router.replace(`${prefixUrl}?${qs}`, undefined, { shallow: true }); + + // sinkronkan state lokal + setOrderBy('popular'); + + appliedDefaultBrandOrder.current = true; + } + }, [router.isReady, router.pathname, router.query?.orderBy, prefixUrl]); + const dataIdCategories = []; useEffect(() => { if (prefixUrl.includes('category')) { @@ -84,7 +110,7 @@ const ProductSearch = ({ if (router.asPath.includes('penawaran')) { query = { ...query, - fq:`flashsale_id_i:${router.query.penawaran} AND flashsale_price_f:[1 TO *]`, + fq: `flashsale_id_i:${router.query.penawaran} AND flashsale_price_f:[1 TO *]`, orderBy: 'flashsale-discount-desc', }; setFinalQuery(query); @@ -404,9 +430,7 @@ const ProductSearch = ({ <div className='p-4 pt-0'> {isNotReadyStockPage && isBrand && isBrand.logo && ( <div className='mb-3'> - <h1 className='mb-2 font-semibold text-h-sm'> - Brand Pencarian {q} - </h1> + <h1 className='mb-2 font-semibold text-h-sm'>Brand Pencarian {q}</h1> <Link href={createSlug('/shop/brands/', isBrand.name, isBrand.id)} className='inline' @@ -419,7 +443,9 @@ const ProductSearch = ({ </Link> </div> )} + <h1 className='mb-2 font-semibold text-h-sm'>Produk</h1> + <FilterChoicesComponent brandValues={brandValues} categoryValues={categoryValues} @@ -428,6 +454,7 @@ const ProductSearch = ({ handleDeleteFilter={handleDeleteFilter} /> + {/* info jumlah hasil */} <div className='mb-2 leading-6 text-gray_r-11'> {!spellings ? ( <> @@ -435,8 +462,7 @@ const ProductSearch = ({ {pageCount > 1 ? ( <> {productStart + 1}- - {parseInt(productStart) + parseInt(productRows) > - productFound + {parseInt(productStart) + parseInt(productRows) > productFound ? productFound : parseInt(productStart) + parseInt(productRows)} dari @@ -448,8 +474,7 @@ const ProductSearch = ({ produk{' '} {query.q && ( <> - untuk pencarian{' '} - <span className='font-semibold'>{query.q}</span> + untuk pencarian <span className='font-semibold'>{query.q}</span> </> )} </> @@ -457,37 +482,37 @@ const ProductSearch = ({ SpellingComponent )} </div> - <LobSectionCategory categories={dataLob} /> - <CategorySection categories={dataCategories} /> {productFound > 0 && ( - <div className='flex items-center gap-x-2 mb-5 justify-between'> + <div className='flex items-center gap-x-2 mt-2 mb-3 justify-end'> <div> <button - className='btn-light py-2 px-5 h-[40px]' + aria-label='Filter' + title='Filter' onClick={popup.activate} + className='btn-light w-fit flex items-center justify-center rounded-md' > - Filter + <AdjustmentsHorizontalIcon className='w-5 h-5' /> </button> </div> - <div className=''> + <div> <select name='limit' - className='form-input w-24' + className='form-input w-20' value={router.query?.limit || ''} onChange={(e) => handleLimit(e)} > {numRows.map((option, index) => ( <option key={index} value={option}> - {' '} - {option}{' '} + {option} </option> ))} </select> </div> </div> )} - + {!!dataLob?.length && <LobSectionCategory categories={dataLob} />} + {!!dataCategories?.length && <CategorySection categories={dataCategories} />} <div className='grid grid-cols-2 gap-3'> {products && products.map((product) => ( @@ -499,7 +524,6 @@ const ProductSearch = ({ pageCount={pageCount} currentPage={parseInt(page)} url={`${prefixUrl}?${toQuery(_.omit(query, ['page', 'fq']))}`} - // url={prefixUrl.includes('category') || prefixUrl.includes('lob')? `${prefixUrl}?${toQuery(_.omit(finalQuery, ['page']))}` : `${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} className='mt-6 mb-2' /> @@ -597,7 +621,7 @@ const ProductSearch = ({ <> {productStart + 1}- {parseInt(productStart) + parseInt(productRows) > - productFound + productFound ? productFound : parseInt(productStart) + parseInt(productRows)} dari @@ -673,8 +697,8 @@ const ProductSearch = ({ href={ query?.q ? whatsappUrl('productSearch', { - name: query.q, - }) + name: query.q, + }) : whatsappUrl() } className='text-danger-500' @@ -759,9 +783,9 @@ const FilterChoicesComponent = ({ </Tag> )} {brandValues?.length > 0 || - categoryValues?.length > 0 || - priceFrom || - priceTo ? ( + categoryValues?.length > 0 || + priceFrom || + priceTo ? ( <span> <button className='btn-transparent py-2 px-5 h-[40px] text-red-700' |
