summaryrefslogtreecommitdiff
path: root/src/lib/product
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/product')
-rw-r--r--src/lib/product/components/Product/ProductDesktopVariant.jsx136
-rw-r--r--src/lib/product/components/Product/ProductMobileVariant.jsx188
-rw-r--r--src/lib/product/components/ProductCard.jsx2
-rw-r--r--src/lib/product/components/ProductFilterDesktop.jsx348
-rw-r--r--src/lib/product/components/ProductSearch.jsx78
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)}
&nbsp;dari&nbsp;
@@ -448,8 +474,7 @@ const ProductSearch = ({
&nbsp;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)}
&nbsp;dari&nbsp;
@@ -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'