summaryrefslogtreecommitdiff
path: root/src-migrate
diff options
context:
space:
mode:
authorFIN-IT_AndriFP <andrifebriyadiputra@gmail.com>2026-01-14 16:16:06 +0700
committerFIN-IT_AndriFP <andrifebriyadiputra@gmail.com>2026-01-14 16:16:06 +0700
commit561c3c992bdbe743bbf2a4afe16d0019b4e0269c (patch)
treef3857a0a4f4f455067d343d601cc9a68d5fb675b /src-migrate
parent26ef51057f90c8f297dfaa90bf54fb46f83d74b0 (diff)
(andri) fix
Diffstat (limited to 'src-migrate')
-rw-r--r--src-migrate/modules/product-detail/components/ProductComparisonModal.tsx776
1 files changed, 380 insertions, 396 deletions
diff --git a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
index 4f1fcd82..f2d7ca57 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, { useEffect, useState, useRef } from 'react'; // Tambah useRef
+import React, { useEffect, useState, useRef } from 'react';
import {
Modal,
ModalOverlay,
@@ -26,7 +26,7 @@ import {
ListItem,
useToast,
Select,
- useOutsideClick // Tambah import ini
+ useOutsideClick
} from '@chakra-ui/react';
import { Search, Trash2 } from 'lucide-react';
@@ -41,20 +41,20 @@ const formatPrice = (price: number) => {
};
const renderSpecValue = (val: any) => {
- if (!val || val === '-') return '-';
- return String(val).replace(/<[^>]*>?/gm, '');
+ if (!val || val === '-') return '-';
+ return String(val).replace(/<[^>]*>?/gm, '');
};
type Props = {
isOpen: boolean;
onClose: () => void;
- mainProduct: any;
- selectedVariant: any;
+ 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[]>([]);
@@ -67,16 +67,15 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
const [isSearching, setIsSearching] = useState(false);
// --- REF & OUTSIDE CLICK ---
- const searchWrapperRef = useRef<HTMLDivElement>(null); // Ref untuk mendeteksi klik
+ const searchWrapperRef = useRef<HTMLDivElement>(null);
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
- }
+ if (activeSearchSlot !== null) {
+ setActiveSearchSlot(null);
+ setSearchResults([]);
+ }
},
});
@@ -86,53 +85,53 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
useEffect(() => {
if (isOpen && mainProduct) {
let activeItem = selectedVariant;
-
+
if (!activeItem && mainProduct.variants && mainProduct.variants.length > 0) {
- activeItem = mainProduct.variants[0];
+ activeItem = mainProduct.variants[0];
}
if (!activeItem) {
- activeItem = mainProduct;
+ 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 || v.displayName || v.display_name,
- price: v.price?.price || v.price || 0,
- image: v.image
+ id: v.id,
+ code: v.default_code || v.code || v.sku,
+ name: v.name || v.displayName || v.display_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
- });
+ variantOptions.push({
+ id: targetId,
+ code: displayCode,
+ name: mainProduct.name,
+ price: activeItem.price?.price || activeItem.price || 0,
+ image: activeItem.image || mainProduct.image
+ });
}
const displayName = activeItem.name || activeItem.displayName || mainProduct.name;
const productSlot1 = {
- id: targetId,
- sku: targetId,
- realCode: displayCode,
- name: displayName,
- price: activeItem.price?.price || activeItem.price || mainProduct.lowest_price?.price || 0,
- image: activeItem.image || mainProduct.image,
- variants: variantOptions
+ id: targetId,
+ sku: targetId,
+ realCode: displayCode,
+ name: displayName,
+ 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;
+ const newSlots = [...prev];
+ if (!newSlots[0] || String(newSlots[0].id) !== String(targetId)) {
+ newSlots[0] = productSlot1;
+ }
+ return newSlots;
});
}
}, [isOpen, mainProduct, selectedVariant]);
@@ -145,226 +144,223 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
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;
+ 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 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);
+ 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
+ // 3. SEARCH LOGIC
// ===========================================================================
useEffect(() => {
- const delayDebounceFn = setTimeout(async () => {
- if (searchQuery.length > 0 && searchQuery.length < 3) {
- setSearchResults([]);
- return;
- }
+ const delayDebounceFn = setTimeout(async () => {
+ if (searchQuery.length > 0 && searchQuery.length < 3) {
+ setSearchResults([]);
+ return;
+ }
- if (activeSearchSlot === null) return;
+ if (activeSearchSlot === null) return;
- const attrSetId = selectedVariant?.attribute_set_id || mainProduct?.attribute_set_id;
+ const attrSetId = selectedVariant?.attribute_set_id || mainProduct?.attribute_set_id;
- if (!attrSetId) {
- console.warn("Search dibatalkan: Produk utama tidak memiliki attribute_set_id");
- setSearchResults([]);
- setIsSearching(false);
- return;
- }
+ if (!attrSetId) {
+ console.warn("Search dibatalkan: Produk utama tidak memiliki attribute_set_id");
+ setSearchResults([]);
+ setIsSearching(false);
+ return;
+ }
- setIsSearching(true);
- try {
- const queryParam = searchQuery === '' ? '*' : searchQuery;
+ setIsSearching(true);
+ try {
+ const queryParam = searchQuery === '' ? '*' : searchQuery;
- const params = new URLSearchParams({
- source: 'compare',
- q: queryParam,
- limit: '20',
- fq: `attribute_set_id_i:${attrSetId}`
- });
+ const params = new URLSearchParams({
+ source: 'compare',
+ q: queryParam,
+ limit: '20',
+ fq: `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);
+ const res = await fetch(`/api/shop/search?${params.toString()}`);
+
+ if (res.ok) {
+ const data = await res.json();
+ setSearchResults(data.response?.products || []);
+ } else {
+ setSearchResults([]);
}
- }, 500);
+ } catch (e) {
+ setSearchResults([]);
+ } finally {
+ setIsSearching(false);
+ }
+ }, 500);
- return () => clearTimeout(delayDebounceFn);
+ 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,
- name: selectedVar.name,
- realCode: selectedVar.code,
- price: selectedVar.price,
- image: selectedVar.image
- };
- setProducts(newProducts);
- }
- };
+ 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,
+ name: selectedVar.name,
+ realCode: selectedVar.code,
+ price: selectedVar.price,
+ image: selectedVar.image
+ };
+ setProducts(newProducts);
+ }
+ };
const handleAddProduct = async (searchItem: any, slotIndex: number) => {
+ if (products.find(p => p && String(p.id) === String(searchItem.id))) {
+ toast({ title: "Produk sudah ada", status: "warning", position: "top" });
+ return;
+ }
- if (products.find(p => p && String(p.id) === String(searchItem.id))) {
- toast({ title: "Produk sudah ada", status: "warning", position: "top" });
- return;
- }
-
- const idToAdd = searchItem.id;
- const codeToAdd = searchItem.defaultCode || searchItem.default_code || searchItem.code;
- const nameToAdd = searchItem.displayName || searchItem.name;
- const imageToAdd = searchItem.image || searchItem.imageS || searchItem.image_s;
- const priceToAdd = searchItem.lowestPrice?.price || searchItem.priceTier1V2F || searchItem.price || 0;
-
- let parentId = searchItem.templateId ||
- searchItem.templateIdI ||
- searchItem.template_id_i ||
- searchItem.template_id;
-
- if (!parentId) {
- try {
- const checkParams = new URLSearchParams({
- source: 'upsell',
- q: '*:*',
- fq: `id:${idToAdd}`
- });
-
- const checkRes = await fetch(`/api/shop/search?${checkParams.toString()}`);
- if (checkRes.ok) {
- const checkData = await checkRes.json();
- const freshItem = checkData.response?.products?.[0];
-
- if (freshItem) {
- const serverReturnedId = freshItem.id;
- if (String(serverReturnedId) !== String(idToAdd)) {
- parentId = serverReturnedId;
- } else {
- parentId = freshItem.templateId || freshItem.templateIdI || idToAdd;
- }
- }
- }
- } catch (e) {
- console.error("Gagal validasi parent:", e);
- parentId = idToAdd;
+ const idToAdd = searchItem.id;
+ const codeToAdd = searchItem.defaultCode || searchItem.default_code || searchItem.code;
+ const nameToAdd = searchItem.displayName || searchItem.name;
+ const imageToAdd = searchItem.image || searchItem.imageS || searchItem.image_s;
+ const priceToAdd = searchItem.lowestPrice?.price || searchItem.priceTier1V2F || searchItem.price || 0;
+
+ let parentId = searchItem.templateId ||
+ searchItem.templateIdI ||
+ searchItem.template_id_i ||
+ searchItem.template_id;
+
+ if (!parentId) {
+ try {
+ const checkParams = new URLSearchParams({
+ source: 'upsell',
+ q: '*:*',
+ fq: `id:${idToAdd}`
+ });
+
+ const checkRes = await fetch(`/api/shop/search?${checkParams.toString()}`);
+ if (checkRes.ok) {
+ const checkData = await checkRes.json();
+ const freshItem = checkData.response?.products?.[0];
+
+ if (freshItem) {
+ const serverReturnedId = freshItem.id;
+ if (String(serverReturnedId) !== String(idToAdd)) {
+ parentId = serverReturnedId;
+ } else {
+ parentId = freshItem.templateId || freshItem.templateIdI || idToAdd;
+ }
}
+ }
+ } catch (e) {
+ console.error("Gagal validasi parent:", e);
+ parentId = idToAdd;
}
-
- const newProductEntry = {
- id: idToAdd,
- sku: idToAdd,
- realCode: codeToAdd,
- name: nameToAdd,
- price: priceToAdd,
- image: imageToAdd,
- variants: [{
- id: idToAdd,
- code: codeToAdd,
- name: nameToAdd,
- price: priceToAdd,
- image: imageToAdd
- }]
- };
+ }
- setProducts((prev) => {
- const newSlots = [...prev];
- newSlots[slotIndex] = newProductEntry;
- return newSlots;
- });
+ const newProductEntry = {
+ id: idToAdd,
+ sku: idToAdd,
+ realCode: codeToAdd,
+ name: nameToAdd,
+ price: priceToAdd,
+ image: imageToAdd,
+ variants: [{
+ id: idToAdd,
+ code: codeToAdd,
+ name: nameToAdd,
+ price: priceToAdd,
+ image: imageToAdd
+ }]
+ };
- setActiveSearchSlot(null);
- setSearchQuery('');
- setSearchResults([]);
-
- if (parentId) {
- try {
- const params = new URLSearchParams({
- source: 'upsell',
- limit: '100',
- fq: `template_id_i:${parentId}`
- });
-
- const res = await fetch(`/api/shop/search?${params.toString()}`);
-
- if (res.ok) {
- const data = await res.json();
- const siblings = data.response?.products || [];
-
- if (siblings.length > 0) {
- const allVariants = siblings.map((s: any) => ({
- id: s.variantId || s.productIdI || s.id,
- code: s.defaultCode || s.default_code || s.code,
- name: s.displayName || s.name || s.nameS,
- price: s.lowestPrice?.price || s.priceTier1V2F || 0,
- image: s.image || s.imageS
- }));
-
- allVariants.sort((a: any, b: any) =>
- String(a.code).localeCompare(String(b.code))
- );
-
- setProducts((prev) => {
- const updated = [...prev];
- if (updated[slotIndex] && String(updated[slotIndex].id) === String(idToAdd)) {
- updated[slotIndex] = {
- ...updated[slotIndex],
- variants: allVariants
- };
- }
- return updated;
- });
- }
+ setProducts((prev) => {
+ const newSlots = [...prev];
+ newSlots[slotIndex] = newProductEntry;
+ return newSlots;
+ });
+
+ setActiveSearchSlot(null);
+ setSearchQuery('');
+ setSearchResults([]);
+
+ if (parentId) {
+ try {
+ const params = new URLSearchParams({
+ source: 'upsell',
+ limit: '100',
+ fq: `template_id_i:${parentId}`
+ });
+
+ const res = await fetch(`/api/shop/search?${params.toString()}`);
+
+ if (res.ok) {
+ const data = await res.json();
+ const siblings = data.response?.products || [];
+
+ if (siblings.length > 0) {
+ const allVariants = siblings.map((s: any) => ({
+ id: s.variantId || s.productIdI || s.id,
+ code: s.defaultCode || s.default_code || s.code,
+ name: s.displayName || s.name || s.nameS,
+ price: s.lowestPrice?.price || s.priceTier1V2F || 0,
+ image: s.image || s.imageS
+ }));
+
+ allVariants.sort((a: any, b: any) =>
+ String(a.code).localeCompare(String(b.code))
+ );
+
+ setProducts((prev) => {
+ const updated = [...prev];
+ if (updated[slotIndex] && String(updated[slotIndex].id) === String(idToAdd)) {
+ updated[slotIndex] = {
+ ...updated[slotIndex],
+ variants: allVariants
+ };
}
- } catch (error) {
- console.error("Gagal fetch variant lain:", error);
+ return updated;
+ });
}
+ }
+ } catch (error) {
+ console.error("Gagal fetch variant lain:", error);
}
+ }
};
const handleRemoveProduct = (index: number) => {
- const newProducts = [...products];
- newProducts[index] = null;
- setProducts(newProducts);
- if (newProducts.every(p => p === null)) setSpecsMatrix([]);
+ const newProducts = [...products];
+ newProducts[index] = null;
+ setProducts(newProducts);
+ if (newProducts.every(p => p === null)) setSpecsMatrix([]);
};
return (
@@ -379,7 +375,7 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
</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 />
@@ -392,16 +388,16 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
{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}
- />
+ <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"
+ <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>
@@ -413,200 +409,188 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
{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"
+
+ <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>
- ))}
+ {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>
+ <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">
-
- {/* CEK ATTRIBUTE SET ID */}
- {!selectedVariant?.attribute_set_id && !mainProduct?.attribute_set_id ? (
- <Box p={4} fontSize="xs" color="orange.600" textAlign="center" bg="orange.50">
- <Text fontWeight="bold" mb={1}>Perbandingan Tidak Tersedia</Text>
- <Text>Produk utama tidak memiliki data kategori yang valid untuk dibandingkan.</Text>
- </Box>
- ) : (
- <>
- {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={3}
- borderBottom="1px solid #f0f0f0"
- _hover={{ bg: 'red.50', cursor: 'pointer' }}
- onClick={() => handleAddProduct(res, index)}
- >
- <Flex align="flex-start" gap={3}> {/* Align top biar rapi kalau teks panjang */}
- {/* GAMBAR */}
- <Image
- src={res.image || '/images/no-image-compare.svg'}
- boxSize="40px" // Gambar sedikit dibesarkan
- objectFit="contain"
- onError={(e) => { (e.target as HTMLImageElement).src = '/images/no-image-compare.svg'; }}
- flexShrink={0} // Mencegah gambar gepeng
- mt={1}
- />
-
- {/* TEXT CONTAINER */}
- <Box flex={1} w="0"> {/* w="0" adalah trik CSS agar text-overflow bekerja di dalam Flex */}
- <Text
- fontSize="xs"
- fontWeight="bold"
- noOfLines={2} // [SOLUSI UTAMA] Ijinkan maksimal 2 baris
- lineHeight="shorter" // Spasi antar baris dirapatkan
- whiteSpace="normal" // Pastikan text wrapping aktif
- mb={1}
- title={res.displayName || res.name} // Menampilkan tooltip native saat hover
- >
- {res.displayName || res.name}
- </Text>
- <Text fontSize="xs" color="red.500" fontWeight="bold">
- {formatPrice(res.lowestPrice?.price || 0)}
- </Text>
- </Box>
- </Flex>
- </ListItem>
- ))}
- </List>
- ) : (
- <Box p={3} fontSize="xs" color="gray.500" textAlign="center">
- {searchQuery === '' ? 'Menampilkan rekomendasi...' : 'Produk tidak ditemukan.'}
- </Box>
- )}
- </>
- )}
+ <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 && (
+ <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">
+ {!selectedVariant?.attribute_set_id && !mainProduct?.attribute_set_id ? (
+ <Box p={4} fontSize="xs" color="orange.600" textAlign="center" bg="orange.50">
+ <Text fontWeight="bold" mb={1}>Perbandingan Tidak Tersedia</Text>
+ <Text>Produk utama tidak memiliki data kategori yang valid untuk dibandingkan.</Text>
</Box>
- )}
+ ) : (
+ <>
+ {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={3}
+ borderBottom="1px solid #f0f0f0"
+ _hover={{ bg: 'red.50', cursor: 'pointer' }}
+ onClick={() => handleAddProduct(res, index)}
+ >
+ <Flex align="flex-start" gap={3}>
+ <Image
+ src={res.image || '/images/no-image-compare.svg'}
+ boxSize="40px"
+ objectFit="contain"
+ onError={(e) => { (e.target as HTMLImageElement).src = '/images/no-image-compare.svg'; }}
+ flexShrink={0}
+ mt={1}
+ />
+ <Box flex={1} w="0">
+ <Text
+ fontSize="xs"
+ fontWeight="bold"
+ noOfLines={2}
+ lineHeight="shorter"
+ whiteSpace="normal"
+ mb={1}
+ title={res.displayName || res.name}
+ >
+ {res.displayName || res.name}
+ </Text>
+ <Text fontSize="xs" color="red.500" fontWeight="bold">
+ {formatPrice(res.lowestPrice?.price || 0)}
+ </Text>
+ </Box>
+ </Flex>
+ </ListItem>
+ ))}
+ </List>
+ ) : (
+ <Box p={3} fontSize="xs" color="gray.500" textAlign="center">
+ {searchQuery === '' ? 'Menampilkan rekomendasi...' : 'Produk tidak ditemukan.'}
+ </Box>
+ )}
+ </>
+ )}
+ </Box>
+ )}
</Box>
- {/* SLOT KOSONG */}
- <Flex
- direction="column"
- align="center"
- justify="center"
- flex={1}
- bg="gray.50"
- borderRadius="md"
+ <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>
+ <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>
+ <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>
+ <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"
+ 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"
- >
- <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>
- ))
+ >
+ {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>
+ <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>