summaryrefslogtreecommitdiff
path: root/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'src/pages')
-rw-r--r--src/pages/api/shop/generate-recomendation.js64
-rw-r--r--src/pages/api/shop/search.js23
-rw-r--r--src/pages/index.jsx48
-rw-r--r--src/pages/my/recomendation/api/recomendation.js17
-rw-r--r--src/pages/my/recomendation/components/products-recomendatison.jsx477
-rw-r--r--src/pages/my/recomendation/index.jsx26
-rw-r--r--src/pages/shop/brands/[slug].jsx6
-rw-r--r--src/pages/shop/promo/[slug].tsx523
-rw-r--r--src/pages/shop/promo/index.jsx40
9 files changed, 1197 insertions, 27 deletions
diff --git a/src/pages/api/shop/generate-recomendation.js b/src/pages/api/shop/generate-recomendation.js
new file mode 100644
index 00000000..dce8ae72
--- /dev/null
+++ b/src/pages/api/shop/generate-recomendation.js
@@ -0,0 +1,64 @@
+import { productMappingSolr } from '@/utils/solrMapping'
+import axios from 'axios';
+import camelcaseObjectDeep from 'camelcase-object-deep';
+
+export default async function handler(req, res) {
+ const { q = null, op = 'AND' } = req.query
+
+ if (!q) {
+ return res.status(422).json({ error: 'parameter missing' })
+ }
+
+ /*let parameter = [
+ `q=${escapeSolrQuery(q)}`,
+ `q.op=${op}`,
+ `indent=true`,
+ `fq=-publish_b:false`,
+ `qf=name_s^2 description_s`,
+ `facetch=true`,
+ `fq=price_tier1_v2_f:[1 TO *]`,
+ `rows=10`,
+ `sort=product_rating_f DESC, price_discount_f DESC`,
+ ];
+ let result = await axios(
+ process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&')
+ );*/
+ let parameter = [
+ `q=${q}`,
+ `q.op=${op}`,
+ `debugQuery=on`,
+ `defType=edismax`,
+ `df=display_name_s`,
+ `fq=-publish_b:false`,
+ `rows=5`,
+ ];
+ if(op == 'AND'){
+ parameter.push(`sort=product_rating_f DESC, price_discount_f DESC`);
+ parameter.push(`rows=1`);
+ }
+
+ let result = await axios(
+ process.env.SOLR_HOST + '/solr/recommendation/select?' + parameter.join('&')
+ );
+ try {
+ result.data = camelcaseObjectDeep(result.data)
+ res.status(200).json(result.data)
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+}
+
+const escapeSolrQuery = (query) => {
+ if (query == '*') return query;
+
+ const specialChars = /([\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
+ const words = query.split(/\s+/);
+ const escapedWords = words.map((word) => {
+ if (specialChars.test(word)) {
+ return `"${word.replace(specialChars, '\\$1')}"`;
+ }
+ return word;
+ });
+
+ return escapedWords.join(' ');
+};
diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js
index b6b8c795..6f98efcb 100644
--- a/src/pages/api/shop/search.js
+++ b/src/pages/api/shop/search.js
@@ -39,10 +39,13 @@ export default async function handler(req, res) {
paramOrderBy += 'flashsale_price_f ASC';
break;
default:
- paramOrderBy += 'product_rating_f DESC, price_discount_f DESC';
+ paramOrderBy += '';
break;
}
-
+
+ let checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/);
+ let newQ = checkQ.length > 1 ? escapeSolrQuery(q) + '*' : escapeSolrQuery(q);
+
let offset = (page - 1) * limit;
let parameter = [
'facet.field=manufacture_name_s',
@@ -51,12 +54,12 @@ export default async function handler(req, res) {
'indent=true',
`facet.query=${escapeSolrQuery(q)}`,
`q.op=${operation}`,
- `q=${escapeSolrQuery(q)}`,
+ `q=${newQ}`,
'qf=name_s',
`start=${parseInt(offset)}`,
`rows=${limit}`,
`sort=${paramOrderBy}`,
- `fq=-publish_b:false`,
+ `fq=-publish_b:false, product_rating_f:[8 TO *], price_tier1_v2_f:[1 TO *]`,
];
if (priceFrom > 0 || priceTo > 0) {
@@ -77,7 +80,10 @@ export default async function handler(req, res) {
parameter.push(
`fq=${brand
.split(',')
- .map((manufacturer) => `manufacture_name:"${encodeURIComponent(manufacturer)}"`)
+ .map(
+ (manufacturer) =>
+ `manufacture_name:"${encodeURIComponent(manufacturer)}"`
+ )
.join(' OR ')}`
);
if (category)
@@ -120,12 +126,14 @@ export default async function handler(req, res) {
const escapeSolrQuery = (query) => {
if (query == '*') return query;
+
+ query = query.replace(/-/g, ' ');
- const specialChars = /([\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
+ const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
const words = query.split(/\s+/);
const escapedWords = words.map((word) => {
if (specialChars.test(word)) {
- return `"${word.replace(specialChars, '\\$1')}"`;
+ return word.replace(specialChars, '\\$1');
}
return word;
});
@@ -133,6 +141,7 @@ const escapeSolrQuery = (query) => {
return escapedWords.join(' ');
};
+
/*const productResponseMap = (products, pricelist) => {
return products.map((product) => {
let price = product.price_tier1_v2_f || 0
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index 9d9d5dc0..4d6e59e0 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -8,12 +8,14 @@ import DesktopView from '@/core/components/views/DesktopView';
import MobileView from '@/core/components/views/MobileView';
import { FlashSaleSkeleton } from '@/lib/flashSale/skeleton/FlashSaleSkeleton';
import PreferredBrandSkeleton from '@/lib/home/components/Skeleton/PreferredBrandSkeleton';
+import BannerPromoSkeleton from '@/lib/home/components/Skeleton/BannerPromoSkeleton';
import PromotinProgram from '@/lib/promotinProgram/components/HomePage';
import PagePopupIformation from '~/modules/popup-information';
import CategoryPilihan from '../lib/home/components/CategoryPilihan';
import odooApi from '@/core/api/odooApi';
import { getAuth } from '~/libs/auth';
// import { getAuth } from '~/libs/auth';
+import useProductDetail from '~/modules/product-detail/stores/useProductDetail';
const BasicLayout = dynamic(() =>
import('@/core/components/layouts/BasicLayout')
@@ -45,9 +47,12 @@ const FlashSale = dynamic(
}
);
-// const ProgramPromotion = dynamic(() =>
-// import('@/lib/home/components/PromotionProgram')
-// );
+const ProgramPromotion = dynamic(() =>
+ import('@/lib/home/components/PromotionProgram'),
+{
+ loading: () => <BannerPromoSkeleton />,
+}
+);
const BannerSection = dynamic(() =>
import('@/lib/home/components/BannerSection')
@@ -93,8 +98,8 @@ export default function Home({categoryId}) {
}, [])
const [dataCategories, setDataCategories] = useState([])
-
return (
+ <>
<BasicLayout>
<Seo
title='Indoteknik.com: B2B Industrial Supply & Solution'
@@ -102,11 +107,9 @@ export default function Home({categoryId}) {
additionalMetaTags={[
{
name: 'keywords',
- content:
- 'indoteknik, indoteknik.com, toko teknik, toko perkakas, jual genset, jual fogging, jual krisbow, harga krisbow, harga alat safety, harga pompa air',
+ content: 'indoteknik, indoteknik.com, toko teknik, toko perkakas, jual genset, jual fogging, jual krisbow, harga krisbow, harga alat safety, harga pompa air',
},
- ]}
- />
+ ]} />
<PagePopupIformation />
@@ -131,28 +134,32 @@ export default function Home({categoryId}) {
</div>
<div className='my-16 flex flex-col gap-y-8'>
- <div className='my-16 flex flex-col gap-y-8'>
<ServiceList />
<div id='flashsale'>
<PreferredBrand />
</div>
{!auth?.feature?.soApproval && (
<>
- {/* <ProgramPromotion /> <FlashSale /> */}
+ <DelayRender renderAfter={200}>
+ <ProgramPromotion />
+ </DelayRender>
+ <DelayRender renderAfter={200}>
+ <FlashSale />
+ </DelayRender>
</>
)}
<PromotinProgram />
- <CategoryPilihan categories={dataCategories}/>
- <CategoryDynamic/>
+ {dataCategories &&(
+ <CategoryPilihan categories={dataCategories} />
+ )}
+ <CategoryDynamic />
<CategoryHomeId />
<BannerSection />
<CustomerReviews />
</div>
- </div>
</div>
- </DesktopView>
-
- <MobileView>
+ </DesktopView>
+ <MobileView>
<DelayRender renderAfter={200}>
<HeroBanner />
</DelayRender>
@@ -168,7 +175,7 @@ export default function Home({categoryId}) {
{!auth?.feature?.soApproval && (
<>
<DelayRender renderAfter={400}>
- {/* <ProgramPromotion /> */}
+ <ProgramPromotion />
</DelayRender>
<DelayRender renderAfter={600}>
<FlashSale />
@@ -179,8 +186,10 @@ export default function Home({categoryId}) {
<PromotinProgram />
</DelayRender>
<DelayRender renderAfter={600}>
- <CategoryPilihan categories={dataCategories}/>
- <CategoryDynamicMobile/>
+ {dataCategories &&(
+ <CategoryPilihan categories={dataCategories} />
+ )}
+ <CategoryDynamicMobile />
</DelayRender>
<DelayRender renderAfter={800}>
<PopularProduct />
@@ -195,5 +204,6 @@ export default function Home({categoryId}) {
</div>
</MobileView>
</BasicLayout>
+ </>
);
} \ No newline at end of file
diff --git a/src/pages/my/recomendation/api/recomendation.js b/src/pages/my/recomendation/api/recomendation.js
new file mode 100644
index 00000000..8ff760d0
--- /dev/null
+++ b/src/pages/my/recomendation/api/recomendation.js
@@ -0,0 +1,17 @@
+import axios from 'axios';
+import { useQuery } from 'react-query';
+
+const GenerateRecomendations = ({ query }) => {
+ const queryString = _.toQuery(query);
+ const GenerateRecomendationProducts = async () =>
+ await axios(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/recomendation?${queryString}`
+ );
+ const productSearch = useQuery(
+ `generateRecomendation-${ququeryStringery}`,
+ GenerateRecomendationProducts
+ );
+
+ return productSearch;
+};
+export default GenerateRecomendations;
diff --git a/src/pages/my/recomendation/components/products-recomendatison.jsx b/src/pages/my/recomendation/components/products-recomendatison.jsx
new file mode 100644
index 00000000..7da2fab1
--- /dev/null
+++ b/src/pages/my/recomendation/components/products-recomendatison.jsx
@@ -0,0 +1,477 @@
+import Menu from '@/lib/auth/components/Menu';
+import { useEffect, useState } from 'react';
+import * as XLSX from 'xlsx';
+import GenerateRecomendations from '../api/recomendation';
+import axios from 'axios';
+import { Button, Link } from '@chakra-ui/react';
+import Image from 'next/image';
+import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
+import formatCurrency from '~/libs/formatCurrency';
+
+const exportToExcel = (data) => {
+ const worksheet = XLSX.utils.json_to_sheet(data);
+ const workbook = XLSX.utils.book_new();
+ XLSX.utils.book_append_sheet(workbook, worksheet, 'Results');
+
+ // Generate Excel file and trigger download in the browser
+ XLSX.writeFile(workbook, 'ProductRecommendations.xlsx');
+};
+
+const ProductsRecomendation = ({ id }) => {
+ const [excelData, setExcelData] = useState(null);
+ const [products, setProducts] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isOpen, setIsOpen] = useState(false);
+ const [variantsOpen, setVariantsOpen] = useState([]);
+ const [otherRec, setOtherRec] = useState(false);
+
+ const mappingProducts = async ({ index, product, result, variants }) => {
+ const resultMapping = {
+ index: index,
+ product: product,
+ result: {
+ id: result?.id || '-',
+ name: result?.nameS || '-',
+ code: result?.defaultCodeS || '-',
+ },
+ };
+
+ return resultMapping;
+ };
+
+ const searchRecomendation = async ({ product, index, operator = 'AND' }) => {
+ let variants = [];
+ let resultMapping = {};
+ const searchProduct = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/generate-recomendation?q=${product}&op=${operator}`
+ );
+
+ if (operator === 'AND') {
+ const result =
+ searchProduct.data.response.numFound > 0
+ ? searchProduct.data.response.products[0]
+ : null;
+
+ if (result?.variantTotal > 1) {
+ const searchVariants = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-detail?id=${result.id}`
+ );
+ variants = searchVariants.data[0].variants;
+ }
+
+ resultMapping = await mappingProducts({
+ index,
+ product,
+ result,
+ variants,
+ });
+ } else {
+ const result =
+ searchProduct.data.response.numFound > 0
+ ? searchProduct.data.response.products
+ : null;
+
+ result.map((item) => {
+ if (item.variantTotal > 1) {
+ const searchVariants = axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/product-detail?id=${item.id}`
+ );
+ variants = searchVariants.data[0].variants;
+ }
+ });
+
+ // console.log('ini result', searchProduct.data.response);
+ }
+
+ return resultMapping;
+ };
+
+ const handleSubmit = async (e) => {
+ setIsLoading(true);
+ e.preventDefault();
+ if (excelData) {
+ const results = await Promise.all(
+ excelData.map(async (row, i) => {
+ const index = i + 1;
+ const product = row['product'];
+ return await generateProductRecomendation({ product, index });
+ })
+ );
+
+ const formattedResults = results.map((result) => {
+ const formattedResult = { product: result.product };
+ for (let i = 0; i <= 5; i++) {
+ formattedResult[`recomendation product ${i + 1} - code`] = result.result[i] == null ? '-' : result.result[i]?.code;
+ formattedResult[`recomendation product ${i + 1} - name`] = result.result[i] == null ? '-' : result.result[i]?.name ;
+ }
+ return formattedResult;
+ });
+
+ exportToExcel(formattedResults);
+ setProducts(results);
+ setIsLoading(false);
+ } else {
+ setIsLoading(false);
+ // console.log('No excel data available');
+ }
+ };
+
+ const handleFileChange = (e) => {
+ setIsLoading(true);
+ const file = e.target.files[0];
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ const data = new Uint8Array(event.target.result);
+ const workbook = XLSX.read(data, { type: 'array' });
+
+ const firstSheetName = workbook.SheetNames[0];
+ const worksheet = workbook.Sheets[firstSheetName];
+ const jsonData = XLSX.utils.sheet_to_json(worksheet);
+
+ setExcelData(jsonData);
+ // console.log('ini json data', jsonData);
+
+ setIsLoading(false);
+ };
+ reader.readAsArrayBuffer(file);
+ };
+
+ const handleVariantsOpen = ({ variants }) => {
+ setVariantsOpen(variants);
+ setIsOpen(true);
+ };
+ const hadnliChooseVariants = ({ id, variant }) => {
+ let foundIndex = products.findIndex((item) => item.result.id === id);
+ if (foundIndex !== -1) {
+ products[foundIndex].result.code = variant?.code;
+ products[foundIndex].result.name = variant?.name;
+ } else {
+ // console.log('Data not found.');
+ }
+ setIsOpen(false);
+ };
+
+ const handlingOtherRec = ({ product }) => {
+ // console.log('ini product', product);
+ const result = async () =>
+ await searchRecomendation({ product, index: 0, operator: 'OR' });
+
+ result();
+ };
+
+ const generateProductRecomendation = async ({ product, index }) => {
+ let variants = [];
+ let resultMapping = {};
+ const searchProduct = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/generate-recomendation?q=${product}&op=AND`
+ );
+ const searchProductOR = await axios.post(
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/generate-recomendation?q=${product}&op=OR`
+ );
+ const resultAND =
+ searchProduct.data.response.numFound > 0
+ ? searchProduct.data.response.docs[0]
+ : null; // hasil satu
+ const resultOR =
+ searchProductOR.data.response.numFound > 0
+ ? searchProductOR.data.response.docs
+ : []; // hasil 5
+
+ resultMapping = {
+ index: index,
+ product: product,
+ result: {},
+ };
+
+ // Add resultAND to resultMapping if it exists
+ resultMapping.result[0] = resultAND
+ ? {
+ id: resultAND?.id || '-',
+ name: resultAND?.nameS || '-',
+ code: resultAND?.defaultCodeS || '-',
+ }
+ : null;
+
+ // Add resultOR to resultMapping
+ if (resultOR.length > 0) {
+ resultOR.forEach((item, idx) => {
+ resultMapping.result[idx + 1] = {
+ id: item?.id || '-',
+ name: item?.nameS || '-',
+ code: item?.defaultCodeS || '-',
+ };
+ });
+ } else {
+ for (let i = 0; i <= 5; i++) {
+ resultMapping.result[i + 1] = null;
+ }
+ }
+ return resultMapping;
+ };
+ return (
+ <>
+ <BottomPopup
+ active={isOpen}
+ close={() => setIsOpen(false)}
+ className='w-full md:!w-[60%]'
+ title='List Variants'
+ >
+ <div className='container'>
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Part Number</th>
+ <th>Variants </th>
+ <th>Harga </th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {variantsOpen?.map((variant, index) => (
+ <tr key={index}>
+ <td>{variant.code}</td>
+ <td>{variant.attributes.join(', ') || '-'}</td>
+ <td>
+ {variant.price.discountPercentage > 0 && (
+ <div className='flex items-center gap-x-1'>
+ <div className={style['disc-badge']}>
+ {Math.floor(variant.price.discountPercentage)}%
+ </div>
+ <div className={style['disc-price']}>
+ Rp {formatCurrency(variant.price.price)}
+ </div>
+ </div>
+ )}
+ {variant.price.priceDiscount > 0 &&
+ `Rp ${formatCurrency(variant.price.priceDiscount)}`}
+ {variant.price.priceDiscount === 0 && '-'}
+ </td>
+ <td>
+ <Button
+ size='sm'
+ w='100%'
+ onClick={() =>
+ hadnliChooseVariants({
+ id: variant.parent.id,
+ variant: variant,
+ })
+ }
+ >
+ Pilih
+ </Button>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </BottomPopup>
+ <BottomPopup
+ active={otherRec}
+ close={() => setOtherRec(false)}
+ className='w-full md:!w-[60%]'
+ title='Other Recomendations'
+ >
+ <div className='container'>
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Product</th>
+ <th>Item Code </th>
+ <th>Description</th>
+ <th>Brand</th>
+ <th>Price</th>
+ <th>Image</th>
+ </tr>
+ </thead>
+ <tbody>
+ {variantsOpen?.map((variant, index) => (
+ <tr key={index}>
+ <td>{variant.code}</td>
+ <td>{variant.attributes.join(', ') || '-'}</td>
+ <td>
+ {variant.price.discountPercentage > 0 && (
+ <div className='flex items-center gap-x-1'>
+ <div className={style['disc-badge']}>
+ {Math.floor(variant.price.discountPercentage)}%
+ </div>
+ <div className={style['disc-price']}>
+ Rp {formatCurrency(variant.price.price)}
+ </div>
+ </div>
+ )}
+ {variant.price.priceDiscount > 0 &&
+ `Rp ${formatCurrency(variant.price.priceDiscount)}`}
+ {variant.price.priceDiscount === 0 && '-'}
+ </td>
+ <td>
+ <Button
+ size='sm'
+ w='100%'
+ onClick={() =>
+ hadnliChooseVariants({
+ id: variant.parent.id,
+ variant: variant,
+ })
+ }
+ >
+ Pilih
+ </Button>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </BottomPopup>
+ <div className='container mx-auto flex py-10'>
+ <div className='w-3/12 pr-4'>
+ <Menu />
+ </div>
+ <div className='w-9/12 p-4 bg-white border border-gray_r-6 rounded'>
+ <div className='flex mb-6 items-center justify-between'>
+ <h1 className='text-title-sm font-semibold'>
+ Generate Recomendation
+ </h1>
+ </div>
+ <div className='group'>
+ <h1 className='text-sm font-semibold'>Contoh Excel</h1>
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Product</th>
+ <th>Qty</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Tekiro Long Nose Pliers Tang Lancip</td>
+ <td>10</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div className='container mx-auto mt-8'>
+ <div className='mb-4'>
+ <label htmlFor='excelFile' className='text-sm font-semibold'>
+ Upload Excel File (.xlsx)
+ </label>
+ <input
+ type='file'
+ id='excelFile'
+ accept='.xlsx'
+ onChange={handleFileChange}
+ className='mt-1 p-2 block w-full border border-gray-300 rounded-md focus:outline-none focus:border-blue-500'
+ />
+ </div>
+ <Button
+ colorScheme='red'
+ w='l'
+ isDisabled={isLoading}
+ onClick={handleSubmit}
+ >
+ Generate
+ </Button>
+ </div>
+ {/* <div className='grup mt-8'>
+ {products && products.length > 0 && (
+ <div className='group'>
+ <table className='table-data'>
+ <thead>
+ <tr>
+ <th>Product</th>
+ <th>Item Code </th>
+ <th>Description</th>
+ <th>Brand</th>
+ <th>Price</th>
+ <th>Image</th>
+ <th>lainnya</th>
+ </tr>
+ </thead>
+ <tbody>
+ {products.map((product, index) => (
+ <tr key={index}>
+ <td>{product?.product}</td>
+ <td>
+ {product?.result?.code === '-' &&
+ product.result.variantTotal > 1 && (
+ <Button
+ border='2px'
+ borderColor='yellow.300'
+ size='sm'
+ onClick={() =>
+ handleVariantsOpen({
+ variants: product?.result?.variants,
+ })
+ }
+ >
+ Lihat Variants
+ </Button>
+ )}
+ {product?.result.code !== '-' &&
+ product?.result.variantTotal > 1 ? (
+ <>
+ {product?.result.code}
+ <Button
+ variant='link'
+ colorScheme='yellow'
+ size='sm'
+ onClick={() =>
+ handleVariantsOpen({
+ variants: product?.result?.variants,
+ })
+ }
+ >
+ Variants lainya
+ </Button>
+ </>
+ ) : (
+ <>{product?.result.code}</>
+ )}
+ </td>
+ <td>{product?.result.name}</td>
+ <td>{product?.result.manufacture}</td>
+ <td>
+ {product?.result.price !== '-'
+ ? `Rp ${formatCurrency(product?.result.price)}`
+ : '-'}
+ </td>
+ <td>
+ {product?.result.image !== '-' ? (
+ <Image
+ src={product?.result.image}
+ width={100}
+ height={100}
+ alt={product?.result.name}
+ />
+ ) : (
+ '-'
+ )}
+ </td>
+ <td>
+ {' '}
+ <Button
+ border='2px'
+ borderColor='red.500'
+ size='sm'
+ onClick={() =>
+ handlingOtherRec({ product: product.product })
+ }
+ >
+ Other
+ </Button>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ )}
+ </div> */}
+ </div>
+ </div>
+ </>
+ );
+};
+
+export default ProductsRecomendation;
diff --git a/src/pages/my/recomendation/index.jsx b/src/pages/my/recomendation/index.jsx
new file mode 100644
index 00000000..684b30c2
--- /dev/null
+++ b/src/pages/my/recomendation/index.jsx
@@ -0,0 +1,26 @@
+import DesktopView from '@/core/components/views/DesktopView';
+import MobileView from '@/core/components/views/MobileView';
+import IsAuth from '../../../lib/auth/components/IsAuth';
+import AppLayout from '@/core/components/layouts/AppLayout';
+import BasicLayout from '@/core/components/layouts/BasicLayout';
+import dynamic from 'next/dynamic';
+import Seo from '@/core/components/Seo'
+
+const ProductsRecomendation = dynamic(() => import('./components/products-recomendatison'))
+export default function MyRecomendation() {
+ return (
+ <IsAuth>
+
+ <Seo title='Dashboard Rekomendasi - Indoteknik.com' />
+
+ <MobileView>
+ <AppLayout></AppLayout>
+ </MobileView>
+ <DesktopView>
+ <BasicLayout>
+ <ProductsRecomendation />
+ </BasicLayout>
+ </DesktopView>
+ </IsAuth>
+ );
+}
diff --git a/src/pages/shop/brands/[slug].jsx b/src/pages/shop/brands/[slug].jsx
index c3a7299f..e786ef78 100644
--- a/src/pages/shop/brands/[slug].jsx
+++ b/src/pages/shop/brands/[slug].jsx
@@ -5,6 +5,7 @@ import _ from 'lodash'
import Seo from '@/core/components/Seo'
import Breadcrumb from '@/lib/brand/components/Breadcrumb'
import useBrand from '@/lib/brand/hooks/useBrand'
+import PageNotFound from '@/pages/404';
const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout'))
const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch'))
@@ -17,10 +18,13 @@ export default function BrandDetail() {
const brandName = getNameFromSlug(slug)
const id = getIdFromSlug(slug)
const {brand} = useBrand({id})
+ if (!brand || !brand.data || _.isEmpty(brand.data)) {
+ return <PageNotFound />;
+ }
return (
<BasicLayout>
<Seo
- title={`Distributor ${brandName} Indonesia Harga Official Indoteknik.com`}
+ title={`Jual Produk Resmi ${brandName} Indonesia | Indoteknik.com`}
description='B2B Marketplace MRO &amp; Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi &amp; Harga Kompetitif'
additionalMetaTags={[
{
diff --git a/src/pages/shop/promo/[slug].tsx b/src/pages/shop/promo/[slug].tsx
new file mode 100644
index 00000000..aaee1249
--- /dev/null
+++ b/src/pages/shop/promo/[slug].tsx
@@ -0,0 +1,523 @@
+import dynamic from 'next/dynamic'
+import NextImage from 'next/image';
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/router'
+import Seo from '../../../core/components/Seo'
+import Promocrumb from '../../../lib/promo/components/Promocrumb'
+import { fetchPromoItemsSolr, fetchVariantSolr } from '../../../api/promoApi'
+import LogoSpinner from '../../../core/components/elements/Spinner/LogoSpinner.jsx'
+import ProductPromoCard from '../../../../src-migrate/modules/product-promo/components/Card'
+import { IPromotion } from '../../../../src-migrate/types/promotion'
+import React from 'react'
+import { SolrResponse } from "../../../../src-migrate/types/solr.ts";
+import DesktopView from '../../../core/components/views/DesktopView';
+import MobileView from '../../../core/components/views/MobileView';
+import 'swiper/swiper-bundle.css';
+import useDevice from '../../../core/hooks/useDevice'
+import ProductFilterDesktop from '../../../lib/product/components/ProductFilterDesktopPromotion';
+import ProductFilter from '../../../lib/product/components/ProductFilter';
+import { HStack, Image, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react';
+import { formatCurrency } from '../../../core/utils/formatValue';
+import Pagination from '../../../core/components/elements/Pagination/Pagination';
+import SideBanner from '../../../../src-migrate/modules/side-banner';
+import whatsappUrl from '../../../core/utils/whatsappUrl';
+import { cons, toQuery } from 'lodash-contrib';
+import _ from 'lodash';
+import useActive from '../../../core/hooks/useActive';
+
+const BasicLayout = dynamic(() => import('../../../core/components/layouts/BasicLayout'))
+
+export default function PromoDetail() {
+ const router = useRouter()
+ const { slug = '', brand ='', category='', priceFrom = '', priceTo = '', page = '1' } = router.query
+ const [promoItems, setPromoItems] = useState<any[]>([])
+ const [promoData, setPromoData] = useState<IPromotion[] | null>(null)
+ const [currentPage, setCurrentPage] = useState(parseInt(page as string, 10) || 1);
+ const itemsPerPage = 12; // Jumlah item yang ingin ditampilkan per halaman
+ const [loading, setLoading] = useState(true);
+ const { isMobile, isDesktop } = useDevice()
+ const [brands, setBrands] = useState<Brand[]>([]);
+ const [categories, setCategories] = useState<Category[]>([]);
+ const [brandValues, setBrandValues] = useState<string[]>([]);
+ const [categoryValues, setCategoryValues] = useState<string[]>([]);
+ const [orderBy, setOrderBy] = useState(router.query?.orderBy || 'popular');
+ const popup = useActive();
+ const prefixUrl = `/shop/promo/${slug}`
+
+ useEffect(() => {
+ if (router.query.brand) {
+ let brandsArray: string[] = [];
+ if (Array.isArray(router.query.brand)) {
+ brandsArray = router.query.brand;
+ } else if (typeof router.query.brand === 'string') {
+ brandsArray = router.query.brand.split(',').map((brand) => brand.trim());
+ }
+ setBrandValues(brandsArray);
+ } else {
+ setBrandValues([]);
+ }
+
+ if (router.query.category) {
+ let categoriesArray: string[] = [];
+
+ if (Array.isArray(router.query.category)) {
+ categoriesArray = router.query.category;
+ } else if (typeof router.query.category === 'string') {
+ categoriesArray = router.query.category.split(',').map((category) => category.trim());
+ }
+ setCategoryValues(categoriesArray);
+ } else {
+ setCategoryValues([]);
+ }
+ }, [router.query.brand, router.query.category]);
+
+ interface Brand {
+ brand: string;
+ qty: number;
+ }
+
+ interface Category {
+ name: string;
+ qty: number;
+ }
+
+ useEffect(() => {
+ const loadPromo = async () => {
+ setLoading(true);
+ const brandsData: Brand[] = [];
+ const categoriesData: Category[] = [];
+
+ const pageNumber = Array.isArray(page) ? parseInt(page[0], 10) : parseInt(page, 10);
+ setCurrentPage(pageNumber)
+
+ try {
+ const items = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug}`,0,100);
+ setPromoItems(items);
+
+ if (items.length === 0) {
+ setPromoData([])
+ setLoading(false);
+ return;
+ }
+
+ const brandArray = Array.isArray(brand) ? brand : brand.split(',');
+ const categoryArray = Array.isArray(category) ? category : category.split(',');
+
+ const promoDataPromises = items.map(async (item) => {
+
+ try {
+ let brandQuery = '';
+ if (brand) {
+ brandQuery = brandArray.map(b => `manufacture_name_s:${b}`).join(' OR ');
+ brandQuery = `(${brandQuery})`;
+ }
+
+ let categoryQuery = '';
+ if (category) {
+ categoryQuery = categoryArray.map(c => `category_name:${c}`).join(' OR ');
+ categoryQuery = `(${categoryQuery})`;
+ }
+
+ let priceQuery = '';
+ if (priceFrom && priceTo) {
+ priceQuery = `price_f:[${priceFrom} TO ${priceTo}]`;
+ } else if (priceFrom) {
+ priceQuery = `price_f:[${priceFrom} TO *]`;
+ } else if (priceTo) {
+ priceQuery = `price_f:[* TO ${priceTo}]`;
+ }
+
+ let combinedQuery = '';
+ let combinedQueryPrice = `${priceQuery}`;
+ if (brand && category && priceFrom || priceTo) {
+ combinedQuery = `${brandQuery} AND ${categoryQuery} `;
+ } else if (brand && category) {
+ combinedQuery = `${brandQuery} AND ${categoryQuery}`;
+ } else if (brand && priceFrom || priceTo) {
+ combinedQuery = `${brandQuery}`;
+ } else if (category && priceFrom || priceTo) {
+ combinedQuery = `${categoryQuery}`;
+ } else if (brand) {
+ combinedQuery = brandQuery;
+ } else if (category) {
+ combinedQuery = categoryQuery;
+ }
+
+ if (combinedQuery && priceFrom || priceTo) {
+ const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`);
+ const product = response.response.docs[0];
+ const product_id = product.id;
+ const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} AND ${combinedQueryPrice}`,0,100);
+ return response2;
+ }else if(combinedQuery){
+ const response = await fetchVariantSolr(`id:${item.product_id} AND ${combinedQuery}`);
+ const product = response.response.docs[0];
+ const product_id = product.id;
+ const response2 = await fetchPromoItemsSolr(`type_value_s:${Array.isArray(slug) ? slug[0] : slug} AND product_ids:${product_id} `,0,100);
+ return response2;
+ } else {
+ const response = await fetchPromoItemsSolr(`id:${item.id}`,0,100);
+ return response;
+ }
+ } catch (fetchError) {
+ return [];
+ }
+ });
+
+ const promoDataArray = await Promise.all(promoDataPromises);
+ const mergedPromoData = promoDataArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
+ setPromoData(mergedPromoData);
+
+ const dataBrandCategoryPromises = promoDataArray.map(async (promoData) => {
+ if (promoData) {
+ const dataBrandCategory = promoData.map(async (item) => {
+ let response;
+ if(category){
+ const categoryQuery = categoryArray.map(c => `category_name:${c}`).join(' OR ');
+ response = await fetchVariantSolr(`id:${item.products[0].product_id} AND (${categoryQuery})`);
+ }else{
+ response = await fetchVariantSolr(`id:${item.products[0].product_id}`)
+ }
+
+
+ if (response.response?.docs?.length > 0) {
+ const product = response.response.docs[0];
+ const manufactureNameS = product.manufacture_name;
+ if (Array.isArray(manufactureNameS)) {
+ for (let i = 0; i < manufactureNameS.length; i += 2) {
+ const brand = manufactureNameS[i];
+ const qty = 1;
+ const existingBrandIndex = brandsData.findIndex(b => b.brand === brand);
+ if (existingBrandIndex !== -1) {
+ brandsData[existingBrandIndex].qty += qty;
+ } else {
+ brandsData.push({ brand, qty });
+ }
+ }
+ }
+
+ const categoryNameS = product.category_name;
+ if (Array.isArray(categoryNameS)) {
+ for (let i = 0; i < categoryNameS.length; i += 2) {
+ const name = categoryNameS[i];
+ const qty = 1;
+ const existingCategoryIndex = categoriesData.findIndex(c => c.name === name);
+ if (existingCategoryIndex !== -1) {
+ categoriesData[existingCategoryIndex].qty += qty;
+ } else {
+ categoriesData.push({ name, qty });
+ }
+ }
+ }
+ }
+ });
+
+ return Promise.all(dataBrandCategory);
+ }
+ });
+
+ await Promise.all(dataBrandCategoryPromises);
+ setBrands(brandsData);
+ setCategories(categoriesData);
+ setLoading(false);
+
+ } catch (loadError) {
+ // console.error("Error loading promo items:", loadError)
+ setLoading(false);
+ }
+ }
+
+ if (slug) {
+ loadPromo()
+ }
+ },[slug, brand, category, priceFrom, priceTo, currentPage]);
+
+
+ function capitalizeFirstLetter(string) {
+ string = string.replace(/_/g, ' ');
+ return string.replace(/(^\w|\s\w)/g, function(match) {
+ return match.toUpperCase();
+ });
+ }
+
+ const handleDeleteFilter = async (source, value) => {
+ let params = {
+ q: router.query.q,
+ orderBy: '',
+ brand: brandValues.join(','),
+ category: categoryValues.join(','),
+ priceFrom: priceFrom || '',
+ priceTo: priceTo || '',
+ };
+
+ let brands = brandValues;
+ let catagories = categoryValues;
+ switch (source) {
+ case 'brands':
+ brands = brandValues.filter((item) => item !== value);
+ params.brand = brands.join(',');
+ await setBrandValues(brands);
+ break;
+ case 'category':
+ catagories = categoryValues.filter((item) => item !== value);
+ params.category = catagories.join(',');
+ await setCategoryValues(catagories);
+ break;
+ case 'price':
+ params.priceFrom = '';
+ params.priceTo = '';
+ break;
+ case 'delete':
+ params = {
+ q: router.query.q,
+ orderBy: '',
+ brand: '',
+ category: '',
+ priceFrom: '',
+ priceTo: '',
+ };
+ break;
+ }
+
+ handleSubmitFilter(params);
+ };
+ const handleSubmitFilter = (params) => {
+ params = _.pickBy(params, _.identity);
+ params = toQuery(params);
+ router.push(`${slug}?${params}`);
+ };
+
+ const visiblePromotions = promoData?.slice( (currentPage-1) * itemsPerPage, currentPage * 12)
+
+ const toQuery = (obj) => {
+ const str = Object.keys(obj)
+ .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
+ .join('&')
+ return str
+ }
+
+ const whatPromo = capitalizeFirstLetter(slug)
+ const queryWithoutSlug = _.omit(router.query, ['slug'])
+ const queryString = toQuery(queryWithoutSlug)
+
+ return (
+ <BasicLayout>
+ <Seo
+ title={`Promo ${Array.isArray(slug) ? slug[0] : slug} Terkini`}
+ description='B2B Marketplace MRO & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & Harga Kompetitif'
+ />
+ <Promocrumb brandName={whatPromo} />
+ <MobileView>
+ <div className='p-4 pt-0'>
+ <h1 className='mb-2 font-semibold text-h-sm'>Promo {whatPromo}</h1>
+
+ <FilterChoicesComponent
+ brandValues={brandValues}
+ categoryValues={categoryValues}
+ priceFrom={priceFrom}
+ priceTo={priceTo}
+ handleDeleteFilter={handleDeleteFilter}
+ />
+ {promoItems.length >= 1 && (
+ <div className='flex items-center gap-x-2 mb-5 justify-between'>
+ <div>
+ <button
+ className='btn-light py-2 px-5 h-[40px]'
+ onClick={popup.activate}
+ >
+ Filter
+ </button>
+ </div>
+ </div>
+ )}
+
+ {loading ? (
+ <div className='container flex justify-center my-4'>
+ <LogoSpinner width={48} height={48} />
+ </div>
+ ) : promoData && promoItems.length >= 1 ? (
+ <>
+ <div className='grid grid-cols-1 gap-x-1 gap-y-1'>
+ {visiblePromotions?.map((promotion) => (
+ <div key={promotion.id} className="min-w-36 max-w-[400px] mb-[20px] sm:w-full md:w-1/2 lg:w-1/3 xl:w-1/4 ">
+ <ProductPromoCard promotion={promotion}/>
+ </div>
+ ))}
+ </div>
+ </>
+ ) : (
+ <div className="text-center my-8">
+ <p>Belum ada promo pada kategori ini</p>
+ </div>
+ )}
+
+ <Pagination
+ pageCount={Math.ceil((promoData?.length ?? 0) / itemsPerPage)}
+ currentPage={currentPage}
+ url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`}
+ className='mt-6 mb-2'
+ />
+ <ProductFilter
+ active={popup.active}
+ close={popup.deactivate}
+ brands={brands || []}
+ categories={categories || []}
+ prefixUrl={router.asPath.includes('?') ? `${router.asPath}` : `${router.asPath}?`}
+ defaultBrand={null}
+ />
+ </div>
+
+ </MobileView>
+ <DesktopView>
+ <div className='container mx-auto flex mb-3 flex-col'>
+ <div className='w-full pl-6'>
+ <h1 className='text-2xl mb-2 font-semibold'>Promo {whatPromo}</h1>
+ <div className=' w-full h-full flex flex-row items-center '>
+
+ <div className='detail-filter w-1/2 flex justify-start items-center mt-4'>
+
+ <FilterChoicesComponent
+ brandValues={brandValues}
+ categoryValues={categoryValues}
+ priceFrom={priceFrom}
+ priceTo={priceTo}
+ handleDeleteFilter={handleDeleteFilter}
+ />
+ </div>
+ <div className='Filter w-1/2 flex flex-col'>
+
+ <ProductFilterDesktop
+ brands={brands || []}
+ categories={categories || []}
+ prefixUrl={'/shop/promo'}
+ // defaultBrand={null}
+ />
+ </div>
+ </div>
+ {loading ? (
+ <div className='container flex justify-center my-4'>
+ <LogoSpinner width={48} height={48} />
+ </div>
+ ) : promoData && promoItems.length >= 1 ? (
+ <>
+ <div className='grid grid-cols-1 gap-x-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3'>
+ {visiblePromotions?.map((promotion) => (
+ <div key={promotion.id} className="min-w-[400px] max-w-[400px] mb-[20px] sm:min-w-[350px] md:min-w-[380px] lg:min-w-[400px] xl:min-w-[400px] ">
+ <ProductPromoCard promotion={promotion}/>
+ </div>
+ ))}
+ </div>
+ </>
+ ) : (
+ <div className="text-center my-8">
+ <p>Belum ada promo pada kategori ini</p>
+ </div>
+ )}
+ <div className='flex justify-between items-center mt-6 mb-2'>
+ <div className='pt-2 pb-6 flex items-center gap-x-3'>
+ <NextImage
+ src='/images/logo-question.png'
+ alt='Logo Question Indoteknik'
+ width={60}
+ height={60}
+ />
+ <div className='text-gray_r-12/90'>
+ <span>
+ Barang yang anda cari tidak ada?{' '}
+ <a
+ href={
+ router.query?.q
+ ? whatsappUrl('productSearch', {
+ name: router.query.q,
+ })
+ : whatsappUrl()
+ }
+ className='text-danger-500'
+ >
+ Hubungi Kami
+ </a>
+ </span>
+ </div>
+ </div>
+
+
+
+ <Pagination
+ pageCount={Math.ceil((promoData?.length ?? 0) / itemsPerPage)}
+ currentPage={currentPage}
+ url={`${prefixUrl}?${toQuery(_.omit(queryWithoutSlug, ['page']))}`}
+ className='mt-6 mb-2'
+ />
+ </div>
+
+ </div>
+ </div>
+ </DesktopView>
+ </BasicLayout>
+ )
+ }
+
+const FilterChoicesComponent = ({
+ brandValues,
+ categoryValues,
+ priceFrom,
+ priceTo,
+ handleDeleteFilter,
+ }) => (
+ <div className='flex items-center mb-4'>
+ <HStack spacing={2} className='flex-wrap'>
+ {brandValues?.map((value, index) => (
+ <Tag
+ size='lg'
+ key={index}
+ borderRadius='lg'
+ variant='outline'
+ colorScheme='gray'
+ >
+ <TagLabel>{value}</TagLabel>
+ <TagCloseButton onClick={() => handleDeleteFilter('brands', value)} />
+ </Tag>
+ ))}
+
+ {categoryValues?.map((value, index) => (
+ <Tag
+ size='lg'
+ key={index}
+ borderRadius='lg'
+ variant='outline'
+ colorScheme='gray'
+ >
+ <TagLabel>{value}</TagLabel>
+ <TagCloseButton
+ onClick={() => handleDeleteFilter('category', value)}
+ />
+ </Tag>
+ ))}
+ {priceFrom && priceTo && (
+ <Tag size='lg' borderRadius='lg' variant='outline' colorScheme='gray'>
+ <TagLabel>
+ {formatCurrency(priceFrom) + '-' + formatCurrency(priceTo)}
+ </TagLabel>
+ <TagCloseButton
+ onClick={() => handleDeleteFilter('price', priceFrom)}
+ />
+ </Tag>
+ )}
+ {brandValues?.length > 0 ||
+ categoryValues?.length > 0 ||
+ priceFrom ||
+ priceTo ? (
+ <span>
+ <button
+ className='btn-transparent py-2 px-5 h-[40px] text-red-700'
+ onClick={() => handleDeleteFilter('delete')}
+ >
+ Hapus Semua
+ </button>
+ </span>
+ ) : (
+ ''
+ )}
+ </HStack>
+ </div>
+);
diff --git a/src/pages/shop/promo/index.jsx b/src/pages/shop/promo/index.jsx
new file mode 100644
index 00000000..01a11aad
--- /dev/null
+++ b/src/pages/shop/promo/index.jsx
@@ -0,0 +1,40 @@
+import Seo from '@/core/components/Seo'
+import BasicLayout from '@/core/components/layouts/BasicLayout';
+import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react';
+import Link from 'next/link';
+import Promo from '~/pages/shop/promo';
+
+import React from 'react';
+
+const PromoPage = () => {
+ return (
+ <BasicLayout>
+ <Seo title='Promo Indoteknik.com' />
+ <div className='container mx-auto py-4 md:py-6 pb-0'>
+ <Breadcrumb>
+ <BreadcrumbItem>
+ <BreadcrumbLink
+ as={Link}
+ href='/'
+ className='!text-danger-500 whitespace-nowrap'
+ >
+ Home
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+
+ <BreadcrumbItem isCurrentPage>
+ <BreadcrumbLink className='whitespace-nowrap'>
+ Promo
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+ </Breadcrumb>
+
+ <div className='h-10' />
+
+ <Promo />
+ </div>
+ </BasicLayout>
+ );
+};
+
+export default PromoPage;