summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src-migrate/modules/product-detail/components/Information.tsx2
-rw-r--r--src-migrate/modules/product-detail/components/ProductComparisonModal.tsx306
2 files changed, 200 insertions, 108 deletions
diff --git a/src-migrate/modules/product-detail/components/Information.tsx b/src-migrate/modules/product-detail/components/Information.tsx
index 6018f6a1..cc16fb6d 100644
--- a/src-migrate/modules/product-detail/components/Information.tsx
+++ b/src-migrate/modules/product-detail/components/Information.tsx
@@ -213,7 +213,7 @@ const Information = ({ product }: Props) => {
{/* === TOMBOL BANDINGKAN PRODUK (HANYA MOBILE) === */}
<MobileView>
<div
- className="flex items-center justify-between py-3 px-4 mt-4 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-50 transition-colors group"
+ className="w-full flex items-center justify-between py-3 px-1 mt-3 bg-white border-t border-b border-black-100 cursor-pointer hover:bg-gray-50 transition-colors group"
onClick={() => setIsCompareOpen(true)}
>
<div className="flex items-center gap-3">
diff --git a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
index 45deabb3..0a81cdba 100644
--- a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
+++ b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx
@@ -34,7 +34,8 @@ import {
useToast,
useOutsideClick,
useBreakpointValue,
- Divider
+ Divider,
+ ScaleFade
} from '@chakra-ui/react';
import {
@@ -44,7 +45,7 @@ import {
AutoCompleteList,
} from '@choc-ui/chakra-autocomplete';
-import { Search, Trash2, ChevronDown, X } from 'lucide-react';
+import { Search, Trash2, ChevronDown, X, Plus } from 'lucide-react';
// --- HELPER FORMATTING ---
const formatPrice = (price: number) => {
@@ -90,10 +91,9 @@ 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 [products, setProducts] = useState<(any | null)[]>([null, null]);
const [specsMatrix, setSpecsMatrix] = useState<any[]>([]);
const [isLoadingMatrix, setIsLoadingMatrix] = useState(false);
@@ -429,15 +429,29 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
const handleRemoveProduct = (index: number) => {
const newProducts = [...products];
- newProducts[index] = null;
+
+ if (newProducts.length > 2) {
+ newProducts.splice(index, 1);
+ } else {
+ newProducts[index] = null;
+ }
+
setProducts(newProducts);
if (newProducts.every(p => p === null)) setSpecsMatrix([]);
};
-
+ const handleAddSlot = () => {
+ if (products.length < 4) {
+ setProducts([...products, null]);
+ }
+ };
+
+ // --- RENDER SLOT ITEM (REUSABLE) ---
const renderProductSlot = (product: any, index: number) => {
+ let content;
if (product) {
- return (
+ // TAMPILAN TERISI
+ content = (
<VStack align="stretch" spacing={3} h="100%">
{index !== 0 && (
<IconButton
@@ -446,7 +460,7 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
colorScheme="red" onClick={() => handleRemoveProduct(index)} zIndex={2}
/>
)}
- <Box h="160px" display="flex" alignItems="center" justifyContent="center" bg="gray.50" borderRadius="md" p={2}>
+ <Box h="160px" display="flex" alignItems="center" justifyContent="center" bg="white" borderRadius="md" p={2}>
<Image
src={product.image || '/images/no-image-compare.svg'}
alt={product.name} maxH="100%" objectFit="contain"
@@ -531,99 +545,116 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
</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>
+ } else {
+ // TAMPILAN KOSONG
+ content = (
+ <VStack align="stretch" spacing={3} h="100%" position="relative">
+ {index !== 0 && products.length > 2 && (
+ <IconButton
+ aria-label="Hapus Kolom" icon={<X size={16} />}
+ size="xs" position="absolute" top={-2} right={-2}
+ colorScheme="gray" variant="solid" borderRadius="full" zIndex={2}
+ onClick={() => handleRemoveProduct(index)}
+ />
)}
- </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 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>
- )}
- </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>
+ <Flex
+ direction="column"
+ align="center"
+ justify="center"
+ flex={1}
+ bg="white"
+ 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>
+ );
+ }
+
+ // Animasi Wrapper ScaleFade
+ return (
+ <ScaleFade initialScale={0.9} in={true}>
+ {content}
+ </ScaleFade>
);
};
@@ -696,7 +727,6 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
// --- MAIN RENDER ---
- // Tampilan Mobile (Drawer 75% + Slim Scrollbar)
if (isMobile) {
return (
<Drawer isOpen={isOpen} placement="bottom" onClose={onClose}>
@@ -708,7 +738,8 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
p={4}
overflowY="auto"
css={{
- '&::-webkit-scrollbar': { width: '9px', height: '10px', },
+ '&::-webkit-scrollbar': { width: '4px' },
+ '&::-webkit-scrollbar-track': { width: '6px' },
'&::-webkit-scrollbar-thumb': { background: '#cbd5e0', borderRadius: '24px' },
}}
>
@@ -719,16 +750,37 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
);
}
- // Tampilan Desktop (Modal 6XL)
+ // Tampilan Desktop (Modal 6XL) - DYNAMIC GRID
+ const totalColumns = 1 + products.length + (products.length < 4 ? 1 : 0);
+ const productColumnsCount = products.length + (products.length < 4 ? 1 : 0);
+
+ const SLOT_WIDTH_PX = 200;
+ const LABEL_WIDTH_PX = 200;
+ const GAP_PX = 16;
+ const PADDING_PX = 48;
+
+ const calculatedWidth = LABEL_WIDTH_PX + (productColumnsCount * SLOT_WIDTH_PX) + (productColumnsCount * GAP_PX) + PADDING_PX + 'px';
+
return (
- <Modal isOpen={isOpen} onClose={onClose} size="6xl" scrollBehavior="inside">
+ <Modal
+ isOpen={isOpen}
+ onClose={onClose}
+ scrollBehavior="inside"
+ isCentered
+ >
<ModalOverlay />
- <ModalContent height="90vh">
+
+ <ModalContent
+ height="90vh"
+ maxW="95vw"
+ w={calculatedWidth}
+ transition="width 0.4s cubic-bezier(0.4, 0, 0.2, 1), max-width 0.4s ease"
+ >
<ModalHeader borderBottom="1px solid #eee" pb={2}>
<HStack spacing={3}>
<Text fontSize="xl" fontWeight="bold">Bandingkan Produk</Text>
- <Badge colorScheme="red" variant="solid" borderRadius="full" px={2}>
- {products.filter(p => p !== null).length} Item
+ <Badge colorScheme="red" variant="solid" borderRadius="full" px={2} textTransform="none">
+ Baru
</Badge>
</HStack>
<Text fontSize="sm" color="gray.500" fontWeight="normal" mt={1}>
@@ -737,8 +789,14 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
</ModalHeader>
<ModalCloseButton />
- <ModalBody p={6} bg="white">
- <Grid templateColumns="200px repeat(4, 1fr)" gap={4}>
+ <ModalBody p={6} bg="white" overflowX="auto">
+ <Grid
+ templateColumns={`${LABEL_WIDTH_PX}px repeat(${productColumnsCount}, ${SLOT_WIDTH_PX}px)`}
+ gap={4}
+ // [TAMBAH] Animasi Transisi Grid
+ transition="all 0.4s ease"
+ >
+
<GridItem />
{products.map((product, index) => (
<GridItem key={index} position="relative" minW="0">
@@ -746,7 +804,34 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
</GridItem>
))}
- <GridItem colSpan={5} py={6} display="flex" alignItems="center" justifyContent="space-between">
+ {/* Render Tombol Tambah Slot (Jika slot < 4) */}
+ {products.length < 4 && (
+ <GridItem display="flex" alignItems="center" justifyContent="center">
+ {/* [TAMBAH] Animasi Wrapper ScaleFade untuk Tombol */}
+ <ScaleFade initialScale={0.9} in={true} style={{ width: '100%', height: '100%' }}>
+ <Button
+ onClick={handleAddSlot}
+ variant="outline"
+ border="2px dashed"
+ borderColor="gray.300"
+ color="gray.500"
+ h="100%"
+ w="100%"
+ flexDirection="column"
+ gap={2}
+ _hover={{ bg: 'gray.50', borderColor: 'gray.400' }}
+ >
+ <Box bg="gray.100" p={2} borderRadius="full">
+ <Plus size={24} />
+ </Box>
+ <Text fontSize="sm">Tambah Produk</Text>
+ <Text fontSize="9px" fontWeight="normal">Bandingkan hingga 4 produk</Text>
+ </Button>s
+ </ScaleFade>
+ </GridItem>
+ )}
+
+ <GridItem colSpan={1 + productColumnsCount} 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>
@@ -761,7 +846,7 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
</GridItem>
{isLoadingMatrix && specsMatrix.length === 0 ? (
- <GridItem colSpan={5} textAlign="center" py={10}>
+ <GridItem colSpan={1 + productColumnsCount} textAlign="center" py={10}>
<Spinner color="red.500" thickness="4px" size="xl" />
<Text mt={2} color="gray.500">Memuat data...</Text>
</GridItem>
@@ -799,10 +884,17 @@ const ProductComparisonModal = ({ isOpen, onClose, mainProduct, selectedVariant
</GridItem>
);
})}
+ {products.length < 4 && (
+ <GridItem
+ bg={rowIndex % 2 !== 0 ? "white" : "gray.50"}
+ borderBottom="1px solid"
+ borderColor="gray.100"
+ />
+ )}
</React.Fragment>
))
) : (
- <GridItem colSpan={5} py={10} textAlign="center" color="gray.500" bg="gray.50">
+ <GridItem colSpan={1 + productColumnsCount} py={10} textAlign="center" color="gray.500" bg="gray.50">
<Text>Data spesifikasi belum tersedia untuk produk ini.</Text>
</GridItem>
)}