diff options
Diffstat (limited to 'src-migrate')
| -rw-r--r-- | src-migrate/modules/product-detail/components/Information.tsx | 2 | ||||
| -rw-r--r-- | src-migrate/modules/product-detail/components/ProductComparisonModal.tsx | 306 |
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> )} |
