diff options
| -rw-r--r-- | src-migrate/modules/product-detail/components/ProductComparisonModal.tsx | 776 |
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> |
