diff options
Diffstat (limited to 'src/pages')
| -rw-r--r-- | src/pages/api/shop/generate-recomendation.js | 64 | ||||
| -rw-r--r-- | src/pages/api/shop/search.js | 23 | ||||
| -rw-r--r-- | src/pages/index.jsx | 48 | ||||
| -rw-r--r-- | src/pages/my/recomendation/api/recomendation.js | 17 | ||||
| -rw-r--r-- | src/pages/my/recomendation/components/products-recomendatison.jsx | 477 | ||||
| -rw-r--r-- | src/pages/my/recomendation/index.jsx | 26 | ||||
| -rw-r--r-- | src/pages/shop/brands/[slug].jsx | 6 | ||||
| -rw-r--r-- | src/pages/shop/promo/[slug].tsx | 523 | ||||
| -rw-r--r-- | src/pages/shop/promo/index.jsx | 40 |
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 & Industri dengan Layanan Pembayaran Tempo, Faktur Pajak, Online Quotation, Garansi Resmi & 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; |
