summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/category/components/Breadcrumb.jsx46
-rw-r--r--src/lib/product/api/productSearchApi.js31
-rw-r--r--src/lib/product/components/ProductSearch.jsx74
-rw-r--r--src/lib/utils/batchSolrQueries.js113
4 files changed, 238 insertions, 26 deletions
diff --git a/src/lib/category/components/Breadcrumb.jsx b/src/lib/category/components/Breadcrumb.jsx
index 8579bb14..29bc9c0a 100644
--- a/src/lib/category/components/Breadcrumb.jsx
+++ b/src/lib/category/components/Breadcrumb.jsx
@@ -11,17 +11,18 @@ import React from 'react';
import { useQuery } from 'react-query';
import useDevice from '@/core/hooks/useDevice';
-const Breadcrumb = ({ categoryId, shortDesc }) => {
+const Breadcrumb = ({ categoryId, shortDesc, currentLabel }) => {
const breadcrumbs = useQuery(
['category-breadcrumbs', categoryId],
async () =>
- await odooApi('GET', `/api/v1/category/${categoryId}/category-breadcrumb`)
+ await odooApi(
+ 'GET',
+ `/api/v1/category/${categoryId}/category-breadcrumb`,
+ ),
);
const { isDesktop, isMobile } = useDevice();
-
const items = breadcrumbs.data ?? [];
- const lastIdx = items.length - 1;
/* =========================
DESKTOP
@@ -58,29 +59,40 @@ const Breadcrumb = ({ categoryId, shortDesc }) => {
{/* Categories */}
{items.map((category, index) => {
- const isLast = index === lastIdx;
+ const isLastCategory = index === items.length - 1;
+ const isClickable = currentLabel || !isLastCategory;
+
return (
- <BreadcrumbItem key={index} isCurrentPage={isLast}>
- {isLast ? (
- <BreadcrumbLink className='block whitespace-normal break-words md:whitespace-nowrap'>
- {category.name}
- </BreadcrumbLink>
- ) : (
+ <BreadcrumbItem key={index} isCurrentPage={!isClickable}>
+ {isClickable ? (
<BreadcrumbLink
as={Link}
href={createSlug(
'/shop/category/',
category.name,
- category.id
+ category.id,
)}
className='!text-danger-500'
>
{category.name}
</BreadcrumbLink>
+ ) : (
+ <BreadcrumbLink className='block whitespace-normal break-words md:whitespace-nowrap'>
+ {category.name}
+ </BreadcrumbLink>
)}
</BreadcrumbItem>
);
})}
+
+ {/* Searchkey / Current Page */}
+ {currentLabel && (
+ <BreadcrumbItem isCurrentPage>
+ <BreadcrumbLink className='block whitespace-normal break-words md:whitespace-nowrap'>
+ {currentLabel}
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+ )}
</ChakraBreadcrumb>
</Skeleton>
{shortDesc && (
@@ -119,6 +131,8 @@ const Breadcrumb = ({ categoryId, shortDesc }) => {
.join(' / ')
: '';
+ const finalLabel = currentLabel || lastCat?.name;
+
return (
<div className='container mx-auto py-2 mt-2'>
<Skeleton isLoaded={!breadcrumbs.isLoading} className='w-full'>
@@ -152,6 +166,7 @@ const Breadcrumb = ({ categoryId, shortDesc }) => {
</BreadcrumbLink>
</BreadcrumbItem>
+ {/* Jika ada kategori sebelum secondLast, tampilkan '..' (link ke beforeSecond) */}
{beforeSecond && (
<BreadcrumbItem>
<BreadcrumbLink
@@ -159,7 +174,7 @@ const Breadcrumb = ({ categoryId, shortDesc }) => {
href={createSlug(
'/shop/category/',
beforeSecond.name,
- beforeSecond.id
+ beforeSecond.id,
)}
title={hiddenText}
className='!text-danger-500'
@@ -176,7 +191,7 @@ const Breadcrumb = ({ categoryId, shortDesc }) => {
href={createSlug(
'/shop/category/',
secondLast.name,
- secondLast.id
+ secondLast.id,
)}
className='!text-danger-500'
>
@@ -185,6 +200,7 @@ const Breadcrumb = ({ categoryId, shortDesc }) => {
</BreadcrumbItem>
)}
+ {/* lastCat (current) dengan truncate & lebar dibatasi */}
{lastCat && (
<BreadcrumbItem isCurrentPage>
<span
@@ -192,7 +208,7 @@ const Breadcrumb = ({ categoryId, shortDesc }) => {
style={{ maxWidth: '60vw' }}
title={lastCat.name}
>
- {lastCat.name}
+ {finalLabel}
</span>
</BreadcrumbItem>
)}
diff --git a/src/lib/product/api/productSearchApi.js b/src/lib/product/api/productSearchApi.js
index a84caa3c..1a6ad36a 100644
--- a/src/lib/product/api/productSearchApi.js
+++ b/src/lib/product/api/productSearchApi.js
@@ -2,8 +2,37 @@ import _ from 'lodash-contrib';
import axios from 'axios';
const productSearchApi = async ({ query, operation = 'OR' }) => {
+ // Use POST for large product ID arrays to avoid URL length limits
+ // GET request URL limit is typically 2KB-8KB; switch to POST if query string is large
+ const QUERY_SIZE_THRESHOLD = 2000; // Switch to POST if query > 2KB
+
+ if (query.length > QUERY_SIZE_THRESHOLD) {
+ console.log(
+ `[productSearchApi] Large query (${query.length} chars), using POST`,
+ );
+
+ // Parse query string into object for POST body
+ const params = new URLSearchParams(query);
+ const bodyData = {
+ ...Object.fromEntries(params),
+ operation,
+ };
+
+ const dataProductSearch = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search`,
+ bodyData,
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+ return dataProductSearch.data;
+ }
+
+ // Small query, use standard GET request
const dataProductSearch = await axios(
- `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}&operation=${operation}`
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}&operation=${operation}`,
);
return dataProductSearch.data;
};
diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx
index c73c7036..3e667966 100644
--- a/src/lib/product/components/ProductSearch.jsx
+++ b/src/lib/product/components/ProductSearch.jsx
@@ -44,7 +44,7 @@ const ProductSearch = ({
const { page = 1 } = query;
const [q, setQ] = useState(query?.q || '*');
const [search, setSearch] = useState(query?.q || '*');
- const [limit, setLimit] = useState(router.query?.limit || 30);
+ const [limit, setLimit] = useState(parseInt(router.query?.limit) || 30);
const [orderBy, setOrderBy] = useState(router.query?.orderBy);
const [finalQuery, setFinalQuery] = useState({});
const [queryFinal, setQueryFinal] = useState({});
@@ -86,6 +86,21 @@ const ProductSearch = ({
}
}, [router.isReady, router.pathname, router.query?.orderBy, prefixUrl]);
+ // 🔹 Sync limit state with router.query
+ useEffect(() => {
+ if (!router.isReady) return;
+ const newLimit = parseInt(router.query?.limit) || 30;
+ setLimit(newLimit);
+ }, [router.query?.limit, router.isReady]);
+
+ // 🔹 Sync orderBy state with router.query
+ useEffect(() => {
+ if (!router.isReady) return;
+ if (router.query?.orderBy) {
+ setOrderBy(router.query.orderBy);
+ }
+ }, [router.query?.orderBy, router.isReady]);
+
const dataIdCategories = [];
useEffect(() => {
if (prefixUrl.includes('category')) {
@@ -180,20 +195,55 @@ const ProductSearch = ({
}
};
fetchCategoryData();
+ } else if (query?.from === 'searchkey' && query?.ids) {
+ const newQuery = {
+ ids: query.ids,
+ from: 'searchkey',
+ page: router.query.page ? router.query.page : 1,
+ category: router.query.category ? router.query.category : '',
+ priceFrom: router.query.priceFrom ? router.query.priceFrom : '',
+ priceTo: router.query.priceTo ? router.query.priceTo : '',
+ limit: router.query.limit ? router.query.limit : '',
+ orderBy: router.query.orderBy ? router.query.orderBy : '',
+ };
+ setFinalQuery(newQuery);
}
- }, [dataCategoriesProduct, dataLob]);
+ }, [dataCategoriesProduct, dataLob, query?.from, query?.ids, router.query]);
useEffect(() => {
if (
prefixUrl.includes('category') ||
prefixUrl.includes('lob') ||
+ query?.from === 'searchkey' ||
router.asPath.includes('penawaran')
) {
- setQueryFinal({ ...finalQuery, q, limit, orderBy });
+ setQueryFinal({
+ ...finalQuery,
+ q,
+ limit,
+ orderBy,
+ page: router.query.page || 1,
+ });
} else {
- setQueryFinal({ ...query, q, limit, orderBy });
+ setQueryFinal({
+ ...query,
+ q,
+ limit,
+ orderBy,
+ page: router.query.page || 1,
+ });
}
- }, [prefixUrl, dataCategoriesProduct, query, finalQuery]);
+ }, [
+ prefixUrl,
+ dataCategoriesProduct,
+ query,
+ finalQuery,
+ router.query,
+ router.query.page,
+ limit,
+ orderBy,
+ q,
+ ]);
const { productSearch } = useProductSearch({
query: queryFinal,
@@ -339,6 +389,7 @@ const ProductSearch = ({
let params = {
...router.query,
limit: e.target.value,
+ page: 1, // Reset to page 1 when limit changes
};
params = _.pickBy(params, _.identity);
params = toQuery(params);
@@ -541,8 +592,10 @@ const ProductSearch = ({
<Pagination
pageCount={pageCount}
- currentPage={parseInt(page)}
- url={`${prefixUrl}?${toQuery(_.omit(query, ['page', 'fq']))}`}
+ currentPage={
+ router.query.page ? parseInt(router.query.page, 10) : 1
+ }
+ url={`${prefixUrl}?${toQuery(_.omit(router.query, ['page', 'ids', 'from']))}`}
className='mt-6 mb-2'
/>
@@ -729,9 +782,10 @@ const ProductSearch = ({
<Pagination
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']))}`}
+ currentPage={
+ router.query.page ? parseInt(router.query.page, 10) : 1
+ }
+ url={`${prefixUrl}?${toQuery(_.omit(router.query, ['page', 'ids', 'from']))}`}
className='!justify-end'
/>
</div>
diff --git a/src/lib/utils/batchSolrQueries.js b/src/lib/utils/batchSolrQueries.js
new file mode 100644
index 00000000..486f1778
--- /dev/null
+++ b/src/lib/utils/batchSolrQueries.js
@@ -0,0 +1,113 @@
+/**
+ * Batch utility functions for handling large product ID arrays in Solr queries
+ * Prevents URL length limit errors when querying with >100 product IDs
+ */
+
+/**
+ * Split an array into chunks of specified size
+ * @param {Array} array - Array to split
+ * @param {number} size - Chunk size (default: 100)
+ * @returns {Array<Array>} Array of chunks
+ */
+export const chunkArray = (array, size = 100) => {
+ if (!Array.isArray(array) || array.length === 0) return [];
+
+ const chunks = [];
+ for (let i = 0; i < array.length; i += size) {
+ chunks.push(array.slice(i, i + size));
+ }
+ return chunks;
+};
+
+/**
+ * Build a product ID OR clause for Solr query
+ * @param {Array<string|number>} ids - Array of product IDs
+ * @returns {string} Formatted OR clause: "product_id_i:123 OR product_id_i:456..."
+ */
+export const buildProductIdOrClause = (ids) => {
+ if (!Array.isArray(ids) || ids.length === 0) {
+ return '*:*';
+ }
+ return ids.map((id) => `product_id_i:${id}`).join(' OR ');
+};
+
+/**
+ * Validate query size to prevent exceeding HTTP limits
+ * @param {string} query - Query string to validate
+ * @param {number} maxSize - Maximum allowed size in characters (default: 6000)
+ * @returns {Object} { valid: boolean, message: string, size: number }
+ */
+export const validateQuerySize = (query, maxSize = 6000) => {
+ const size = query.length;
+ return {
+ valid: size <= maxSize,
+ message:
+ size > maxSize
+ ? `Query size ${size} exceeds limit of ${maxSize}`
+ : `Query size ${size} is within limit`,
+ size,
+ };
+};
+
+/**
+ * Build batched Solr query parameters for large ID arrays
+ * Chunks IDs into groups and creates separate OR clauses
+ * @param {Array<string|number>} ids - Product IDs to query
+ * @param {number} chunkSize - How many IDs per chunk (default: 100)
+ * @returns {Array<string>} Array of OR clauses, one per chunk
+ */
+export const buildBatchedOrClauses = (ids, chunkSize = 100) => {
+ if (!Array.isArray(ids) || ids.length === 0) {
+ return ['*:*'];
+ }
+
+ const chunks = chunkArray(ids, chunkSize);
+
+ if (chunks.length === 1) {
+ // Single chunk, return standard OR clause
+ return [buildProductIdOrClause(ids)];
+ }
+
+ // Multiple chunks: return OR clauses wrapped with parentheses for combining
+ return chunks.map((chunk) => `(${buildProductIdOrClause(chunk)})`);
+};
+
+/**
+ * Combine multiple OR clauses into a single query (for Solr)
+ * @param {Array<string>} orClauses - Array of OR clauses
+ * @returns {string} Combined query with OR between clauses
+ */
+export const combineOrClauses = (orClauses) => {
+ if (!Array.isArray(orClauses) || orClauses.length === 0) {
+ return '*:*';
+ }
+ if (orClauses.length === 1) {
+ return orClauses[0];
+ }
+ return orClauses.join(' OR ');
+};
+
+/**
+ * Merge Solr response documents from multiple queries
+ * Removes duplicates based on product_id_i
+ * @param {Array<Array>} responseArrays - Array of response.docs arrays
+ * @returns {Array} Merged and deduplicated docs
+ */
+export const mergeSolrResults = (responseArrays) => {
+ const seen = new Set();
+ const merged = [];
+
+ responseArrays.forEach((docs) => {
+ if (Array.isArray(docs)) {
+ docs.forEach((doc) => {
+ const id = doc.product_id_i;
+ if (id && !seen.has(id)) {
+ seen.add(id);
+ merged.push(doc);
+ }
+ });
+ }
+ });
+
+ return merged;
+};