summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/lib/product/api/productSearchApi.js12
-rw-r--r--src/pages/api/shop/search.js398
-rw-r--r--src/pages/sitemap/products.xml.js13
-rw-r--r--src/pages/sitemap/products/[page].js16
4 files changed, 175 insertions, 264 deletions
diff --git a/src/lib/product/api/productSearchApi.js b/src/lib/product/api/productSearchApi.js
index 8ff8e57d..a84caa3c 100644
--- a/src/lib/product/api/productSearchApi.js
+++ b/src/lib/product/api/productSearchApi.js
@@ -1,11 +1,11 @@
-import _ from 'lodash-contrib'
-import axios from 'axios'
+import _ from 'lodash-contrib';
+import axios from 'axios';
const productSearchApi = async ({ query, operation = 'OR' }) => {
const dataProductSearch = await axios(
`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}&operation=${operation}`
- )
- return dataProductSearch.data
-}
+ );
+ return dataProductSearch.data;
+};
-export default productSearchApi
+export default productSearchApi;
diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js
index 8c9782cb..1b1b6a9c 100644
--- a/src/pages/api/shop/search.js
+++ b/src/pages/api/shop/search.js
@@ -1,207 +1,11 @@
import { productMappingSolr } from '@/utils/solrMapping';
import axios from 'axios';
import camelcaseObjectDeep from 'camelcase-object-deep';
-import searchSuggestApi from '@/core/api/searchSuggestApi';
-import { ECDH } from 'crypto';
-
-export default async function handler(req, res) {
- const {
- q = '*',
- page = 1,
- brand = '',
- category = '',
- priceFrom = 0,
- priceTo = 0,
- orderBy = '',
- operation = 'AND',
- fq = '',
- limit = 30,
- source = '',
- } = req.query;
-
- let { stock = '' } = req.query;
- let paramOrderBy = '';
- switch (orderBy) {
- case 'flashsale-discount-desc':
- paramOrderBy += 'flashsale_discount_f DESC';
- break;
- case 'price-asc':
- paramOrderBy += 'price_tier1_v2_f ASC';
- break;
- case 'price-desc':
- paramOrderBy += 'price_tier1_v2_f DESC';
- break;
- case 'popular':
- paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,';
- break;
- case 'popular-weekly':
- paramOrderBy += 'search_rank_weekly_i DESC';
- break;
- case 'stock':
- paramOrderBy += 'product_rating_f DESC, stock_total_f DESC';
- break;
- case 'flashsale-price-asc':
- paramOrderBy += 'flashsale_price_f ASC';
- break;
- default:
- paramOrderBy += '';
- break;
- }
-
- // let suggestWord = null;
- // let keywords = q;
- // let checkQ = null;
-
- // if (q != '*') {
- // checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/);
- // if (checkQ.length > 1) {
- // const dataSearchSuggest = await axios(
- // `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/suggest?q=${checkQ[1]}`
- // );
- // suggestWord = dataSearchSuggest.data.suggestions[0];
- // }
- // if (suggestWord && suggestWord?.term.split(' ').length <= 1) {
- // keywords = `"${escapeSolrQuery(checkQ[0] + ' ' + suggestWord?.term)}"`;
- // }
- // }
-
- // let newQ = keywords;
-
- let checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/);
- let newQ = escapeSolrQuery(q);
-
- const formattedQuery = `(${newQ
- .split(' ')
- .map((term) => (term.length < 2 ? term : `${term}*`))
- .join(' ')})`;
-
- const mm =
- checkQ.length > 2
- ? checkQ.length > 5
- ? '55%'
- : '85%'
- : `${checkQ.length}`;
-
- const filterQueries = [
- '-publish_b:false',
- 'product_rating_f:[8 TO *]',
- 'price_tier1_v2_f:[1 TO *]',
- ];
-
- if (orderBy === 'stock') {
- filterQueries.push('stock_total_f:[1 TO *]');
- }
-
- if (fq && source != 'similar' && typeof fq != 'string') {
- // filterQueries.push(fq);
- fq.push(...filterQueries);
- }
- const fq_ = filterQueries.join(' AND ');
-
- let keywords = newQ;
- if (source === 'similar' || checkQ.length < 3) {
- if (checkQ.length < 2 || checkQ[1].length < 2) {
- keywords = newQ;
- } else {
- keywords = newQ + '*';
- }
- } else {
- keywords = formattedQuery;
- }
-
- let offset = (page - 1) * limit;
- let parameter = [
- // === Facet disjunctive: exclude filter brand/cat saat hitung facet ===
- 'facet.field={!ex=brand}manufacture_name_s',
- 'facet.field={!ex=cat}category_name',
- 'facet=true',
- 'indent=true',
- `facet.query=${escapeSolrQuery(q)}`,
- `q.op=OR`,
- `q=${keywords}`,
- `defType=edismax`,
- 'qf=name_s description_clean_t category_name manufacture_name_s variants_code_t variants_name_t category_id_ids default_code_s manufacture_id_i category_id_i ',
- `start=${parseInt(offset)}`,
- `rows=${limit}`,
- `sort=${paramOrderBy}`,
- `fq=${encodeURIComponent(fq_)}`,
- `mm=${encodeURIComponent(mm)}`,
- ];
-
- if (priceFrom > 0 || priceTo > 0) {
- parameter.push(
- `fq=price_tier1_v2_f:[${priceFrom == '' ? '*' : priceFrom} TO ${
- priceTo == '' ? '*' : priceTo
- }]`
- );
- }
-
- let { auth } = req.cookies;
- if (auth) {
- auth = JSON.parse(auth);
- if (auth.feature.onlyReadyStock) stock = true;
- }
-
- if (brand) {
- // bentuk ekspresi sama seperti versi kamu, tapi dibungkus tag brand
- const brandExpr = brand
- .split(',')
- .map(
- (manufacturer) =>
- `manufacture_name:"${encodeURIComponent(manufacturer)}"`
- )
- .join(' OR ');
- parameter.push(`fq={!tag=brand}(${brandExpr})`);
- }
-
- if (category) {
- // sama: tag kategori
- const catExpr = category
- .split(',')
- .map((cat) => `category_name:"${encodeURIComponent(cat)}"`)
- .join(' OR ');
- parameter.push(`fq={!tag=cat}(${catExpr})`);
- }
-
- // if (category) parameter.push(`fq=category_name:${capitalizeFirstLetter(category.replace(/,/g, ' OR '))}`)
- if (stock) parameter.push(`fq=stock_total_f:(1 TO *)`);
-
- // Single fq in url params
- if (typeof fq === 'string') parameter.push(`fq=${encodeURIComponent(fq)}`);
- // Multi fq in url params
- if (Array.isArray(fq))
- parameter = parameter.concat(
- fq.map((val) => `fq=${encodeURIComponent(val)}`)
- );
-
- let result = await axios(
- process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&')
- );
-
- try {
- result.data.response.products = productMappingSolr(
- result.data.response.docs,
- auth?.pricelist || false
- );
- result.data.responseHeader.params.start = parseInt(
- result.data.responseHeader.params.start
- );
- result.data.responseHeader.params.rows = parseInt(
- result.data.responseHeader.params.rows
- );
- delete result.data.response.docs;
- result.data = camelcaseObjectDeep(result.data);
- res.status(200).json(result.data);
- } catch (error) {
- res.status(400).json({ error: error.message });
- }
-}
+// helper untuk escape karakter spesial
const escapeSolrQuery = (query) => {
if (query == '*') return query;
-
query = query.replace(/-/g, ' ');
-
const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
const words = query.split(/\s+/);
const escapedWords = words.map((word) => {
@@ -210,64 +14,166 @@ const escapeSolrQuery = (query) => {
}
return word;
});
-
return escapedWords.join(' ');
};
-/*const productResponseMap = (products, pricelist) => {
- return products.map((product) => {
- let price = product.price_tier1_v2_f || 0
- let priceDiscount = product.price_discount_f || 0
- let discountPercentage = product.discount_f || 0
-
- if (pricelist) {
- // const pricelistDiscount = product?.[`price_${pricelist}_f`] || false
- // const pricelistDiscountPerc = product?.[`discount_${pricelist}_f`] || false
-
- // if (pricelistDiscount && pricelistDiscount > 0) priceDiscount = pricelistDiscount
- // if (pricelistDiscountPerc && pricelistDiscountPerc > 0)
- // discountPercentage = pricelistDiscountPerc
+export default async function handler(req, res) {
+ try {
+ const {
+ q = '*',
+ page = 1,
+ brand = '',
+ category = '',
+ priceFrom = 0,
+ priceTo = 0,
+ orderBy = '',
+ operation = 'AND',
+ fq = '',
+ limit = 30,
+ source = '', // <-- penting
+ } = req.query;
+
+ console.log('🔍 API /shop/search → source:', source); // debug biar tahu kebaca atau enggak
+
+ // --- 1️⃣ MODE SITEMAP: ambil semua produk tanpa filter ---
+ if (source === 'sitemap') {
+ const offset = (page - 1) * limit;
+ const parameter = ['q=*:*', `rows=${limit}`, `start=${offset}`];
+
+ const solrUrl =
+ process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&');
+
+ console.log('[SOLR QUERY SITEMAP]', solrUrl);
+
+ const result = await axios(solrUrl);
+
+ result.data.response.products = productMappingSolr(
+ result.data.response.docs,
+ false
+ );
+ delete result.data.response.docs;
+ result.data = camelcaseObjectDeep(result.data);
+
+ return res.status(200).json(result.data);
+ }
- price = product?.[`price_${pricelist}_f`] || 0
+ // --- 2️⃣ MODE NORMAL (search biasa di website) ---
+ let checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/);
+ let newQ = escapeSolrQuery(q);
+
+ const formattedQuery = `(${newQ
+ .split(' ')
+ .map((term) => (term.length < 2 ? term : `${term}*`))
+ .join(' ')})`;
+
+ const mm =
+ checkQ.length > 2
+ ? checkQ.length > 5
+ ? '55%'
+ : '85%'
+ : `${checkQ.length}`;
+
+ // filter default (mode normal)
+ const filterQueries = [
+ '-publish_b:false',
+ 'product_rating_f:[8 TO *]',
+ 'price_tier1_v2_f:[1 TO *]',
+ ];
+ const fq_ = filterQueries.join(' AND ');
+
+ let keywords = newQ;
+ if (checkQ.length >= 3) keywords = formattedQuery;
+
+ let offset = (page - 1) * limit;
+
+ let paramOrderBy = '';
+ switch (orderBy) {
+ case 'flashsale-discount-desc':
+ paramOrderBy += 'flashsale_discount_f DESC';
+ break;
+ case 'price-asc':
+ paramOrderBy += 'price_tier1_v2_f ASC';
+ break;
+ case 'price-desc':
+ paramOrderBy += 'price_tier1_v2_f DESC';
+ break;
+ case 'popular':
+ paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,';
+ break;
+ case 'popular-weekly':
+ paramOrderBy += 'search_rank_weekly_i DESC';
+ break;
+ case 'stock':
+ paramOrderBy += 'product_rating_f DESC, stock_total_f DESC';
+ break;
+ case 'flashsale-price-asc':
+ paramOrderBy += 'flashsale_price_f ASC';
+ break;
+ default:
+ paramOrderBy += '';
+ break;
}
- if (product?.flashsale_id_i > 0) {
- price = product?.flashsale_base_price_f || 0
- priceDiscount = product?.flashsale_price_f || 0
- discountPercentage = product?.flashsale_discount_f || 0
+ // parameter query normal
+ let parameter = [
+ 'facet.field={!ex=brand}manufacture_name_s',
+ 'facet.field={!ex=cat}category_name',
+ 'facet=true',
+ 'indent=true',
+ `facet.query=${escapeSolrQuery(q)}`,
+ `q.op=${operation}`,
+ `q=${keywords}`,
+ `defType=edismax`,
+ 'qf=name_s description_clean_t category_name manufacture_name_s variants_code_t variants_name_t category_id_ids default_code_s manufacture_id_i category_id_i',
+ `start=${parseInt(offset)}`,
+ `rows=${limit}`,
+ `sort=${paramOrderBy}`,
+ `fq=${encodeURIComponent(fq_)}`,
+ `mm=${encodeURIComponent(mm)}`,
+ ];
+
+ if (priceFrom > 0 || priceTo > 0) {
+ parameter.push(
+ `fq=price_tier1_v2_f:[${priceFrom || '*'} TO ${priceTo || '*'}]`
+ );
}
- let productMapped = {
- id: product.product_id_i || '',
- image: product.image_s || '',
- code: product.default_code_s || '',
- name: product.name_s || '',
- lowestPrice: { price, priceDiscount, discountPercentage },
- variantTotal: product.variant_total_i || 0,
- stockTotal: product.stock_total_f || 0,
- weight: product.weight_f || 0,
- manufacture: {},
- categories: [],
- flashSale: {
- id: product?.flashsale_id_i,
- name: product?.product?.flashsale_name_s,
- tag : product?.flashsale_tag_s || 'FLASH SALE'
- }
+ if (brand) {
+ const brandExpr = brand
+ .split(',')
+ .map(
+ (manufacturer) =>
+ `manufacture_name:"${encodeURIComponent(manufacturer)}"`
+ )
+ .join(' OR ');
+ parameter.push(`fq={!tag=brand}(${brandExpr})`);
}
- if (product.manufacture_id_i && product.manufacture_name_s) {
- productMapped.manufacture = {
- id: product.manufacture_id_i || '',
- name: product.manufacture_name_s || ''
- }
+ if (category) {
+ const catExpr = category
+ .split(',')
+ .map((cat) => `category_name:"${encodeURIComponent(cat)}"`)
+ .join(' OR ');
+ parameter.push(`fq={!tag=cat}(${catExpr})`);
}
- productMapped.categories = [
- {
- id: product.category_id_i || '',
- name: product.category_name_s || ''
- }
- ]
- return productMapped
- })
-}*/
+ const solrUrl =
+ process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&');
+
+ console.log('[SOLR QUERY NORMAL]', solrUrl);
+
+ const result = await axios(solrUrl);
+
+ result.data.response.products = productMappingSolr(
+ result.data.response.docs,
+ false
+ );
+ delete result.data.response.docs;
+ result.data = camelcaseObjectDeep(result.data);
+
+ res.status(200).json(result.data);
+ } catch (error) {
+ console.error('[ERROR /shop/search]', error.message);
+ res.status(400).json({ error: error.message });
+ }
+}
diff --git a/src/pages/sitemap/products.xml.js b/src/pages/sitemap/products.xml.js
index 5ed6b759..b3db3b93 100644
--- a/src/pages/sitemap/products.xml.js
+++ b/src/pages/sitemap/products.xml.js
@@ -1,12 +1,18 @@
import productSearchApi from '@/lib/product/api/productSearchApi';
import { create } from 'xmlbuilder';
-import _ from 'lodash-contrib';
export async function getServerSideProps({ res }) {
const baseUrl = process.env.SELF_HOST + '/sitemap/products';
const limit = 2500;
- const query = { limit };
- const products = await productSearchApi({ query: _.toQuery(query) });
+
+ // ⬇️ Ganti ke URLSearchParams biar key "source" gak hilang
+ const params = new URLSearchParams({
+ limit,
+ source: 'sitemap',
+ }).toString();
+
+ const products = await productSearchApi({ query: params });
+
const pageCount = Math.ceil(products.response.numFound / limit);
const pages = Array.from({ length: pageCount }, (_, i) => i + 1);
const sitemapIndex = create('sitemapindex', { encoding: 'UTF-8' }).att(
@@ -15,7 +21,6 @@ export async function getServerSideProps({ res }) {
);
const date = new Date();
- // const date = '2025-10-30';
pages.forEach((page) => {
const sitemap = sitemapIndex.ele('sitemap');
sitemap.ele('loc', `${baseUrl}/${page}.xml`);
diff --git a/src/pages/sitemap/products/[page].js b/src/pages/sitemap/products/[page].js
index 3603d64c..846a2df9 100644
--- a/src/pages/sitemap/products/[page].js
+++ b/src/pages/sitemap/products/[page].js
@@ -1,27 +1,27 @@
import productSearchApi from '@/lib/product/api/productSearchApi';
import { create } from 'xmlbuilder';
-import _ from 'lodash-contrib';
import { createSlug } from '@/core/utils/slug';
export async function getServerSideProps({ query, res }) {
const baseUrl = process.env.SELF_HOST + '/shop/product/';
const { page } = query;
const limit = 2500;
- const queries = {
+
+ // ⬇️ Ganti juga ke URLSearchParams
+ const params = new URLSearchParams({
limit,
page: page.replace('.xml', ''),
- // '-publish_b': false,
- // product_rating_f: '[8 TO *]',
- // price_tier1_v2_f: '[1 TO *]',
- };
- const products = await productSearchApi({ query: _.toQuery(queries) });
+ source: 'sitemap',
+ }).toString();
+
+ const products = await productSearchApi({ query: params });
+
const sitemap = create('urlset', { encoding: 'utf-8' }).att(
'xmlns',
'http://www.sitemaps.org/schemas/sitemap/0.9'
);
const date = new Date();
- // const date = '2025-10-30';
products.response.products.forEach((product) => {
const url = sitemap.ele('url');
url.ele('loc', createSlug(baseUrl, product.name, product.id));