summaryrefslogtreecommitdiff
path: root/src-migrate/modules
diff options
context:
space:
mode:
authorFIN-IT_AndriFP <andrifebriyadiputra@gmail.com>2025-12-22 09:22:14 +0700
committerFIN-IT_AndriFP <andrifebriyadiputra@gmail.com>2025-12-22 09:22:14 +0700
commit190bd5c5f66fc319d91d7ad3979ef8ff125dbd52 (patch)
treefde8b945bd04c5fc5e8cd3fe531f6b392bc9ddc6 /src-migrate/modules
parentcb447f728d30e31f0cc4f4cb9f7363274925ceef (diff)
(andri) fix view
Diffstat (limited to 'src-migrate/modules')
-rw-r--r--src-migrate/modules/product-detail/components/ProductComparisonModal.tsx233
1 files changed, 147 insertions, 86 deletions
diff --git a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
index 43f43ac4..1190e95d 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 } from 'react';
+import React, { useEffect, useState, useRef } from 'react'; // Tambah useRef
import {
Modal,
ModalOverlay,
@@ -25,10 +25,11 @@ import {
List,
ListItem,
useToast,
- Select
+ Select,
+ useOutsideClick // Tambah import ini
} from '@chakra-ui/react';
-import { Search, ShoppingCart, Trash2 } from 'lucide-react';
+import { Search, Trash2 } from 'lucide-react';
// --- HELPER FORMATTING ---
const formatPrice = (price: number) => {
@@ -65,6 +66,20 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
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
// ===========================================================================
@@ -80,7 +95,6 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
}
const targetId = activeItem.id;
- // Ambil code dari properti yang tersedia
const displayCode = activeItem.default_code || activeItem.code || activeItem.sku || mainProduct.default_code || mainProduct.code;
const variantOptions = mainProduct.variants?.map((v: any) => ({
@@ -103,8 +117,8 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
const productSlot1 = {
id: targetId,
- sku: targetId, // ID untuk API & Key
- realCode: displayCode, // String untuk Tampilan
+ sku: targetId,
+ realCode: displayCode,
name: mainProduct.name,
price: activeItem.price?.price || activeItem.price || mainProduct.lowest_price?.price || 0,
image: activeItem.image || mainProduct.image,
@@ -131,7 +145,6 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
const fetchSpecs = async () => {
setIsLoadingMatrix(true);
try {
- // Kirim ID ke API
const allSkus = validProducts.map(p => p.sku).join(',');
const mainSku = validProducts[0]?.sku;
@@ -153,34 +166,26 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
}, [products, isOpen]);
// ===========================================================================
- // 3. SEARCH LOGIC (MATCHED WITH API JSON)
+ // 3. SEARCH LOGIC
// ===========================================================================
useEffect(() => {
- // Gunakan timeout untuk debounce (menunggu user selesai ketik)
const delayDebounceFn = setTimeout(async () => {
-
- // LOGIKA BARU:
- // Jika ada text tapi kurang dari 3 huruf -> Jangan search (hemat API)
- // Jika text KOSONG (0) -> Lanjut search (ini untuk fitur 'klik muncul barang')
if (searchQuery.length > 0 && searchQuery.length < 3) {
setSearchResults([]);
return;
}
- // Jangan trigger search kalau slot belum dipilih (mencegah fetch saat modal baru buka)
if (activeSearchSlot === null) return;
setIsSearching(true);
try {
const attrSetId = selectedVariant?.attribute_set_id || mainProduct?.attribute_set_id;
-
- // Tentukan Query: Kalau kosong pakai bintang (*), kalau ada isi pakai isinya
const queryParam = searchQuery === '' ? '*' : searchQuery;
const params = new URLSearchParams({
source: 'compare',
- q: queryParam, // <-- Pakai logika wildcard
- limit: '20', // <-- Ubah limit jadi 20
+ q: queryParam,
+ limit: '20',
fq: attrSetId ? `attribute_set_id_i:${attrSetId}` : ''
});
@@ -228,16 +233,12 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
const handleAddProduct = (searchItem: any, slotIndex: number) => {
const newProducts = [...products];
- // [FIX] MAPPING DARI JSON API ANDA (CAMELCASE)
- // JSON: { id: 88019, code: "RX-SP0006", displayName: "...", lowestPrice: { price: ... } }
-
const idToAdd = searchItem.id;
- const codeToAdd = searchItem.code; // Langsung 'code', bukan 'default_code_s'
+ const codeToAdd = searchItem.code;
const nameToAdd = searchItem.displayName || searchItem.name;
const imageToAdd = searchItem.image;
const priceToAdd = searchItem.lowestPrice?.price || 0;
- // Cek Duplikat
if (newProducts.find(p => p && String(p.id) === String(idToAdd))) {
toast({ title: "Produk sudah ada", status: "warning", position: "top" });
return;
@@ -245,8 +246,8 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
newProducts[slotIndex] = {
id: idToAdd,
- sku: idToAdd, // ID untuk API
- realCode: codeToAdd, // Code String untuk Tampilan
+ sku: idToAdd,
+ realCode: codeToAdd,
name: nameToAdd,
price: priceToAdd,
image: imageToAdd,
@@ -284,7 +285,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 />
@@ -331,95 +332,155 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
</Select>
<HStack spacing={2}>
- <IconButton aria-label="Cart" icon={<Icon as={ShoppingCart} />} variant="outline" colorScheme="red" size="sm" />
+ <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">
- Lihat Detail
+ Beli Sekarang
</Button>
</HStack>
</VStack>
) : (
<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>
-
- {/* --- HASIL SEARCH (MAPPING FIX) --- */}
- {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">
-
- {/* Tampilkan Loading jika sedang searching */}
- {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>
- ) : (
- // Logic tambahan: Jika input kosong tapi hasil 0 (biasanya belum kelar loading awal), jangan tampilkan "Tidak Ditemukan"
- // Tapi kalau sudah ngetik dan hasil 0, baru tampilkan "Tidak Ditemukan"
- <Box p={3} fontSize="xs" color="gray.500" textAlign="center">
- {searchQuery === '' ? 'Menampilkan rekomendasi...' : 'Tidak ditemukan.'}
- </Box>
- )}
- </Box>
- )}
- <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>
+ {/* 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="100px"
+ mb={2}
+ opacity={0.6}
+ />
+ <Text fontSize="xs" color="gray.500" textAlign="center">
+ Produk Belum Ditambahkan
+ </Text>
</Flex>
</VStack>
)}
</GridItem>
))}
- <GridItem colSpan={5} py={6}>
- <Box borderBottom="2px solid" borderColor="gray.100" pb={2}><Text fontSize="lg" fontWeight="bold">Spesifikasi Teknis</Text></Box>
+ {/* --- 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>
- {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>
+ {/* --- 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">
+ <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">
- <Text fontSize="sm" color="gray.600">{renderSpecValue(val)}</Text>
+ <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>
+ <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>