summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFIN-IT_AndriFP <andrifebriyadiputra@gmail.com>2025-12-16 14:35:33 +0700
committerFIN-IT_AndriFP <andrifebriyadiputra@gmail.com>2025-12-16 14:35:33 +0700
commit31853dc731c6e4105c9cf9bd373c63e6e989caa4 (patch)
tree9d8ae27723cdc4d29c231f2456f9bd26966ca5a0
parent13b30c5da917264cb63f41d058f2dc66f28affcc (diff)
(andri) try compare with data
-rw-r--r--src-migrate/modules/product-detail/components/ProductComparisonModal.tsx547
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx4
-rw-r--r--src/pages/api/shop/search.js94
3 files changed, 449 insertions, 196 deletions
diff --git a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
index 14b177ad..a58ad5b2 100644
--- a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
+++ b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import {
Modal,
ModalOverlay,
@@ -18,94 +18,238 @@ import {
InputLeftElement,
VStack,
HStack,
- Select,
IconButton,
Flex,
- Icon, // Dibutuhkan untuk membungkus icon dari Lucide
+ Icon,
+ Spinner,
+ List,
+ ListItem,
+ useToast,
+ Select
} from '@chakra-ui/react';
-// Import hanya icon standar UI dari lucide-react (pengganti @chakra-ui/icons)
-import { Search } from 'lucide-react';
-
-// --- Dummy Data Types ---
-type Product = {
- id: string;
- name: string;
- price: string;
- originalPrice: string;
- image: string;
- variants: string[];
- specs: Record<string, string>;
+import { Search, ShoppingCart, 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;
};
-// --- Dummy Data Configuration ---
-const SPEC_LABELS = [
- 'Tipe Baterai',
- 'Kecepatan Tanpa Beban',
- 'Torsi Maksimum',
- 'Ukuran Drive',
- 'Berat Bersih',
- 'Garansi',
-];
-
-const DUMMY_PRODUCTS: (Product | null)[] = [
- {
- id: '1',
- name: 'TEKIRO Cordless Impact Wrench 1/2 Inch XV Brushless',
- price: 'Rp 999.999',
- originalPrice: 'Rp 1.500.000',
- image: '/images/no-image-compare.svg',
- variants: ['Unit Only', '1 Baterai Kit', '2 Baterai Kit'],
- specs: {
- 'Tipe Baterai': '20V Lithium-Ion',
- 'Kecepatan Tanpa Beban': '0-2400 RPM',
- 'Torsi Maksimum': '300 N.m',
- 'Ukuran Drive': '1/2 Inch',
- 'Berat Bersih': '1.5 Kg',
- 'Garansi': '1 Tahun',
- },
- },
- {
- id: '2',
- name: 'Makita Cordless Impact Wrench TW001GZ (40V)',
- price: 'Rp 2.450.000',
- originalPrice: 'Rp 3.000.000',
- image: '/images/no-image-compare.svg',
- variants: ['Unit Only', 'Full Set'],
- specs: {
- 'Tipe Baterai': '40V Max XGT',
- 'Kecepatan Tanpa Beban': '0-2500 RPM',
- 'Torsi Maksimum': '1350 N.m',
- 'Ukuran Drive': '3/4 Inch',
- 'Berat Bersih': '2.8 Kg',
- 'Garansi': '2 Tahun',
- },
- },
- {
- id: '3',
- name: 'DEWALT Max Brushless 1/2 High Torque Impact',
- price: 'Rp 3.100.000',
- originalPrice: 'Rp 3.500.000',
- image: '/images/no-image-compare.svg',
- variants: ['Unit Only'],
- specs: {
- 'Tipe Baterai': '20V XR Li-Ion',
- 'Kecepatan Tanpa Beban': '0-1900 RPM',
- 'Torsi Maksimum': '950 N.m',
- 'Ukuran Drive': '1/2 Inch',
- 'Berat Bersih': '2.6 Kg',
- 'Garansi': '3 Tahun',
- },
- },
- null, // Slot kosong
-];
-
-const ProductComparisonModal = ({ isOpen, onClose }: Props) => {
+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);
+
+ // ===========================================================================
+ // 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, // ID untuk API
+ 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 < 3) {
+ setSearchResults([]);
+ return;
+ }
+
+ setIsSearching(true);
+ try {
+ const attrSetId = selectedVariant?.attribute_set_id || mainProduct?.attribute_set_id;
+ const params = new URLSearchParams({
+ source: 'compare',
+ q: searchQuery,
+ limit: '5',
+ fq: attrSetId ? `attribute_set_id_i:${attrSetId}` : ''
+ });
+
+ const res = await fetch(`/api/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]);
+
+ // ===========================================================================
+ // 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 = (solrProduct: any, slotIndex: number) => {
+ const newProducts = [...products];
+
+ const idToAdd = solrProduct.product_id_i || solrProduct.id;
+ const codeToAdd = solrProduct.default_code_s || solrProduct.sku;
+
+ 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: solrProduct.display_name_s || solrProduct.name_s,
+ price: solrProduct.price_tier1_v2_f || 0,
+ image: solrProduct.image_s,
+ variants: [{
+ id: idToAdd,
+ code: codeToAdd,
+ name: solrProduct.name_s,
+ price: solrProduct.price_tier1_v2_f,
+ image: solrProduct.image_s
+ }]
+ };
+
+ 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 />
@@ -114,158 +258,189 @@ const ProductComparisonModal = ({ isOpen, onClose }: Props) => {
<HStack spacing={3}>
<Text fontSize="xl" fontWeight="bold">Bandingkan Produk</Text>
<Badge colorScheme="red" variant="solid" borderRadius="full" px={2}>
- Baru
+ {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
+ Detail Spesifikasi Produk yang kamu pilih
</Text>
</ModalHeader>
<ModalCloseButton />
- <ModalBody p={6}>
- {/* Main Grid Layout: 5 Columns */}
+ <ModalBody p={6} bg="white">
<Grid templateColumns="200px repeat(4, 1fr)" gap={4}>
- {/* Cell 1: Top Left Empty Space */}
+ {/* Cell 1: Kosong */}
<GridItem />
- {/* Cell 2-5: Render Products */}
- {DUMMY_PRODUCTS.map((product, index) => (
- <GridItem key={index}>
+ {/* Loop Slot Produk */}
+ {products.map((product, index) => (
+ <GridItem key={index} position="relative" minW="0">
{product ? (
- // --- KARTU PRODUK TERISI ---
- <VStack align="stretch" spacing={3}>
- {/* Search Bar (Pakai Icon Lucide) */}
- <InputGroup size="sm">
- <InputLeftElement pointerEvents="none">
- <Icon as={Search} color="gray.300" />
- </InputLeftElement>
- <Input placeholder="Cari Produk lain" borderRadius="md" />
- </InputGroup>
+ <VStack align="stretch" spacing={3} h="100%">
+
+ {/* Tombol Hapus */}
+ {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}
+ />
+ )}
- {/* Gambar Produk */}
- <Box h="180px" display="flex" alignItems="center" justifyContent="center">
+ {/* Gambar */}
+ <Box h="160px" display="flex" alignItems="center" justifyContent="center" bg="gray.50" borderRadius="md" p={2}>
<Image
- src={product.image}
+ src={product.image || '/images/noimage.jpeg'}
alt={product.name}
- maxH="100%"
- objectFit="contain"
+ maxH="100%" objectFit="contain"
+ onError={(e) => { (e.target as HTMLImageElement).src = '/images/noimage.jpeg'; }}
/>
</Box>
- {/* Harga */}
+ {/* Info Harga & Nama */}
<Box>
- <Text color="red.500" fontWeight="bold" fontSize="lg">
- {product.price}
+ <Text color="red.600" fontWeight="bold" fontSize="md">
+ {product.price > 0 ? formatPrice(product.price) : 'Hubungi Admin'}
</Text>
- <Text as="s" color="gray.400" fontSize="xs">
- {product.originalPrice}
+ {/* Margin Bottom agar tidak tertutup dropdown */}
+ <Text fontSize="xs" fontWeight="bold" noOfLines={2} h="35px" title={product.name} mb={2}>
+ {product.name}
</Text>
</Box>
- {/* Nama Produk */}
- <Text fontSize="sm" fontWeight="bold" noOfLines={2} h="40px">
- {product.name}
- </Text>
-
{/* Dropdown Varian */}
- <Select size="sm" placeholder="Pilih Varian" borderRadius="md">
- {product.variants.map((v) => (
- <option key={v} value={v}>{v}</option>
- ))}
+ <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>
- {/* Tombol Aksi (Pakai SVG Custom untuk Keranjang) */}
+ {/* Tombol */}
<HStack spacing={2}>
- <IconButton
- aria-label="Add to cart"
- icon={<Image src="/images/keranjang-compare.svg" w="20px" />}
- variant="outline"
- colorScheme="red"
- size="sm"
- w="40px"
- />
- <Button colorScheme="red" size="sm" flex={1} fontSize="xs">
- Beli Sekarang
- </Button>
+ <IconButton aria-label="Cart" icon={<Icon as={ShoppingCart} />} variant="outline" colorScheme="red" size="sm" />
+ <Button as="a" href={`/product/${product.id}`} target="_blank" colorScheme="red" size="sm" flex={1} fontSize="xs">
+ Lihat Detail
+ </Button>
</HStack>
</VStack>
) : (
- // --- SLOT KOSONG ---
- <VStack align="stretch" spacing={3} h="100%">
- <InputGroup size="sm">
- <InputLeftElement pointerEvents="none">
- <Icon as={Search} color="gray.300" />
- </InputLeftElement>
- <Input placeholder="Cari Produk lain" borderRadius="md" />
+ // SLOT KOSONG
+ <VStack align="stretch" spacing={3} h="100%" position="relative">
+ <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>
+
+ {activeSearchSlot === index && searchQuery.length > 0 && (
+ <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.product_id_i || 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_s || '/images/noimage.jpeg'} boxSize="30px" objectFit="contain" onError={(e) => { (e.target as HTMLImageElement).src = '/images/noimage.jpeg'; }} />
+ <Box>
+ <Text fontSize="xs" fontWeight="bold" noOfLines={1}>{res.display_name_s || res.name_s}</Text>
+ <Text fontSize="xs" color="red.500">{formatPrice(res.price_tier1_v2_f || 0)}</Text>
+ </Box>
+ </Flex>
+ </ListItem>
+ ))}
+ </List>
+ ) : (
+ <Box p={3} fontSize="xs" color="gray.500" textAlign="center">Tidak ditemukan.</Box>
+ )}
+ </Box>
+ )}
- <Flex
- direction="column"
- align="center"
- justify="center"
- h="300px"
- border="2px dashed"
- borderColor="gray.200"
- borderRadius="md"
- bg="gray.50"
- >
- {/* Placeholder Image (SVG Custom) */}
- <Image
- src="/images/no-image-compare.svg"
- w="60px"
- mb={4}
- opacity={0.5}
- />
- <Text fontSize="sm" color="gray.400" textAlign="center">
- Produk belum ditambahkan
- </Text>
+ <Flex direction="column" align="center" justify="center" flex={1} border="2px dashed" borderColor="gray.200" borderRadius="md" bg="gray.50" color="gray.400">
+ <Icon as={Search} w={8} h={8} opacity={0.3} mb={2} />
+ <Text fontSize="xs" textAlign="center">Tambah produk<br/>untuk membandingkan</Text>
</Flex>
</VStack>
)}
</GridItem>
))}
- {/* --- SEPARATOR HEADER SPEC --- */}
+ {/* --- BAGIAN SPESIFIKASI --- */}
<GridItem colSpan={5} py={6}>
- <Box>
+ <Box borderBottom="2px solid" borderColor="gray.100" pb={2}>
<Text fontSize="lg" fontWeight="bold">Spesifikasi Teknis</Text>
- <Text fontSize="sm" color="gray.500">Detail Spesifikasi Produk yang kamu pilih</Text>
</Box>
</GridItem>
- {/* --- TABLE SPEC --- */}
- {SPEC_LABELS.map((label, rowIndex) => (
- <React.Fragment key={label}>
- {/* Kolom Label */}
- <GridItem
- py={3}
- borderBottom="1px solid"
- borderColor="gray.100"
- bg={rowIndex % 2 !== 0 ? "white" : "gray.50"}
- >
- <Text fontWeight="bold" fontSize="sm" color="gray.700">
- {label}
- </Text>
- </GridItem>
+ {isLoadingMatrix ? (
+ <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}>
+ {/* Label (Kiri) */}
+ <GridItem
+ py={3}
+ px={2}
+ borderBottom="1px solid"
+ borderColor="gray.100"
+ bg={rowIndex % 2 !== 0 ? "white" : "gray.50"}
+ display="flex"
+ alignItems="center"
+ >
+ <Text fontWeight="bold" fontSize="sm" color="gray.700">{row.label}</Text>
+ </GridItem>
- {/* Kolom Value */}
- {DUMMY_PRODUCTS.map((product, colIndex) => (
- <GridItem
- key={`${label}-${colIndex}`}
- py={3}
- borderBottom="1px solid"
- borderColor="gray.100"
- bg={rowIndex % 2 !== 0 ? "white" : "gray.50"}
- >
- <Text fontSize="sm" color="gray.600">
- {product ? (product.specs[label] || '-') : '-'}
- </Text>
- </GridItem>
- ))}
- </React.Fragment>
- ))}
+ {/* Value (Rata Tengah) */}
+ {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" // [FIX] Flex untuk centering
+ alignItems="center" // [FIX] Vertical Center
+ justifyContent="center" // [FIX] Horizontal Center
+ textAlign="center" // [FIX] Text Align Center
+ >
+ <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>
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx
index 0f851560..58276f3c 100644
--- a/src-migrate/modules/product-detail/components/ProductDetail.tsx
+++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx
@@ -334,7 +334,9 @@ const ProductDetail = ({ product }: Props) => {
{/* Render di luar layout utama agar tidak tertutup elemen lain */}
<ProductComparisonModal
isOpen={isCompareOpen}
- onClose={() => setCompareOpen(false)}
+ onClose={() => setCompareOpen(false)}
+ mainProduct={product}
+ selectedVariant={selectedVariant}
/>
<div className='relative'>
diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js
index 7d4adfcb..90841e09 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,6 +28,82 @@ export default async function handler(req, res) {
let { stock = '' } = req.query;
// ============================================================
+ // [BARU] 1. LOGIC KHUSUS COMPARE (Wajib ditaruh paling atas)
+ // ============================================================
+ if (source === 'compare') {
+ try {
+ let qCompare = q === '*' ? '*:*' : q;
+
+ // Sanitasi Query
+ if (qCompare !== '*:*') {
+ const escaped = escapeSolrQuery(qCompare);
+ qCompare = `*${escaped}*`;
+ }
+
+ // Susun Parameter Solr
+ const parameter = [
+ `q=${encodeURIComponent(qCompare)}`,
+ `rows=${limit}`,
+ 'wt=json',
+ 'indent=true',
+ 'defType=edismax',
+
+ // Grouping agar varian tidak banjir (per template)
+ 'group=true',
+ 'group.field=template_id_i',
+ 'group.limit=1',
+ 'group.main=true',
+
+ // Field Wajib (Perhatikan: kita butuh product_id_i/default_code_s)
+ '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 Dasar
+ 'fq=-publish_b:false',
+ 'fq=price_tier1_v2_f:[1 TO *]'
+ ];
+
+ // Logic Locking (Filter Attribute Set ID dari Frontend)
+ // Frontend akan mengirim fq="attribute_set_id_i:9"
+ if (fq) {
+ if (Array.isArray(fq)) {
+ fq.forEach(f => parameter.push(`fq=${encodeURIComponent(f)}`));
+ } else {
+ parameter.push(`fq=${encodeURIComponent(fq)}`);
+ }
+ }
+
+ // Target Core: VARIANTS (Karena compare butuh data spesifik)
+ const solrUrl = process.env.SOLR_HOST + '/solr/variants/select?' + parameter.join('&');
+
+ const result = await axios(solrUrl);
+
+ // 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);
+ // Return JSON valid meski kosong, agar frontend tidak error syntax
+ return res.status(200).json({ response: { products: [], numFound: 0 } });
+ }
+ }
+
+ // ============================================================
// LOGIC KHUSUS UPSELL (Simple & Direct)
// ============================================================
if (source === 'upsell') {
@@ -219,12 +303,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