From 4d5dbcda7334c90b54ad25d828c8cf2a6433b682 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 19 Dec 2025 15:25:10 +0700 Subject: add keyword to breadcrumb --- src/lib/category/components/Breadcrumb.jsx | 72 +++++++++++++++++++----------- 1 file changed, 46 insertions(+), 26 deletions(-) (limited to 'src/lib') diff --git a/src/lib/category/components/Breadcrumb.jsx b/src/lib/category/components/Breadcrumb.jsx index 50557c3e..acd2cbff 100644 --- a/src/lib/category/components/Breadcrumb.jsx +++ b/src/lib/category/components/Breadcrumb.jsx @@ -11,17 +11,19 @@ import React from 'react'; import { useQuery } from 'react-query'; import useDevice from '@/core/hooks/useDevice'; -const Breadcrumb = ({ categoryId }) => { +const Breadcrumb = ({ categoryId, currentLabel }) => { const breadcrumbs = useQuery( ['category-breadcrumbs', categoryId], async () => await odooApi('GET', `/api/v1/category/${categoryId}/category-breadcrumb`) ); - const { isDesktop, isMobile } = useDevice(); + const { isDesktop, isMobile } = useDevice(); const items = breadcrumbs.data ?? []; - const lastIdx = items.length - 1; + /* ========================= + DESKTOP + ========================== */ if (isDesktop) { return (
@@ -54,14 +56,12 @@ const Breadcrumb = ({ categoryId }) => { {/* Categories */} {items.map((category, index) => { - const isLast = index === lastIdx; + const isLastCategory = index === items.length - 1; + const isClickable = currentLabel || !isLastCategory; + return ( - - {isLast ? ( - - {category.name} - - ) : ( + + {isClickable ? ( { > {category.name} + ) : ( + + {category.name} + )} ); })} + + {/* Searchkey / Current Page */} + {currentLabel && ( + + + {currentLabel} + + + )}
); } + /* ========================= + MOBILE + ========================== */ if (isMobile) { - const items = breadcrumbs.data ?? []; const n = items.length; - const lastCat = n >= 1 ? items[n - 1] : null; // terakhir (current) - const secondLast = n >= 2 ? items[n - 2] : null; // sebelum current - const beforeSecond = n >= 3 ? items[n - 3] : null; // sebelum secondLast + const lastCat = n >= 1 ? items[n - 1] : null; + const secondLast = n >= 2 ? items[n - 2] : null; + const beforeSecond = n >= 3 ? items[n - 3] : null; + const hiddenText = n >= 3 ? items @@ -97,19 +113,21 @@ const Breadcrumb = ({ categoryId }) => { .join(' / ') : ''; + const finalLabel = currentLabel || lastCat?.name; + return (
/} // lebih rapat + separator={/} spacing='4px' sx={{ '& ol': { display: 'flex', alignItems: 'center', - overflow: 'hidden', // untuk ellipsis - whiteSpace: 'nowrap', // untuk ellipsis - gap: '0', // no extra gap + overflow: 'hidden', + whiteSpace: 'nowrap', + gap: '0', }, '& li': { display: 'inline-flex', alignItems: 'center' }, '& li:not(:last-of-type)': { @@ -117,7 +135,7 @@ const Breadcrumb = ({ categoryId }) => { whiteSpace: 'nowrap', }, '& li:last-of-type': { - flex: '0 1 auto', // jangan ambil full space biar gak keliatan “space kosong” + flex: '0 1 auto', minWidth: 0, }, }} @@ -130,7 +148,7 @@ const Breadcrumb = ({ categoryId }) => { - {/* Jika ada kategori sebelum secondLast, tampilkan '..' (link ke beforeSecond) */} + {/* Ellipsis */} {beforeSecond && ( { )} - {/* secondLast sebagai link (kalau ada) */} + {/* Second last category */} {secondLast && ( { )} - {/* lastCat (current) dengan truncate & lebar dibatasi */} - {lastCat && ( + {/* Current */} + {finalLabel && ( - {lastCat.name} + {finalLabel} )} @@ -183,6 +201,8 @@ const Breadcrumb = ({ categoryId }) => {
); } + + return null; }; export default Breadcrumb; -- cgit v1.2.3 From 3e67e48da0ea88f011f1d7d7390758ab2ad40339 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 18 Feb 2026 15:51:13 +0700 Subject: cr renca --- src/lib/category/components/Breadcrumb.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/lib') diff --git a/src/lib/category/components/Breadcrumb.jsx b/src/lib/category/components/Breadcrumb.jsx index fa2846e4..29bc9c0a 100644 --- a/src/lib/category/components/Breadcrumb.jsx +++ b/src/lib/category/components/Breadcrumb.jsx @@ -11,7 +11,7 @@ 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 () => -- cgit v1.2.3 From 4a85f437cfa7a61bebd341c9a509abccbd64745b Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 18 Feb 2026 17:26:59 +0700 Subject: fix pagination --- src/lib/product/components/ProductSearch.jsx | 74 ++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 10 deletions(-) (limited to 'src/lib') 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 = ({ @@ -729,9 +782,10 @@ const ProductSearch = ({ -- cgit v1.2.3 From adb28e824ca2d05244ad939a273067e5b7e38f76 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 19 Feb 2026 14:25:30 +0700 Subject: done all --- src/lib/product/api/productSearchApi.js | 31 ++++++++- src/lib/utils/batchSolrQueries.js | 113 ++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 src/lib/utils/batchSolrQueries.js (limited to 'src/lib') 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/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 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} 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} ids - Product IDs to query + * @param {number} chunkSize - How many IDs per chunk (default: 100) + * @returns {Array} 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} 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} 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; +}; -- cgit v1.2.3