diff options
| author | FIN-IT_AndriFP <andrifebriyadiputra@gmail.com> | 2026-01-19 09:39:25 +0700 |
|---|---|---|
| committer | FIN-IT_AndriFP <andrifebriyadiputra@gmail.com> | 2026-01-19 09:39:25 +0700 |
| commit | f56cc888934d4b4ef962e967d40533ab5ded2414 (patch) | |
| tree | 5448da31a5136939aa19d5c85fa571c376c93e8e /src-migrate | |
| parent | d885bbb998c31c809b0ff77faa4695c1335a3717 (diff) | |
(andri) fix view mobile compare
Diffstat (limited to 'src-migrate')
3 files changed, 357 insertions, 269 deletions
diff --git a/src-migrate/modules/product-detail/components/Information.tsx b/src-migrate/modules/product-detail/components/Information.tsx index 6e2c930e..f3abe0c7 100644 --- a/src-migrate/modules/product-detail/components/Information.tsx +++ b/src-migrate/modules/product-detail/components/Information.tsx @@ -12,15 +12,21 @@ import { useEffect, useRef, useState } from 'react'; import axios from 'axios'; import currencyFormat from '@/core/utils/currencyFormat'; -import { InputGroup, InputRightElement, SimpleGrid, Flex, Text, Box, Center } from '@chakra-ui/react'; +import { InputGroup, InputRightElement, SimpleGrid, Flex, Text, Box, Center, Icon } from '@chakra-ui/react'; import { ChevronDownIcon } from '@heroicons/react/24/outline'; -import Image from 'next/image'; +import ImageNext from 'next/image'; import { formatToShortText } from '~/libs/formatNumber'; import { createSlug } from '~/libs/slug'; import { IProductDetail } from '~/types/product'; import { useProductDetail } from '../stores/useProductDetail'; import useVariant from '../hook/useVariant'; +// Import View Components +import MobileView from '@/core/components/views/MobileView'; // Pastikan path import benar + +// Import Modal Compare +import ProductComparisonModal from './ProductComparisonModal'; + const Skeleton = dynamic(() => import('@chakra-ui/react').then((mod) => mod.Skeleton) ); @@ -30,8 +36,7 @@ type Props = { }; const Information = ({ product }: Props) => { - const { selectedVariant, setSelectedVariant, setSla, sla } = - useProductDetail(); + const { selectedVariant, setSelectedVariant, setSla, sla } = useProductDetail(); const [inputValue, setInputValue] = useState<string | null>( selectedVariant?.code + ' - ' + selectedVariant?.attributes[0] @@ -48,6 +53,9 @@ const Information = ({ product }: Props) => { const [warranties, setWarranties] = useState<Record<string, string>>({}); const [loadingWarranty, setLoadingWarranty] = useState(false); + // State untuk Modal Compare + const [isCompareOpen, setIsCompareOpen] = useState(false); + useEffect(() => { const fetchWarrantyDirectly = async () => { if (!product?.variants || product.variants.length === 0) return; @@ -166,9 +174,7 @@ const Information = ({ product }: Props) => { } _selected={ option.id === selectedVariant?.id - ? { - bg: 'gray.300', - } + ? { bg: 'gray.300' } : undefined } textTransform='capitalize' @@ -183,13 +189,7 @@ const Information = ({ product }: Props) => { ? ' - ' + option?.attributes[0] : '')} </div> - <div - className={ - option?.price?.discount_percentage - ? 'flex gap-x-4 items-center justify-between' - : '' - } - > + <div className={option?.price?.discount_percentage ? 'flex gap-x-4 items-center justify-between' : ''}> {option?.price?.discount_percentage > 0 && ( <> <div className='badge-solid-red text-xs'> @@ -209,6 +209,38 @@ const Information = ({ product }: Props) => { ))} </AutoCompleteList> </AutoComplete> + + {/* === 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" + onClick={() => setIsCompareOpen(true)} + > + <div className="flex items-center gap-3"> + <div className="bg-red-50 p-2 rounded-full group-hover:bg-red-100 transition-colors"> + <ImageNext src="/images/logo-bandingkan.svg" width={15} height={15} alt="bandingkan" /> + </div> + <div className="flex flex-col"> + <span className="text-sm font-bold text-gray-800">Bandingkan Produk</span> + <span className="text-xs text-gray-500">Coba bandingkan dengan produk lainnya</span> + </div> + </div> + <div className="flex items-center gap-2"> + <span className="bg-red-600 text-white text-[10px] font-bold px-2 py-0.5 rounded-full">Baru</span> + <Icon as={ChevronDownIcon} className="w-4 h-4 text-gray-400 transform -rotate-90" /> + </div> + </div> + </MobileView> + + {/* Render Modal (Logic open/close ada di dalam component) */} + {isCompareOpen && ( + <ProductComparisonModal + isOpen={isCompareOpen} + onClose={() => setIsCompareOpen(false)} + mainProduct={product} + selectedVariant={selectedVariant} + /> + )} </div> {/* ITEM CODE */} @@ -230,7 +262,7 @@ const Information = ({ product }: Props) => { )} > {product?.manufacture.logo ? ( - <Image + <ImageNext height={50} width={100} src={product.manufacture.logo} @@ -265,45 +297,33 @@ const Information = ({ product }: Props) => { </div> </div> - {/* === DETAIL INFORMASI PRODUK (Horizontal Minimalis) === */} + {/* === DETAIL INFORMASI PRODUK === */} <div className="mt-6 border-t pt-4"> <h2 className="hidden md:block font-bold text-gray-800 text-sm mb-4"> Detail Informasi Produk </h2> - {/* Mobile: 3 Kolom, Spacing Kecil. Desktop: 3 Kolom, Spacing Besar */} <SimpleGrid columns={{ base: 3, md: 3 }} spacing={{ base: 2, md: 10 }}> - - {/* 1. Distributor Resmi */} <Flex direction={{ base: 'column', md: 'row' }} align="center" textAlign={{ base: 'center', md: 'left' }} gap={{ base: 2, md: 3 }} > - <img - src="/images/produk_asli.svg" - alt="Distributor Resmi" - className="w-8 h-8 md:w-10 md:h-10 shrink-0" - /> + <img src="/images/produk_asli.svg" alt="Distributor Resmi" className="w-8 h-8 md:w-10 md:h-10 shrink-0" /> <Box> <Text fontSize={{ base: "10px", md: "11px" }} color="gray.500" lineHeight="short" mb="1px">Distributor Resmi</Text> <Text fontSize={{ base: "10px", md: "12px" }} fontWeight="bold" color="gray.800" lineHeight="1.2">Jaminan Produk Asli</Text> </Box> </Flex> - {/* 2. Estimasi Penyiapan */} <Flex direction={{ base: 'column', md: 'row' }} align="center" textAlign={{ base: 'center', md: 'left' }} gap={{ base: 2, md: 3 }} > - <img - src="/images/estimasi.svg" - alt="Estimasi Penyiapan" - className="w-8 h-8 md:w-9 md:h-9 shrink-0" - /> + <img src="/images/estimasi.svg" alt="Estimasi Penyiapan" className="w-8 h-8 md:w-9 md:h-9 shrink-0" /> <Box> <Text fontSize={{ base: "10px", md: "11px" }} color="gray.500" lineHeight="short" mb="1px">Estimasi Penyiapan</Text> {isLoading ? ( @@ -316,35 +336,26 @@ const Information = ({ product }: Props) => { </Box> </Flex> - {/* 3. Garansi Produk */} <Flex direction={{ base: 'column', md: 'row' }} align="center" textAlign={{ base: 'center', md: 'left' }} gap={{ base: 2, md: 3 }} > - <img - src="/images/garansi.svg" - alt="Garansi Produk" - className="w-8 h-8 md:w-10 md:h-10 shrink-0" - /> + <img src="/images/garansi.svg" alt="Garansi Produk" className="w-8 h-8 md:w-10 md:h-10 shrink-0" /> <Box> <Text fontSize={{ base: "10px", md: "11px" }} color="gray.500" lineHeight="short" mb="1px">Garansi Produk</Text> - {loadingWarranty ? ( - <Center><Skeleton height="10px" width="50px" mt="2px" /></Center> + <Center><Skeleton height="10px" width="50px" mt="2px" /></Center> ) : ( <Text fontSize={{ base: "10px", md: "12px" }} fontWeight="bold" color="gray.800" lineHeight="1.2"> - {selectedVariant && warranties[selectedVariant.id] - ? warranties[selectedVariant.id] - : '-'} + {selectedVariant && warranties[selectedVariant.id] ? warranties[selectedVariant.id] : '-'} </Text> )} </Box> </Flex> </SimpleGrid> </div> - </div> ); }; 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> ))} diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index 54a0fb52..d63eb365 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -565,12 +565,12 @@ const ProductDetail = ({ product }: Props) => { > Spesifikasi </Tab> - <Tab + {/* <Tab _selected={{ color: 'red.600', borderColor: 'red.600', borderBottomWidth: '3px', fontWeight: 'bold', marginBottom: '-1.5px' }} color="gray.500" fontWeight="medium" fontSize="sm" px={4} py={3} > Detail Lainnya - </Tab> + </Tab> */} </TabList> <TabPanels> |
