summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/images/keranjang-compare.svg3
-rw-r--r--public/images/logo-bandingkan.svg3
-rw-r--r--public/images/no-image-compare.svg9
-rw-r--r--public/robots.txt6
-rw-r--r--src-migrate/modules/product-detail/components/AddToQuotation.tsx91
-rw-r--r--src-migrate/modules/product-detail/components/PriceAction.tsx38
-rw-r--r--src-migrate/modules/product-detail/components/ProductComparisonModal.tsx492
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx75
-rw-r--r--src-migrate/types/productVariant.ts3
-rw-r--r--src/lib/product/components/Product/ProductDesktopVariant.jsx13
-rw-r--r--src/lib/product/components/Product/ProductMobileVariant.jsx9
-rw-r--r--src/pages/api/shop/search.js116
-rw-r--r--src/utils/solrMapping.js3
13 files changed, 769 insertions, 92 deletions
diff --git a/public/images/keranjang-compare.svg b/public/images/keranjang-compare.svg
new file mode 100644
index 00000000..6504e420
--- /dev/null
+++ b/public/images/keranjang-compare.svg
@@ -0,0 +1,3 @@
+<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16.22 7.54536C18.2696 7.54536 20 5.8375 20 3.77286C20 1.70821 18.2918 0 16.22 0C14.1554 0 12.4471 1.70821 12.4471 3.77286C12.4471 5.85214 14.1554 7.54536 16.22 7.54536ZM6.03786 12.1796H14.705C14.9943 12.1796 15.2468 11.9421 15.2468 11.6225C15.2468 11.3036 14.9943 11.0657 14.705 11.0657H6.16429C5.74071 11.0657 5.48071 10.7686 5.41429 10.3157L5.3025 9.53571H14.7196C15.4921 9.53571 15.9896 9.20893 16.2718 8.57036C15.6332 8.57036 15.0314 8.44429 14.4746 8.19893C14.3632 8.3475 14.2221 8.41429 14.0139 8.41429L5.13179 8.42179L4.50071 4.11464H11.4518C11.415 3.77286 11.4371 3.32714 11.4889 2.99321H4.33714L4.20357 2.04964C4.12179 1.47821 3.92143 1.18821 3.16393 1.18821H0.549643C0.2525 1.18821 0 1.44821 0 1.74536C0 2.04964 0.2525 2.30964 0.549643 2.30964H3.05964L4.24821 10.4643C4.40393 11.5189 4.96107 12.1796 6.03786 12.1796ZM16.2271 6.22357C15.9675 6.22357 15.7371 6.045 15.7371 5.77071V4.22571H14.3186C14.1986 4.22515 14.0838 4.17726 13.999 4.09245C13.9142 4.00765 13.8663 3.89279 13.8657 3.77286C13.8657 3.52786 14.0661 3.31964 14.3186 3.31964H15.7371V1.7825C15.7371 1.50036 15.9675 1.32179 16.2275 1.32179C16.4871 1.32179 16.7096 1.50036 16.7096 1.7825V3.31964H18.1286C18.3811 3.31964 18.5893 3.5275 18.5893 3.77286C18.5893 4.01821 18.3811 4.22571 18.1286 4.22571H16.71V5.77071C16.71 6.04536 16.4868 6.22357 16.2271 6.22357ZM6.625 15.7893C6.78342 15.7907 6.94054 15.7605 7.08716 15.7005C7.23379 15.6405 7.367 15.5519 7.47901 15.4398C7.59102 15.3278 7.67959 15.1945 7.73954 15.0479C7.7995 14.9013 7.82964 14.7441 7.82821 14.5857C7.82883 14.4275 7.79811 14.2708 7.73784 14.1245C7.67756 13.9782 7.58892 13.8454 7.47702 13.7335C7.36513 13.6217 7.2322 13.5332 7.0859 13.473C6.9396 13.4128 6.78284 13.3821 6.62464 13.3829C6.46628 13.3815 6.30924 13.4117 6.16267 13.4717C6.01611 13.5317 5.88296 13.6203 5.771 13.7323C5.65903 13.8443 5.57048 13.9775 5.51053 14.1241C5.45057 14.2707 5.4204 14.4277 5.42179 14.5861C5.41992 14.7446 5.44976 14.9019 5.50957 15.0487C5.56938 15.1955 5.65793 15.3289 5.77004 15.441C5.88214 15.5531 6.01552 15.6417 6.16235 15.7015C6.30917 15.7613 6.46647 15.7912 6.625 15.7893ZM13.5764 15.7893C13.7348 15.7906 13.8919 15.7604 14.0385 15.7004C14.1851 15.6404 14.3182 15.5517 14.4302 15.4397C14.5422 15.3277 14.6307 15.1944 14.6906 15.0478C14.7506 14.9012 14.7807 14.7441 14.7793 14.5857C14.7799 14.4276 14.7492 14.2709 14.689 14.1247C14.6287 13.9785 14.5402 13.8456 14.4283 13.7338C14.3165 13.622 14.1837 13.5334 14.0375 13.4732C13.8913 13.4129 13.7346 13.3822 13.5764 13.3829C12.9004 13.3829 12.3586 13.9175 12.3586 14.5861C12.3586 15.2618 12.9004 15.7893 13.5764 15.7893Z" fill="#E20613"/>
+</svg>
diff --git a/public/images/logo-bandingkan.svg b/public/images/logo-bandingkan.svg
new file mode 100644
index 00000000..4e441182
--- /dev/null
+++ b/public/images/logo-bandingkan.svg
@@ -0,0 +1,3 @@
+<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5 1.42857H1.42857C0.642857 1.42857 0 2.07143 0 2.85714V12.8571C0 13.6429 0.642857 14.2857 1.42857 14.2857H5V15.7143H6.42857V0H5V1.42857ZM5 12.1429H1.42857L5 7.85714V12.1429ZM11.4286 1.42857H7.85714V2.85714H11.4286V12.1429L7.85714 7.85714V14.2857H11.4286C12.2143 14.2857 12.8571 13.6429 12.8571 12.8571V2.85714C12.8571 2.07143 12.2143 1.42857 11.4286 1.42857Z" fill="#E21F27"/>
+</svg>
diff --git a/public/images/no-image-compare.svg b/public/images/no-image-compare.svg
new file mode 100644
index 00000000..07b0b781
--- /dev/null
+++ b/public/images/no-image-compare.svg
@@ -0,0 +1,9 @@
+<svg width="154" height="154" viewBox="0 0 154 154" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<rect opacity="0.12" width="154" height="154" fill="url(#pattern0_7532_10753)"/>
+<defs>
+<pattern id="pattern0_7532_10753" patternContentUnits="objectBoundingBox" width="1" height="1">
+<use xlink:href="#image0_7532_10753" transform="scale(0.005)"/>
+</pattern>
+<image id="image0_7532_10753" width="200" height="200" preserveAspectRatio="none" xlink:href="data:image/png;base64,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"/>
+</defs>
+</svg>
diff --git a/public/robots.txt b/public/robots.txt
index fc22ba6d..da5d58ad 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -14,8 +14,6 @@ Disallow: /shop/cart
Disallow: /shop/checkout
Disallow: /my/*
Disallow: /shop/search/*
-Disallow: /_next/static
-Disallow: /api/*
User-agent: Adsbot-Google
Allow: /my/*
@@ -23,6 +21,6 @@ Allow: /shop/search/*
Sitemap: https://indoteknik.com/sitemap/products.xml
-Sitemap: https://indoteknik.com/sitemap/brands.xml
+Sitemap: https://indoteknik.com/sitemap/brands.xml
Sitemap: https://indoteknik.com/sitemap/categories.xml
-Sitemap: https://indoteknik.com/sitemap/videos.xml \ No newline at end of file
+Sitemap: https://indoteknik.com/sitemap/videos.xml
diff --git a/src-migrate/modules/product-detail/components/AddToQuotation.tsx b/src-migrate/modules/product-detail/components/AddToQuotation.tsx
index 3e811330..e26e271f 100644
--- a/src-migrate/modules/product-detail/components/AddToQuotation.tsx
+++ b/src-migrate/modules/product-detail/components/AddToQuotation.tsx
@@ -1,7 +1,7 @@
import BottomPopup from '@/core/components/elements/Popup/BottomPopup';
import style from '../styles/price-action.module.css';
import { Button, Link, useToast } from '@chakra-ui/react';
-import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
+// import { ScaleIcon } from '@heroicons/react/24/outline'; // Tidak perlu lagi
import product from 'next-seo/lib/jsonld/product';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
@@ -17,12 +17,15 @@ import { createSlug } from '~/libs/slug';
import formatCurrency from '~/libs/formatCurrency';
import { useProductDetail } from '../stores/useProductDetail';
import useDevice from '@/core/hooks/useDevice';
+import DesktopView from '@/core/components/views/DesktopView';
+import MobileView from '@/core/components/views/MobileView';
type Props = {
variantId: number | null;
quantity?: number;
source?: 'buy' | 'add_to_cart';
products: IProductDetail;
+ onCompare?: () => void;
};
type Status = 'idle' | 'loading' | 'success';
@@ -32,6 +35,7 @@ const AddToQuotation = ({
quantity = 1,
source = 'add_to_cart',
products,
+ onCompare
}: Props) => {
const auth = getAuth();
const router = useRouter();
@@ -106,37 +110,60 @@ const AddToQuotation = ({
}, 3000);
}, [status]);
- const btnConfig = {
- add_to_cart: {
- colorScheme: 'red',
- variant: 'outline',
- text: 'Keranjang',
- },
- buy: {
- colorScheme: 'red',
- variant: 'solid',
- text: 'Beli',
- },
- };
-
return (
- <div className='w-full'>
- <Button
- onClick={handleButton}
- color={'red'}
- colorScheme='white'
- className='w-full border-2 p-2 gap-1 hover:bg-slate-100 flex items-center'
- isDisabled={!hasPrice}
- >
- <ImageNext
- src={isDesktop ? '/images/doc_red.svg' : '/images/doc.svg'}
- alt='penawaran instan'
- className=''
- width={25}
- height={25}
- />
- {isDesktop ? 'Penawaran Harga Instan' : ''}
- </Button>
+ <div className='w-full flex flex-col gap-3'>
+
+ {/* 3. TAMPILAN DESKTOP: GRID 2 KOLOM (Bandingkan & Penawaran) */}
+ <DesktopView>
+ <div className="grid grid-cols-2 gap-3 w-full">
+ {/* Tombol Kiri: Bandingkan */}
+ <Button
+ onClick={onCompare}
+ variant="outline"
+ colorScheme="gray"
+ className="w-full border border-gray-300 p-2 gap-2 flex items-center justify-center text-gray-600 hover:text-red-600 hover:border-red-600 transition-all font-normal text-sm"
+ >
+ {/* UPDATE ICON DISINI */}
+ <ImageNext src="/images/logo-bandingkan.svg" width={15} height={15} alt="bandingkan" />
+ Bandingkan
+ </Button>
+
+ {/* Tombol Kanan: Penawaran (Link WA) */}
+ <Button
+ as={Link}
+ href={askAdminUrl}
+ target='_blank'
+ variant="outline"
+ colorScheme="gray"
+ className="w-full border border-gray-300 p-2 gap-2 flex items-center justify-center text-gray-600 hover:text-red-600 hover:border-red-600 transition-all font-normal text-sm"
+ _hover={{ textDecoration: 'none' }}
+ onClick={handleButton}
+ >
+ <ImageNext src="/images/doc_red.svg" width={20} height={20} alt="penawaran" />
+ Penawaran
+ </Button>
+ </div>
+ </DesktopView>
+
+ {/* TAMPILAN MOBILE */}
+ <MobileView>
+ <Button
+ onClick={handleButton}
+ color={'red'}
+ colorScheme='white'
+ className='w-full border-2 p-2 gap-1 hover:bg-slate-100 flex items-center'
+ isDisabled={!hasPrice}
+ >
+ <ImageNext
+ src='/images/doc.svg'
+ alt='penawaran instan'
+ className=''
+ width={25}
+ height={25}
+ />
+ </Button>
+ </MobileView>
+
<BottomPopup
className='!container'
title='Berhasil Ditambahkan'
@@ -243,4 +270,4 @@ const AddToQuotation = ({
);
};
-export default AddToQuotation;
+export default AddToQuotation; \ No newline at end of file
diff --git a/src-migrate/modules/product-detail/components/PriceAction.tsx b/src-migrate/modules/product-detail/components/PriceAction.tsx
index d73ab5f6..ee8009ef 100644
--- a/src-migrate/modules/product-detail/components/PriceAction.tsx
+++ b/src-migrate/modules/product-detail/components/PriceAction.tsx
@@ -1,5 +1,4 @@
import style from '../styles/price-action.module.css';
-
import Image from 'next/image';
import Link from 'next/link';
import { useEffect, useState } from 'react';
@@ -15,14 +14,17 @@ import { Button, Skeleton } from '@chakra-ui/react';
import DesktopView from '@/core/components/views/DesktopView';
import MobileView from '@/core/components/views/MobileView';
+// 1. Tambahkan onCompare (Optional) di sini
type Props = {
product: IProductDetail;
+ onCompare?: () => void;
};
const PPN: number = process.env.NEXT_PUBLIC_PPN
? parseFloat(process.env.NEXT_PUBLIC_PPN)
: 0;
-const PriceAction = ({ product }: Props) => {
+
+const PriceAction = ({ product, onCompare }: Props) => {
const {
activePrice,
setActive,
@@ -146,19 +148,6 @@ const PriceAction = ({ product }: Props) => {
</>
)}
- {/* {!!activePrice && activePrice.price === 0 && (
- <span>
- Hubungi kami untuk dapatkan harga terbaik,{' '}
- <Link
- href={askAdminUrl}
- target='_blank'
- className={style['contact-us']}
- >
- klik disini
- </Link>
- </span>
- )} */}
-
<DesktopView>
<div className='h-4' />
<div className='flex gap-x-5 items-center'>
@@ -227,9 +216,6 @@ const PriceAction = ({ product }: Props) => {
)}
</div>
</div>
- {/* <span className='text-[12px] text-red-500 italic'>
- * {qtyPickUp} barang bisa di pickup
- </span> */}
</DesktopView>
{/* ===== MOBILE: grid kiri-kanan, kanan hanya qty ===== */}
@@ -263,12 +249,6 @@ const PriceAction = ({ product }: Props) => {
</Link>
)}
</div>
-
- {/* {qtyPickUp > 0 && (
- <div className='text-[12px] mt-1 text-red-500 italic'>
- * {qtyPickUp} barang bisa di pickup
- </div>
- )} */}
</div>
{/* Kanan: hanya qty, rata kanan */}
@@ -295,9 +275,9 @@ const PriceAction = ({ product }: Props) => {
value={quantityInput}
onChange={(e) => setQuantityInput(e.target.value)}
className='h-11 md:h-12 w-16 md:w-20 text-center text-lg md:text-xl outline-none border-x
- [appearance:textfield]
- [&::-webkit-outer-spin-button]:appearance-none
- [&::-webkit-inner-spin-button]:appearance-none'
+ [appearance:textfield]
+ [&::-webkit-outer-spin-button]:appearance-none
+ [&::-webkit-inner-spin-button]:appearance-none'
disabled={!hasPrice}
/>
@@ -335,11 +315,13 @@ const PriceAction = ({ product }: Props) => {
)}
</div>
<div className='mt-4'>
+ {/* 2. TERUSKAN onCompare KE SINI */}
<AddToQuotation
source='buy'
products={product}
variantId={activeVariantId}
quantity={Number(quantityInput)}
+ onCompare={onCompare}
/>
</div>
</DesktopView>
@@ -376,4 +358,4 @@ const PriceAction = ({ product }: Props) => {
);
};
-export default PriceAction;
+export default PriceAction; \ No newline at end of file
diff --git a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
new file mode 100644
index 00000000..97f1d101
--- /dev/null
+++ b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
@@ -0,0 +1,492 @@
+import React, { useEffect, useState, useRef } from 'react'; // Tambah useRef
+import {
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalCloseButton,
+ Button,
+ Text,
+ Box,
+ Badge,
+ Grid,
+ GridItem,
+ Image,
+ Input,
+ InputGroup,
+ InputLeftElement,
+ VStack,
+ HStack,
+ IconButton,
+ Flex,
+ Icon,
+ Spinner,
+ List,
+ ListItem,
+ useToast,
+ Select,
+ useOutsideClick // Tambah import ini
+} from '@chakra-ui/react';
+
+import { Search, Trash2 } from 'lucide-react';
+
+// --- HELPER FORMATTING ---
+const formatPrice = (price: number) => {
+ return new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ minimumFractionDigits: 0,
+ }).format(price);
+};
+
+const renderSpecValue = (val: any) => {
+ if (!val || val === '-') return '-';
+ return String(val).replace(/<[^>]*>?/gm, '');
+};
+
+type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+ mainProduct: any;
+ selectedVariant: any;
+};
+
+const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant }: Props) => {
+ const toast = useToast();
+
+ // --- STATE ---
+ const [products, setProducts] = useState<(any | null)[]>([null, null, null, null]);
+ const [specsMatrix, setSpecsMatrix] = useState<any[]>([]);
+ const [isLoadingMatrix, setIsLoadingMatrix] = useState(false);
+
+ // Search State
+ const [activeSearchSlot, setActiveSearchSlot] = useState<number | null>(null);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchResults, setSearchResults] = useState<any[]>([]);
+ const [isSearching, setIsSearching] = useState(false);
+
+ // --- REF & OUTSIDE CLICK ---
+ const searchWrapperRef = useRef<HTMLDivElement>(null); // Ref untuk mendeteksi klik
+
+ useOutsideClick({
+ ref: searchWrapperRef,
+ handler: () => {
+ // Jika user klik di luar area search yang aktif, tutup search
+ if (activeSearchSlot !== null) {
+ setActiveSearchSlot(null);
+ setSearchResults([]); // Opsional: bersihkan hasil juga
+ }
+ },
+ });
+
+ // ===========================================================================
+ // 1. LOGIC UTAMA: ISI SLOT 1
+ // ===========================================================================
+ useEffect(() => {
+ if (isOpen && mainProduct) {
+ let activeItem = selectedVariant;
+
+ if (!activeItem && mainProduct.variants && mainProduct.variants.length > 0) {
+ activeItem = mainProduct.variants[0];
+ }
+ if (!activeItem) {
+ activeItem = mainProduct;
+ }
+
+ const targetId = activeItem.id;
+ const displayCode = activeItem.default_code || activeItem.code || activeItem.sku || mainProduct.default_code || mainProduct.code;
+
+ const variantOptions = mainProduct.variants?.map((v: any) => ({
+ id: v.id,
+ code: v.default_code || v.code || v.sku,
+ name: v.name,
+ price: v.price?.price || v.price || 0,
+ image: v.image
+ })) || [];
+
+ if (variantOptions.length === 0) {
+ variantOptions.push({
+ id: targetId,
+ code: displayCode,
+ name: mainProduct.name,
+ price: activeItem.price?.price || activeItem.price || 0,
+ image: activeItem.image || mainProduct.image
+ });
+ }
+
+ const productSlot1 = {
+ id: targetId,
+ sku: targetId,
+ realCode: displayCode,
+ name: mainProduct.name,
+ price: activeItem.price?.price || activeItem.price || mainProduct.lowest_price?.price || 0,
+ image: activeItem.image || mainProduct.image,
+ variants: variantOptions
+ };
+
+ setProducts((prev) => {
+ const newSlots = [...prev];
+ if (!newSlots[0] || String(newSlots[0].id) !== String(targetId)) {
+ newSlots[0] = productSlot1;
+ }
+ return newSlots;
+ });
+ }
+ }, [isOpen, mainProduct, selectedVariant]);
+
+ // ===========================================================================
+ // 2. FETCH SPECS
+ // ===========================================================================
+ useEffect(() => {
+ const validProducts = products.filter(p => p !== null);
+ if (!isOpen || validProducts.length === 0) return;
+
+ const fetchSpecs = async () => {
+ setIsLoadingMatrix(true);
+ try {
+ const allSkus = validProducts.map(p => p.sku).join(',');
+ const mainSku = validProducts[0]?.sku;
+
+ const res = await fetch(`/api/magento-product?skus=${allSkus}&main_sku=${mainSku}`);
+ if (!res.ok) return;
+
+ const data = await res.json();
+ if (data.specsMatrix) {
+ setSpecsMatrix(data.specsMatrix);
+ }
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setIsLoadingMatrix(false);
+ }
+ };
+
+ fetchSpecs();
+ }, [products, isOpen]);
+
+ // ===========================================================================
+ // 3. SEARCH LOGIC
+ // ===========================================================================
+ useEffect(() => {
+ const delayDebounceFn = setTimeout(async () => {
+ if (searchQuery.length > 0 && searchQuery.length < 3) {
+ setSearchResults([]);
+ return;
+ }
+
+ if (activeSearchSlot === null) return;
+
+ setIsSearching(true);
+ try {
+ const attrSetId = selectedVariant?.attribute_set_id || mainProduct?.attribute_set_id;
+ const queryParam = searchQuery === '' ? '*' : searchQuery;
+
+ const params = new URLSearchParams({
+ source: 'compare',
+ q: queryParam,
+ limit: '20',
+ fq: attrSetId ? `attribute_set_id_i:${attrSetId}` : ''
+ });
+
+ const res = await fetch(`/api/shop/search?${params.toString()}`);
+
+ if (res.ok) {
+ const data = await res.json();
+ setSearchResults(data.response?.products || []);
+ } else {
+ setSearchResults([]);
+ }
+ } catch (e) {
+ setSearchResults([]);
+ } finally {
+ setIsSearching(false);
+ }
+ }, 500);
+
+ return () => clearTimeout(delayDebounceFn);
+ }, [searchQuery, mainProduct, selectedVariant, activeSearchSlot]);
+
+ // ===========================================================================
+ // 4. HANDLERS
+ // ===========================================================================
+ const handleVariantChange = (slotIndex: number, newId: string) => {
+ const currentProduct = products[slotIndex];
+ if (!currentProduct || !currentProduct.variants) return;
+
+ const selectedVar = currentProduct.variants.find((v: any) => String(v.id) === String(newId));
+
+ if (selectedVar) {
+ const newProducts = [...products];
+ newProducts[slotIndex] = {
+ ...currentProduct,
+ id: selectedVar.id,
+ sku: selectedVar.id,
+ realCode: selectedVar.code,
+ price: selectedVar.price,
+ image: selectedVar.image
+ };
+ setProducts(newProducts);
+ }
+ };
+
+ const handleAddProduct = (searchItem: any, slotIndex: number) => {
+ const newProducts = [...products];
+
+ const idToAdd = searchItem.id;
+ const codeToAdd = searchItem.code;
+ const nameToAdd = searchItem.displayName || searchItem.name;
+ const imageToAdd = searchItem.image;
+ const priceToAdd = searchItem.lowestPrice?.price || 0;
+
+ if (newProducts.find(p => p && String(p.id) === String(idToAdd))) {
+ toast({ title: "Produk sudah ada", status: "warning", position: "top" });
+ return;
+ }
+
+ newProducts[slotIndex] = {
+ id: idToAdd,
+ sku: idToAdd,
+ realCode: codeToAdd,
+ name: nameToAdd,
+ price: priceToAdd,
+ image: imageToAdd,
+ variants: [{
+ id: idToAdd,
+ code: codeToAdd,
+ name: nameToAdd,
+ price: priceToAdd,
+ image: imageToAdd
+ }]
+ };
+
+ setProducts(newProducts);
+ setActiveSearchSlot(null);
+ setSearchQuery('');
+ setSearchResults([]);
+ };
+
+ const handleRemoveProduct = (index: number) => {
+ const newProducts = [...products];
+ newProducts[index] = null;
+ setProducts(newProducts);
+ if (newProducts.every(p => p === null)) setSpecsMatrix([]);
+ };
+
+ return (
+ <Modal isOpen={isOpen} onClose={onClose} size="6xl" scrollBehavior="inside">
+ <ModalOverlay />
+ <ModalContent height="90vh">
+ <ModalHeader borderBottom="1px solid #eee" pb={2}>
+ <HStack spacing={3}>
+ <Text fontSize="xl" fontWeight="bold">Bandingkan Produk</Text>
+ <Badge colorScheme="red" variant="solid" borderRadius="full" px={2}>
+ {products.filter(p => p !== null).length} Item
+ </Badge>
+ </HStack>
+ <Text fontSize="sm" color="gray.500" fontWeight="normal" mt={1}>
+ Detail Spesifikasi Produk yang kamu pilih
+ </Text>
+ </ModalHeader>
+ <ModalCloseButton />
+
+ <ModalBody p={6} bg="white">
+ <Grid templateColumns="200px repeat(4, 1fr)" gap={4}>
+ <GridItem />
+ {products.map((product, index) => (
+ <GridItem key={index} position="relative" minW="0">
+ {product ? (
+ <VStack align="stretch" spacing={3} h="100%">
+ {index !== 0 && (
+ <IconButton
+ aria-label="Hapus" icon={<Trash2 size={16}/>}
+ size="xs" position="absolute" top={-2} right={-2}
+ colorScheme="red" onClick={() => handleRemoveProduct(index)} zIndex={2}
+ />
+ )}
+ <Box h="160px" display="flex" alignItems="center" justifyContent="center" bg="gray.50" borderRadius="md" p={2}>
+ <Image
+ src={product.image || '/images/no-image-compare.svg'}
+ alt={product.name} maxH="100%" objectFit="contain"
+ onError={(e) => { (e.target as HTMLImageElement).src = '/images/no-image-compare.svg'; }}
+ />
+ </Box>
+ <Box>
+ <Text color="red.600" fontWeight="bold" fontSize="md">
+ {product.price > 0 ? formatPrice(product.price) : 'Hubungi Admin'}
+ </Text>
+ <Text fontSize="xs" fontWeight="bold" noOfLines={3} h="45px" title={product.name} mb={2}>
+ {product.name}
+ </Text>
+ </Box>
+
+ <Select
+ size="sm" borderRadius="md" fontSize="xs"
+ value={product.id}
+ onChange={(e) => handleVariantChange(index, e.target.value)}
+ isDisabled={false} bg="white"
+ >
+ {product.variants && product.variants.map((v: any) => (
+ <option key={v.id} value={v.id}>{v.code}</option>
+ ))}
+ </Select>
+
+ <HStack spacing={2}>
+ <IconButton
+ aria-label="Cart"
+ icon={<Image src="/images/keranjang-compare.svg" w="15px" h="15px" objectFit="contain" />}
+ variant="outline"
+ colorScheme="red"
+ size="sm"
+ />
+ <Button as="a" href={`/product/${product.id}`} target="_blank" colorScheme="red" size="sm" flex={1} fontSize="xs">
+ Beli Sekarang
+ </Button>
+ </HStack>
+ </VStack>
+ ) : (
+ <VStack align="stretch" spacing={3} h="100%" position="relative">
+
+ {/* WRAPPER SEARCH DENGAN REF */}
+ {/* Hanya berikan ref jika ini adalah slot yang sedang aktif dicari */}
+ <Box position="relative" w="100%" ref={activeSearchSlot === index ? searchWrapperRef : null}>
+ <InputGroup size="sm">
+ <InputLeftElement pointerEvents="none"><Icon as={Search} color="gray.300" /></InputLeftElement>
+ <Input
+ placeholder="Cari Produk..." borderRadius="md"
+ value={activeSearchSlot === index ? searchQuery : ''}
+ onFocus={() => { setActiveSearchSlot(index); setSearchQuery(''); }}
+ onChange={(e) => setSearchQuery(e.target.value)}
+ />
+ </InputGroup>
+
+ {/* HASIL SEARCH */}
+ {activeSearchSlot === index && (
+ <Box position="absolute" top="35px" left={0} right={0} bg="white" boxShadow="lg" zIndex={10} borderRadius="md" border="1px solid" borderColor="gray.200" maxH="250px" overflowY="auto">
+ {isSearching ? (
+ <Box p={4} textAlign="center"><Spinner size="sm" color="red.500"/></Box>
+ ) : searchResults.length > 0 ? (
+ <List spacing={0}>
+ {searchResults.map((res) => (
+ <ListItem
+ key={res.id}
+ p={2} borderBottom="1px solid #f0f0f0"
+ _hover={{ bg: 'red.50', cursor: 'pointer' }}
+ onClick={() => handleAddProduct(res, index)}
+ >
+ <Flex align="center" gap={2}>
+ <Image src={res.image || '/images/no-image-compare.svg'} boxSize="30px" objectFit="contain" onError={(e) => { (e.target as HTMLImageElement).src = '/images/no-image-compare.svg'; }} />
+ <Box>
+ <Text fontSize="xs" fontWeight="bold" noOfLines={1}>{res.displayName || res.name}</Text>
+ <Text fontSize="xs" color="red.500">
+ {formatPrice(res.lowestPrice?.price || 0)}
+ </Text>
+ </Box>
+ </Flex>
+ </ListItem>
+ ))}
+ </List>
+ ) : (
+ <Box p={3} fontSize="xs" color="gray.500" textAlign="center">
+ {searchQuery === '' ? 'Menampilkan rekomendasi...' : 'Tidak ditemukan.'}
+ </Box>
+ )}
+ </Box>
+ )}
+ </Box>
+
+ {/* SLOT KOSONG */}
+ <Flex
+ direction="column"
+ align="center"
+ justify="center"
+ flex={1}
+ bg="gray.50"
+ borderRadius="md"
+ >
+ <Image
+ src="/images/no-image-compare.svg"
+ alt="Empty Slot"
+ boxSize="125px"
+ mb={2}
+ opacity={0.6}
+ />
+ <Text fontSize="xs" color="gray.500" textAlign="center">
+ Produk Belum Ditambahkan
+ </Text>
+ </Flex>
+ </VStack>
+ )}
+ </GridItem>
+ ))}
+
+ {/* --- HEADER SPESIFIKASI --- */}
+ <GridItem colSpan={5} py={6} display="flex" alignItems="center" justifyContent="space-between">
+ <Box borderBottom="2px solid" borderColor="gray.100" pb={2} width="100%">
+ <HStack>
+ <Text fontSize="lg" fontWeight="bold">Spesifikasi Teknis</Text>
+ {isLoadingMatrix && specsMatrix.length > 0 && (
+ <HStack spacing={2}>
+ <Spinner size="xs" color="red.500" />
+ <Text fontSize="xs" color="gray.500">Updating...</Text>
+ </HStack>
+ )}
+ </HStack>
+ </Box>
+ </GridItem>
+
+ {/* --- MATRIX SPEK --- */}
+ {isLoadingMatrix && specsMatrix.length === 0 ? (
+ <GridItem colSpan={5} textAlign="center" py={10}>
+ <Spinner color="red.500" thickness="4px" size="xl" />
+ <Text mt={2} color="gray.500">Memuat data...</Text>
+ </GridItem>
+ ) : specsMatrix.length > 0 ? (
+ specsMatrix.map((row, rowIndex) => (
+ <React.Fragment key={row.code || rowIndex}>
+ <GridItem
+ py={3} px={2}
+ borderBottom="1px solid" borderColor="gray.100"
+ bg={rowIndex % 2 !== 0 ? "white" : "gray.50"}
+ display="flex" alignItems="center"
+ opacity={isLoadingMatrix ? 0.6 : 1}
+ transition="opacity 0.2s"
+ >
+ <Text fontWeight="bold" fontSize="sm" color="gray.700">{row.label}</Text>
+ </GridItem>
+
+ {products.map((product, colIndex) => {
+ const val = product ? (row.values[String(product.sku)] || '-') : '';
+ return (
+ <GridItem
+ key={`${row.code}-${colIndex}`}
+ py={3} px={2}
+ borderBottom="1px solid" borderColor="gray.100"
+ bg={rowIndex % 2 !== 0 ? "white" : "gray.50"}
+ display="flex" alignItems="center" justifyContent="center" textAlign="center"
+ opacity={isLoadingMatrix ? 0.6 : 1}
+ transition="opacity 0.2s"
+ >
+ {isLoadingMatrix && product && !row.values[String(product.sku)] ? (
+ <Spinner size="xs" color="gray.400"/>
+ ) : (
+ <Text fontSize="sm" color="gray.600">{renderSpecValue(val)}</Text>
+ )}
+ </GridItem>
+ );
+ })}
+ </React.Fragment>
+ ))
+ ) : (
+ <GridItem colSpan={5} py={10} textAlign="center" color="gray.500" bg="gray.50">
+ <Text>Data spesifikasi belum tersedia untuk produk ini.</Text>
+ </GridItem>
+ )}
+ </Grid>
+ </ModalBody>
+ </ModalContent>
+ </Modal>
+ );
+};
+
+export default ProductComparisonModal; \ No newline at end of file
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx
index 05b84260..54a0fb52 100644
--- a/src-migrate/modules/product-detail/components/ProductDetail.tsx
+++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx
@@ -49,6 +49,9 @@ import SimilarBottom from './SimilarBottom';
import SimilarSide from './SimilarSide';
import dynamic from 'next/dynamic';
+// 1. IMPORT MODAL (Baru)
+import ProductComparisonModal from './ProductComparisonModal';
+
import { gtagProductDetail } from '@/core/utils/googleTag';
type Props = {
@@ -101,6 +104,8 @@ const ProductDetail = ({ product }: Props) => {
const { isDesktop, isMobile } = useDevice();
const router = useRouter();
const [auth, setAuth] = useState<any>(null);
+
+ console.log('Render ProductDetail for product ID:', product);
// State Data dari Magento
const [specsMatrix, setSpecsMatrix] = useState<any[]>([]);
@@ -110,6 +115,9 @@ const ProductDetail = ({ product }: Props) => {
const [loadingSpecs, setLoadingSpecs] = useState(false);
+ // 2. STATE MODAL COMPARE (Baru)
+ const [isCompareOpen, setCompareOpen] = useState(false);
+
useEffect(() => {
try {
setAuth(getAuth() ?? null);
@@ -398,6 +406,15 @@ const ProductDetail = ({ product }: Props) => {
return (
<>
+ {/* 3. MODAL POPUP DIRENDER DISINI */}
+ {/* Render di luar layout utama agar tidak tertutup elemen lain */}
+ <ProductComparisonModal
+ isOpen={isCompareOpen}
+ onClose={() => setCompareOpen(false)}
+ mainProduct={product}
+ selectedVariant={selectedVariant}
+ />
+
<div className='relative'>
{isDesktop && !hasPrice && (
<div className='absolute inset-0 z-[20] flex items-center justify-center pointer-events-none select-none'>
@@ -478,8 +495,13 @@ const ProductDetail = ({ product }: Props) => {
<div className='md:w-8/12 px-4 md:pl-6'>
{!hasPrice && (
<div className='bg-red-50 p-2 py-1.5 rounded-lg border border-red-500 flex gap-1 items-center '>
- <AlertTriangle size={18} className='text-red-600 shrink-0 mx-2' />
- <h1 className='text-red-600 font-normal text-h-sm'>Maaf untuk saat ini Produk yang anda cari tidak tersedia</h1>
+ <AlertTriangle
+ size={18}
+ className='text-red-600 shrink-0 mx-2'
+ />
+ <h1 className='text-red-600 font-normal text-h-sm'>
+ Maaf untuk saat ini Produk yang anda cari tidak tersedia
+ </h1>
</div>
)}
<div className='h-6 md:h-0' />
@@ -493,8 +515,13 @@ const ProductDetail = ({ product }: Props) => {
<div className='md:w-8/12 px-4 md:pl-6 relative'>
{!hasPrice && (
<div className='bg-red-50 p-2 py-1.5 border-b border-red-500 flex gap-1 items-center w-screen relative left-1/2 right-1/2 -translate-x-1/2'>
- <AlertTriangle size={18} className='text-red-600 shrink-0 mx-2' />
- <h1 className='text-red-600 font-normal text-h-sm'>Maaf untuk saat ini Produk yang anda cari tidak tersedia</h1>
+ <AlertTriangle
+ size={18}
+ className='text-red-600 shrink-0 mx-2'
+ />
+ <h1 className='text-red-600 font-normal text-h-sm'>
+ Maaf untuk saat ini Produk yang anda cari tidak tersedia
+ </h1>
</div>
)}
<h1 className={style['title']}>{product.name}</h1>
@@ -514,7 +541,10 @@ const ProductDetail = ({ product }: Props) => {
<div className='h-2 md:h-10' />
{!!activeVariantId && !isApproval && (
- <ProductPromoSection product={product} productId={activeVariantId} />
+ <ProductPromoSection
+ product={product}
+ productId={activeVariantId}
+ />
)}
<div className='h-0 md:h-6' />
@@ -523,8 +553,24 @@ const ProductDetail = ({ product }: Props) => {
<div className={style['section-card']}>
<Tabs variant="unstyled">
<TabList borderBottom="1px solid" borderColor="gray.200">
- <Tab _selected={{ color: 'red.600', borderColor: 'red.600', borderBottomWidth: '3px', fontWeight: 'bold', marginBottom: '-1.5px' }} color="gray.500" fontWeight="medium" fontSize="sm" px={4} py={3}>Deskripsi</Tab>
- <Tab _selected={{ color: 'red.600', borderColor: 'red.600', borderBottomWidth: '3px', fontWeight: 'bold', marginBottom: '-1.5px' }} color="gray.500" fontWeight="medium" fontSize="sm" px={4} py={3}>Spesifikasi</Tab>
+ <Tab
+ _selected={{ color: 'red.600', borderColor: 'red.600', borderBottomWidth: '3px', fontWeight: 'bold', marginBottom: '-1.5px' }}
+ color="gray.500" fontWeight="medium" fontSize="sm" px={4} py={3}
+ >
+ Deskripsi
+ </Tab>
+ <Tab
+ _selected={{ color: 'red.600', borderColor: 'red.600', borderBottomWidth: '3px', fontWeight: 'bold', marginBottom: '-1.5px' }}
+ color="gray.500" fontWeight="medium" fontSize="sm" px={4} py={3}
+ >
+ Spesifikasi
+ </Tab>
+ <Tab
+ _selected={{ color: 'red.600', borderColor: 'red.600', borderBottomWidth: '3px', fontWeight: 'bold', marginBottom: '-1.5px' }}
+ color="gray.500" fontWeight="medium" fontSize="sm" px={4} py={3}
+ >
+ Detail Lainnya
+ </Tab>
</TabList>
<TabPanels>
@@ -729,13 +775,22 @@ const ProductDetail = ({ product }: Props) => {
</div>
</div>
- {/* ... (Bagian Sidebar & Bottom SAMA) ... */}
{isDesktop && (
<div className='md:w-3/12'>
- <PriceAction product={product} />
+ {/* 4. INTEGRASI: PASSING HANDLER MODAL KE PRICE ACTION */}
+ <PriceAction
+ product={product}
+ onCompare={() => setCompareOpen(true)}
+ />
+
<div className='flex gap-x-5 items-center justify-center py-4'>
- {/* ... Buttons ... */}
+ <Button as={Link} href={askAdminUrl} variant='link' target='_blank' colorScheme='gray' leftIcon={<MessageCircleIcon size={18} />} isDisabled={!hasPrice}>Ask Admin</Button>
+ <span>|</span>
+ <div className={hasPrice ? '' : 'opacity-40 pointer-events-none'}><AddToWishlist productId={product.id} /></div>
+ <span>|</span>
+ {canShare && (<RWebShare data={{ text: 'Check out this product', title: `${product.name} - Indoteknik.com`, url: (process.env.NEXT_PUBLIC_SELF_HOST || '') + (router?.asPath || '/'), }}><Button variant='link' colorScheme='gray' leftIcon={<Share2Icon size={18} />} isDisabled={!hasPrice}>Share</Button></RWebShare>)}
</div>
+
<div className='h-6' />
<div className={style['heading']}>Produk Serupa</div>
<div className='h-4' />
diff --git a/src-migrate/types/productVariant.ts b/src-migrate/types/productVariant.ts
index 5144e7c1..31cedf8c 100644
--- a/src-migrate/types/productVariant.ts
+++ b/src-migrate/types/productVariant.ts
@@ -4,6 +4,9 @@ export interface IProductVariantDetail {
code: string;
name: string;
weight: number;
+ attribute_set_id: number;
+ attribute_set_name: string;
+ search_keywords: string;
is_in_bu: boolean;
is_flashsale: {
remaining_time: number;
diff --git a/src/lib/product/components/Product/ProductDesktopVariant.jsx b/src/lib/product/components/Product/ProductDesktopVariant.jsx
index 6b4ab1e1..1584dc92 100644
--- a/src/lib/product/components/Product/ProductDesktopVariant.jsx
+++ b/src/lib/product/components/Product/ProductDesktopVariant.jsx
@@ -314,6 +314,11 @@ const ProductDesktopVariant = ({
fetchData();
}, [product]);
+ const getImageVariant = (img) => {
+ if (!img || img.trim() === '') return '/images/noimage.jpeg';
+ return `${img}?variant=True`;
+ };
+
return (
<DesktopView>
<div className='relative'>
@@ -332,8 +337,8 @@ const ProductDesktopVariant = ({
<div className='w-full flex flex-wrap'>
<div className='w-5/12'>
<Image
- src={product.image + '?variant=True'}
- alt={product.name}
+ src={getImageVariant(product?.image)}
+ alt={product?.name}
className='w-full h-[350px]'
/>
</div>
@@ -345,9 +350,9 @@ const ProductDesktopVariant = ({
size={18}
className='text-red-600 shrink-0 mx-2'
/>
- <h1 className='text-red-600 font-normal text-h-sm'>
+ <div className='text-red-600 font-normal text-h-lg p-2'>
Maaf untuk saat ini Produk yang anda cari tidak tersedia
- </h1>
+ </div>
</div>
)}
<h1 className='text-title-md leading-10 font-medium'>
diff --git a/src/lib/product/components/Product/ProductMobileVariant.jsx b/src/lib/product/components/Product/ProductMobileVariant.jsx
index 0f4953df..9d375deb 100644
--- a/src/lib/product/components/Product/ProductMobileVariant.jsx
+++ b/src/lib/product/components/Product/ProductMobileVariant.jsx
@@ -181,6 +181,11 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
return Math.floor(Math.random() * 100) + 1;
});
+ const getImageVariant = (img) => {
+ if (!img || img.trim() === '') return '/images/noimage.jpeg';
+ return `${img}?variant=True`;
+ };
+
return (
<MobileView>
<div className='relative'>
@@ -338,7 +343,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
</div>
<Image
- src={product.image + '?variant=True'}
+ src={getImageVariant(product?.image)}
alt={product.name}
className='h-72 object-contain object-center w-full border-b border-gray_r-4'
/>
@@ -532,7 +537,7 @@ const ProductMobileVariant = ({ product, wishlist, toggleWishlist }) => {
<div className='flex mt-4'>
<div className='w-[15%]'>
<Image
- src={product.image + '?variant=True'}
+ src={getImageVariant(product?.image)}
alt={product.name}
className='h-20 object-contain object-center w-full border border-gray_r-4'
/>
diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js
index 7d4adfcb..5ea6a70a 100644
--- a/src/pages/api/shop/search.js
+++ b/src/pages/api/shop/search.js
@@ -2,6 +2,14 @@ import { productMappingSolr } from '@/utils/solrMapping';
import axios from 'axios';
import camelcaseObjectDeep from 'camelcase-object-deep';
+const escapeSolrQuery = (query) => {
+ if (query == '*') return query;
+ query = query.replace(/-/g, ' ');
+ const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
+ const words = query.split(/\s+/);
+ return words.map((word) => specialChars.test(word) ? word.replace(specialChars, '\\$1') : word).join(' ');
+};
+
export default async function handler(req, res) {
const {
q = '*',
@@ -20,7 +28,99 @@ export default async function handler(req, res) {
let { stock = '' } = req.query;
// ============================================================
- // LOGIC KHUSUS UPSELL (Simple & Direct)
+ // [PERBAIKAN] 1. LOGIC KHUSUS COMPARE (PAKAI URLSearchParams)
+ // ============================================================
+ if (source === 'compare') {
+ try {
+ let qCompare = q === '*' ? '*:*' : q;
+
+ // Sanitasi Query
+ if (qCompare !== '*:*') {
+ // Kita escape, tapi biarkan stringnya bersih (jangan ditambah wildcard * manual)
+ // karena kita serahkan ke 'edismax' parser
+ qCompare = escapeSolrQuery(qCompare);
+ }
+
+ // [SOLUSI] Gunakan URLSearchParams untuk menyusun URL dengan aman
+ const params = new URLSearchParams();
+
+ params.append('q', qCompare);
+ params.append('rows', limit);
+ params.append('wt', 'json');
+ params.append('indent', 'true');
+
+ // Gunakan eDisMax parser (Otak Cerdas)
+ params.append('defType', 'edismax');
+
+ // Set Prioritas Pencarian (Boost ^)
+ // 1. default_code_s^20 : SKU persis (Prioritas Tertinggi)
+ // 2. search_keywords_t^10 : Field baru (Case insensitive)
+ // 3. display_name_s^1 : Cadangan
+ params.append('qf', 'default_code_s^20 search_keywords_t^10 display_name_s^1');
+
+ // Minimum Match 100% (Semua kata harus ada), ubah jika ingin lebih longgar
+ params.append('mm', '100%');
+
+ // Grouping
+ params.append('group', 'true');
+ params.append('group.field', 'template_id_i');
+ params.append('group.limit', '1');
+ params.append('group.main', 'true');
+
+ // Field List (fl)
+ params.append('fl', 'id,display_name_s,default_code_s,image_s,price_tier1_v2_f,attribute_set_id_i,attribute_set_name_s,template_id_i,product_id_i');
+
+ // Filter Query (fq) Dasar
+ params.append('fq', '-publish_b:false');
+ params.append('fq', 'price_tier1_v2_f:[1 TO *]');
+
+ // Logic Locking (Filter Attribute Set ID dari Frontend)
+ if (fq) {
+ if (Array.isArray(fq)) {
+ fq.forEach(f => params.append('fq', f));
+ } else {
+ params.append('fq', fq);
+ }
+ }
+
+ // Target Core: VARIANTS
+ // HAPUS parameter manual dari string URL, gunakan params object
+ const solrUrl = process.env.SOLR_HOST + '/solr/variants/select';
+
+ // Axios akan otomatis handle encoding % dan & dengan benar
+ const result = await axios.get(solrUrl, { params: params });
+
+ // Mapping Result
+ const mappedProducts = productMappingSolr(
+ result.data.response.docs,
+ false
+ );
+
+ const finalResponse = {
+ ...result.data,
+ response: {
+ ...result.data.response,
+ products: mappedProducts
+ }
+ };
+
+ delete finalResponse.response.docs;
+ const camelCasedData = camelcaseObjectDeep(finalResponse);
+
+ return res.status(200).json(camelCasedData);
+
+ } catch (e) {
+ console.error('[COMPARE SEARCH ERROR]', e.message);
+ if (e.response && e.response.data) {
+ // Log detail error dari Solr
+ console.error('[SOLR DETAILS]:', JSON.stringify(e.response.data, null, 2));
+ }
+ return res.status(200).json({ response: { products: [], numFound: 0 } });
+ }
+ }
+
+ // ============================================================
+ // LOGIC KHUSUS UPSELL (KODE LAMA ANDA)
// ============================================================
if (source === 'upsell') {
try {
@@ -88,7 +188,7 @@ export default async function handler(req, res) {
}
// ============================================================
- // SITEMAP (Biarkan tetap sama)
+ // SITEMAP (KODE LAMA ANDA)
// ============================================================
if (source === 'sitemap') {
try {
@@ -113,7 +213,7 @@ export default async function handler(req, res) {
}
// ============================================================
- // SEARCH NORMAL (LOGIKA LAMA)
+ // SEARCH NORMAL (KODE LAMA ANDA)
// ============================================================
let paramOrderBy = '';
@@ -219,12 +319,4 @@ export default async function handler(req, res) {
} catch (error) {
res.status(400).json({ error: error.message });
}
-}
-
-const escapeSolrQuery = (query) => {
- if (query == '*') return query;
- query = query.replace(/-/g, ' ');
- const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g;
- const words = query.split(/\s+/);
- return words.map((word) => specialChars.test(word) ? word.replace(specialChars, '\\$1') : word).join(' ');
-}; \ No newline at end of file
+} \ No newline at end of file
diff --git a/src/utils/solrMapping.js b/src/utils/solrMapping.js
index 33f0cbaf..8c0abcf1 100644
--- a/src/utils/solrMapping.js
+++ b/src/utils/solrMapping.js
@@ -127,6 +127,9 @@ export const variantsMappingSolr = (parent, products, pricelist) => {
variantTotal: product.variant_total_i || 0,
stockTotal: product.stock_total_f || 0,
weight: product.weight_f || 0,
+ attribute_set_id: product.attribute_set_id_i || 0,
+ attribute_set_name: product.attribute_set_name_s || '',
+ search_keywords: product.search_keywords_t || '',
manufacture: {},
parent: {},
qtySold: product?.qty_sold_f || 0,