summaryrefslogtreecommitdiff
path: root/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
diff options
context:
space:
mode:
authorFIN-IT_AndriFP <andrifebriyadiputra@gmail.com>2026-01-19 09:39:25 +0700
committerFIN-IT_AndriFP <andrifebriyadiputra@gmail.com>2026-01-19 09:39:25 +0700
commitf56cc888934d4b4ef962e967d40533ab5ded2414 (patch)
tree5448da31a5136939aa19d5c85fa571c376c93e8e /src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
parentd885bbb998c31c809b0ff77faa4695c1335a3717 (diff)
(andri) fix view mobile compare
Diffstat (limited to 'src-migrate/modules/product-detail/components/ProductComparisonModal.tsx')
-rw-r--r--src-migrate/modules/product-detail/components/ProductComparisonModal.tsx527
1 files changed, 302 insertions, 225 deletions
diff --git a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
index 00eaebe5..b26be520 100644
--- a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
+++ b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
@@ -6,6 +6,12 @@ import {
ModalHeader,
ModalBody,
ModalCloseButton,
+ Drawer, // Tambahan untuk Mobile
+ DrawerOverlay,
+ DrawerContent,
+ DrawerHeader,
+ DrawerBody,
+ DrawerCloseButton,
Button,
Text,
Box,
@@ -26,7 +32,9 @@ import {
List,
ListItem,
useToast,
- useOutsideClick
+ useOutsideClick,
+ useBreakpointValue, // Tambahan untuk Mobile
+ Divider // Tambahan untuk Mobile
} from '@chakra-ui/react';
import {
@@ -36,7 +44,7 @@ import {
AutoCompleteList,
} from '@choc-ui/chakra-autocomplete';
-import { Search, Trash2, ChevronDown } from 'lucide-react';
+import { Search, Trash2, ChevronDown, X } from 'lucide-react';
// --- HELPER FORMATTING ---
const formatPrice = (price: number) => {
@@ -82,6 +90,8 @@ type Props = {
const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant }: Props) => {
const toast = useToast();
+ // Deteksi Mobile
+ const isMobile = useBreakpointValue({ base: true, md: false });
const [products, setProducts] = useState<(any | null)[]>([null, null, null, null]);
const [specsMatrix, setSpecsMatrix] = useState<any[]>([]);
@@ -150,7 +160,7 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
const tempActiveVar = {
code: displayCode,
- name: displayName,
+ name: displayName,
displayName: displayName,
attributes: activeItem.attributes || []
};
@@ -273,7 +283,6 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
const currentProduct = products[slotIndex];
if (!currentProduct || !currentProduct.variants) return;
- // Cari varian yang labelnya cocok dengan string input
const selectedVar = currentProduct.variants.find((v: any) => {
return getVariantLabel(v) === selectedValueString;
});
@@ -334,11 +343,10 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
}
}
- // Siapkan object sementara untuk generate Label (Memanfaatkan Extract Attribute)
const tempVar = {
code: codeToAdd,
name: nameToAdd,
- displayName: searchItem.displayName || nameToAdd, // Pastikan ada displayName untuk diparsing
+ displayName: searchItem.displayName || nameToAdd,
attributes: searchItem.attributes || []
};
const initialLabel = getVariantLabel(tempVar);
@@ -354,7 +362,7 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
id: idToAdd,
code: codeToAdd,
name: nameToAdd,
- displayName: searchItem.displayName || nameToAdd, // Simpan untuk varian sendiri
+ displayName: searchItem.displayName || nameToAdd,
price: priceToAdd,
image: imageToAdd,
attributes: searchItem.attributes || []
@@ -391,10 +399,10 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
id: s.variantId || s.productIdI || s.id,
code: s.defaultCode || s.default_code || s.code,
name: s.displayName || s.name || s.nameS,
- displayName: s.displayName, // Wajib disimpan agar extractAttribute bisa bekerja
+ displayName: s.displayName,
price: s.lowestPrice?.price || s.priceTier1V2F || 0,
image: s.image || s.imageS,
- attributes: [] // Biarkan kosong, nanti dihandle extractAttribute via displayName
+ attributes: []
}));
allVariants.sort((a: any, b: any) =>
@@ -426,6 +434,290 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
if (newProducts.every(p => p === null)) setSpecsMatrix([]);
};
+ // --- RENDER SLOT ITEM ---
+ const renderProductSlot = (product: any, index: number) => {
+ if (product) {
+ return (
+ <VStack align="stretch" spacing={3} h="100%">
+ {index !== 0 && (
+ <IconButton
+ aria-label="Hapus" icon={<Trash2 size={16} />}
+ size="xs" position="absolute" top={-2} right={-2}
+ colorScheme="red" onClick={() => handleRemoveProduct(index)} zIndex={2}
+ />
+ )}
+ <Box h="160px" display="flex" alignItems="center" justifyContent="center" bg="gray.50" borderRadius="md" p={2}>
+ <Image
+ src={product.image || '/images/no-image-compare.svg'}
+ alt={product.name} maxH="100%" objectFit="contain"
+ onError={(e) => { (e.target as HTMLImageElement).src = '/images/no-image-compare.svg'; }}
+ />
+ </Box>
+ <Box>
+ <Text color="red.600" fontWeight="bold" fontSize="md">
+ {product.price > 0 ? formatPrice(product.price) : 'Hubungi Admin'}
+ </Text>
+ <Text fontSize="xs" fontWeight="bold" noOfLines={3} h="45px" title={product.name} mb={2}>
+ {product.name}
+ </Text>
+ </Box>
+
+ <Box w="100%">
+ <AutoComplete
+ openOnFocus
+ disableFilter={disableVariantFilter}
+ onChange={(val) => handleVariantChange(index, val)}
+ value={product.inputValue}
+ >
+ <InputGroup size="sm">
+ <AutoCompleteInput
+ variant="outline"
+ fontSize="xs"
+ borderRadius="md"
+ placeholder="Cari Varian..."
+ value={product.inputValue}
+ onChange={(e) => handleInputChange(index, e.target.value)}
+ onFocus={() => setDisableVariantFilter(true)}
+ />
+ <InputRightElement h="100%" pointerEvents="none">
+ <Icon as={ChevronDown} color="gray.400" size={14} />
+ </InputRightElement>
+ </InputGroup>
+
+ <AutoCompleteList fontSize="xs" maxH="250px" overflowY="auto" p={0}>
+ {product.variants && product.variants.map((v: any, vIdx: number) => {
+ const attributeText = extractAttribute(v);
+ const label = `${v.code} - ${attributeText}`;
+ return (
+ <AutoCompleteItem
+ key={`option-${vIdx}`}
+ value={label}
+ textTransform="capitalize"
+ _selected={{ bg: 'red.50', borderLeft: '3px solid red' }}
+ _focus={{ bg: 'gray.50' }}
+ p={2}
+ borderBottom="1px dashed"
+ borderColor="gray.200"
+ >
+ <Flex justify="space-between" align="start" w="100%">
+ <Box flex={1} mr={2} overflow="hidden">
+ <Text fontWeight="bold" fontSize="xs" color="gray.700">{v.code}</Text>
+ <Text fontSize="xs" color="gray.500" noOfLines={1} title={attributeText} textTransform="capitalize">
+ {attributeText}
+ </Text>
+ </Box>
+ <Text color="red.600" fontWeight="bold" fontSize="xs" whiteSpace="nowrap" bg="red.50" px={2} py={0.5} borderRadius="md" h="fit-content">
+ {formatPrice(v.price)}
+ </Text>
+ </Flex>
+ </AutoCompleteItem>
+ );
+ })}
+ </AutoCompleteList>
+ </AutoComplete>
+ </Box>
+
+ <HStack spacing={2}>
+ <IconButton
+ aria-label="Cart"
+ icon={<Image src="/images/keranjang.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>
+ );
+ }
+
+ return (
+ <VStack align="stretch" spacing={3} h="100%" position="relative">
+ <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)}
+ />
+ {activeSearchSlot === index && searchQuery && (
+ <InputRightElement cursor="pointer" onClick={() => { setSearchQuery(''); setActiveSearchSlot(null); }}>
+ <Icon as={X} color="gray.400" size={14}/>
+ </InputRightElement>
+ )}
+ </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>
+
+ <Flex
+ direction="column"
+ align="center"
+ justify="center"
+ flex={1}
+ bg="gray.50"
+ borderRadius="md"
+ >
+ <Image
+ src="/images/no-image-compare.svg"
+ alt="Empty Slot"
+ boxSize="125px"
+ mb={2}
+ opacity={0.6}
+ />
+ <Text fontSize="xs" color="gray.500" textAlign="center">
+ Produk Belum Ditambahkan
+ </Text>
+ </Flex>
+ </VStack>
+ );
+ };
+
+ // --- RENDER MOBILE CONTENT ---
+ const renderMobileContent = () => {
+ const mobileProducts = products.slice(0, 2);
+
+ return (
+ <Box pb={6}>
+ {/* Sticky Header */}
+ <Box
+ position="sticky" top="-16px" zIndex={10}
+ bg="white" pt={4} pb={2} mx="-16px" px="16px"
+ borderBottom="1px solid" borderColor="gray.100" shadow="sm"
+ >
+ <Grid templateColumns="1fr 1fr" gap={4} mb={4}>
+ {mobileProducts.map((p, i) => (
+ <GridItem key={i} position="relative">
+ {renderProductSlot(p, i)}
+ </GridItem>
+ ))}
+ </Grid>
+
+ <Flex justify="center" align="center" gap={2}>
+ <Text fontSize="md" fontWeight="bold" color="gray.700">
+ Spesifikasi Teknis
+ </Text>
+ {/* Loader Header Hapus saja, kita pindah ke per-item */}
+ </Flex>
+ </Box>
+
+ {/* Specs List with Loader per Line */}
+ <Box mt={4}>
+ {specsMatrix.length > 0 ? (
+ <VStack spacing={0} align="stretch" divider={<Divider />}>
+ {specsMatrix.map((row, rIdx) => (
+ <Box key={rIdx} py={4}>
+ <Grid templateColumns="1fr 1fr" gap={4}>
+ {mobileProducts.map((p, cIdx) => {
+ const val = p ? (row.values[String(p.sku)] || '-') : '-';
+ // Logic Loader Per Item
+ const isItemLoading = isLoadingMatrix && p && !row.values[String(p.sku)];
+
+ return (
+ <VStack key={cIdx} spacing={1} align="center">
+ {/* VALUE (SPINNER JIKA LOADING) */}
+ {isItemLoading ? (
+ <Spinner size="xs" color="red.500" />
+ ) : (
+ <Text fontWeight="semibold" fontSize="s" color="gray.800" textAlign="center" lineHeight="shorter">
+ {renderSpecValue(val)}
+ </Text>
+ )}
+
+ {/* LABEL */}
+ <Text fontSize="xs" color="gray.600" fontWeight="normal" textAlign="center">
+ {row.label}
+ </Text>
+ </VStack>
+ )
+ })}
+ </Grid>
+ </Box>
+ ))}
+ </VStack>
+ ) : (
+ <Box textAlign="center" py={10} color="gray.500">
+ {isLoadingMatrix ? <VStack><Spinner color="red.500" /><Text fontSize="xs">Memuat data...</Text></VStack> : "Data spesifikasi tidak tersedia"}
+ </Box>
+ )}
+ </Box>
+ </Box>
+ );
+ };
+
+ // --- MAIN RENDER ---
+
+ // Tampilan Mobile (Drawer 75%)
+ if (isMobile) {
+ return (
+ <Drawer isOpen={isOpen} placement="bottom" onClose={onClose}>
+ <DrawerOverlay />
+ <DrawerContent borderTopRadius="20px" h="88vh" bg="white">
+ <DrawerCloseButton zIndex={20} />
+ <DrawerHeader borderBottomWidth="1px" fontSize="md" textAlign="center">Bandingkan Produk</DrawerHeader>
+ <DrawerBody p={4} overflowY="auto">
+ {renderMobileContent()}
+ </DrawerBody>
+ </DrawerContent>
+ </Drawer>
+ );
+ }
+
+ // Tampilan Desktop (Modal 6XL)
return (
<Modal isOpen={isOpen} onClose={onClose} size="6xl" scrollBehavior="inside">
<ModalOverlay />
@@ -448,222 +740,7 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
<GridItem />
{products.map((product, index) => (
<GridItem key={index} position="relative" minW="0">
- {product ? (
- <VStack align="stretch" spacing={3} h="100%">
- {index !== 0 && (
- <IconButton
- aria-label="Hapus" icon={<Trash2 size={16} />}
- size="xs" position="absolute" top={-2} right={-2}
- colorScheme="red" onClick={() => handleRemoveProduct(index)} zIndex={2}
- />
- )}
- <Box h="160px" display="flex" alignItems="center" justifyContent="center" bg="gray.50" borderRadius="md" p={2}>
- <Image
- src={product.image || '/images/no-image-compare.svg'}
- alt={product.name} maxH="100%" objectFit="contain"
- onError={(e) => { (e.target as HTMLImageElement).src = '/images/no-image-compare.svg'; }}
- />
- </Box>
- <Box>
- <Text color="red.600" fontWeight="bold" fontSize="md">
- {product.price > 0 ? formatPrice(product.price) : 'Hubungi Admin'}
- </Text>
- <Text fontSize="xs" fontWeight="bold" noOfLines={3} h="45px" title={product.name} mb={2}>
- {product.name}
- </Text>
- </Box>
-
- {/* --- AUTOCOMPLETE DROPDOWN (FINAL FIX) --- */}
- <Box w="100%">
- <AutoComplete
- openOnFocus
- disableFilter={disableVariantFilter}
- onChange={(val) => handleVariantChange(index, val)}
- value={product.inputValue}
- >
- <InputGroup size="sm">
- <AutoCompleteInput
- variant="outline"
- fontSize="xs"
- borderRadius="md"
- placeholder="Cari Varian..."
- value={product.inputValue}
- onChange={(e) => handleInputChange(index, e.target.value)}
- onFocus={() => setDisableVariantFilter(true)}
- />
- <InputRightElement h="100%" pointerEvents="none">
- <Icon as={ChevronDown} color="gray.400" size={14} />
- </InputRightElement>
- </InputGroup>
-
- {/* Dropdown List dengan Style Information.tsx */}
- <AutoCompleteList fontSize="xs" maxH="250px" overflowY="auto" p={0}>
- {product.variants && product.variants.map((v: any, vIdx: number) => {
-
- // Gunakan Helper extractAttribute untuk mendapatkan teks spesifikasi
- // Ini akan bekerja baik untuk Slot 1 (pakai attributes)
- // maupun Slot Search (parsing displayName)
- const attributeText = extractAttribute(v);
- const label = `${v.code} - ${attributeText}`;
-
- return (
- <AutoCompleteItem
- key={`option-${vIdx}`}
- value={label}
- textTransform="capitalize"
- _selected={{ bg: 'red.50', borderLeft: '3px solid red' }}
- _focus={{ bg: 'gray.50' }}
- p={2}
- borderBottom="1px dashed" // Style dashed border
- borderColor="gray.200"
- >
- <Flex justify="space-between" align="start" w="100%">
- {/* KIRI: KODE & ATRIBUT */}
- <Box flex={1} mr={2} overflow="hidden">
- <Text fontWeight="bold" fontSize="xs" color="gray.700">
- {v.code}
- </Text>
- <Text
- fontSize="xs"
- color="gray.500"
- noOfLines={1}
- title={attributeText}
- textTransform="capitalize"
- >
- {attributeText}
- </Text>
- </Box>
-
- {/* KANAN: HARGA */}
- <Text
- color="red.600"
- fontWeight="bold"
- fontSize="xs"
- whiteSpace="nowrap"
- bg="red.50"
- px={2}
- py={0.5}
- borderRadius="md"
- h="fit-content"
- >
- {formatPrice(v.price)}
- </Text>
- </Flex>
- </AutoCompleteItem>
- );
- })}
- </AutoCompleteList>
- </AutoComplete>
- </Box>
-
- <HStack spacing={2}>
- <IconButton
- aria-label="Cart"
- icon={<Image src="/images/keranjang-compare.svg" w="15px" h="15px" objectFit="contain" />}
- variant="outline"
- colorScheme="red"
- size="sm"
- />
- <Button as="a" href={`/product/${product.id}`} target="_blank" colorScheme="red" size="sm" flex={1} fontSize="xs">
- Beli Sekarang
- </Button>
- </HStack>
- </VStack>
- ) : (
- <VStack align="stretch" spacing={3} h="100%" position="relative">
- <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>
-
- {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>
-
- <Flex
- direction="column"
- align="center"
- justify="center"
- flex={1}
- bg="gray.50"
- borderRadius="md"
- >
- <Image
- src="/images/no-image-compare.svg"
- alt="Empty Slot"
- boxSize="125px"
- mb={2}
- opacity={0.6}
- />
- <Text fontSize="xs" color="gray.500" textAlign="center">
- Produk Belum Ditambahkan
- </Text>
- </Flex>
- </VStack>
- )}
+ {renderProductSlot(product, index)}
</GridItem>
))}