diff options
Diffstat (limited to 'src-migrate')
5 files changed, 629 insertions, 70 deletions
diff --git a/src-migrate/modules/product-detail/components/AddToQuotation.tsx b/src-migrate/modules/product-detail/components/AddToQuotation.tsx index 3e811330..e26e271f 100644 --- a/src-migrate/modules/product-detail/components/AddToQuotation.tsx +++ b/src-migrate/modules/product-detail/components/AddToQuotation.tsx @@ -1,7 +1,7 @@ import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; import style from '../styles/price-action.module.css'; import { Button, Link, useToast } from '@chakra-ui/react'; -import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; +// import { ScaleIcon } from '@heroicons/react/24/outline'; // Tidak perlu lagi import product from 'next-seo/lib/jsonld/product'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; @@ -17,12 +17,15 @@ import { createSlug } from '~/libs/slug'; import formatCurrency from '~/libs/formatCurrency'; import { useProductDetail } from '../stores/useProductDetail'; import useDevice from '@/core/hooks/useDevice'; +import DesktopView from '@/core/components/views/DesktopView'; +import MobileView from '@/core/components/views/MobileView'; type Props = { variantId: number | null; quantity?: number; source?: 'buy' | 'add_to_cart'; products: IProductDetail; + onCompare?: () => void; }; type Status = 'idle' | 'loading' | 'success'; @@ -32,6 +35,7 @@ const AddToQuotation = ({ quantity = 1, source = 'add_to_cart', products, + onCompare }: Props) => { const auth = getAuth(); const router = useRouter(); @@ -106,37 +110,60 @@ const AddToQuotation = ({ }, 3000); }, [status]); - const btnConfig = { - add_to_cart: { - colorScheme: 'red', - variant: 'outline', - text: 'Keranjang', - }, - buy: { - colorScheme: 'red', - variant: 'solid', - text: 'Beli', - }, - }; - return ( - <div className='w-full'> - <Button - onClick={handleButton} - color={'red'} - colorScheme='white' - className='w-full border-2 p-2 gap-1 hover:bg-slate-100 flex items-center' - isDisabled={!hasPrice} - > - <ImageNext - src={isDesktop ? '/images/doc_red.svg' : '/images/doc.svg'} - alt='penawaran instan' - className='' - width={25} - height={25} - /> - {isDesktop ? 'Penawaran Harga Instan' : ''} - </Button> + <div className='w-full flex flex-col gap-3'> + + {/* 3. TAMPILAN DESKTOP: GRID 2 KOLOM (Bandingkan & Penawaran) */} + <DesktopView> + <div className="grid grid-cols-2 gap-3 w-full"> + {/* Tombol Kiri: Bandingkan */} + <Button + onClick={onCompare} + variant="outline" + colorScheme="gray" + className="w-full border border-gray-300 p-2 gap-2 flex items-center justify-center text-gray-600 hover:text-red-600 hover:border-red-600 transition-all font-normal text-sm" + > + {/* UPDATE ICON DISINI */} + <ImageNext src="/images/logo-bandingkan.svg" width={15} height={15} alt="bandingkan" /> + Bandingkan + </Button> + + {/* Tombol Kanan: Penawaran (Link WA) */} + <Button + as={Link} + href={askAdminUrl} + target='_blank' + variant="outline" + colorScheme="gray" + className="w-full border border-gray-300 p-2 gap-2 flex items-center justify-center text-gray-600 hover:text-red-600 hover:border-red-600 transition-all font-normal text-sm" + _hover={{ textDecoration: 'none' }} + onClick={handleButton} + > + <ImageNext src="/images/doc_red.svg" width={20} height={20} alt="penawaran" /> + Penawaran + </Button> + </div> + </DesktopView> + + {/* TAMPILAN MOBILE */} + <MobileView> + <Button + onClick={handleButton} + color={'red'} + colorScheme='white' + className='w-full border-2 p-2 gap-1 hover:bg-slate-100 flex items-center' + isDisabled={!hasPrice} + > + <ImageNext + src='/images/doc.svg' + alt='penawaran instan' + className='' + width={25} + height={25} + /> + </Button> + </MobileView> + <BottomPopup className='!container' title='Berhasil Ditambahkan' @@ -243,4 +270,4 @@ const AddToQuotation = ({ ); }; -export default AddToQuotation; +export default AddToQuotation;
\ No newline at end of file diff --git a/src-migrate/modules/product-detail/components/PriceAction.tsx b/src-migrate/modules/product-detail/components/PriceAction.tsx index d73ab5f6..ee8009ef 100644 --- a/src-migrate/modules/product-detail/components/PriceAction.tsx +++ b/src-migrate/modules/product-detail/components/PriceAction.tsx @@ -1,5 +1,4 @@ import style from '../styles/price-action.module.css'; - import Image from 'next/image'; import Link from 'next/link'; import { useEffect, useState } from 'react'; @@ -15,14 +14,17 @@ import { Button, Skeleton } from '@chakra-ui/react'; import DesktopView from '@/core/components/views/DesktopView'; import MobileView from '@/core/components/views/MobileView'; +// 1. Tambahkan onCompare (Optional) di sini type Props = { product: IProductDetail; + onCompare?: () => void; }; const PPN: number = process.env.NEXT_PUBLIC_PPN ? parseFloat(process.env.NEXT_PUBLIC_PPN) : 0; -const PriceAction = ({ product }: Props) => { + +const PriceAction = ({ product, onCompare }: Props) => { const { activePrice, setActive, @@ -146,19 +148,6 @@ const PriceAction = ({ product }: Props) => { </> )} - {/* {!!activePrice && activePrice.price === 0 && ( - <span> - Hubungi kami untuk dapatkan harga terbaik,{' '} - <Link - href={askAdminUrl} - target='_blank' - className={style['contact-us']} - > - klik disini - </Link> - </span> - )} */} - <DesktopView> <div className='h-4' /> <div className='flex gap-x-5 items-center'> @@ -227,9 +216,6 @@ const PriceAction = ({ product }: Props) => { )} </div> </div> - {/* <span className='text-[12px] text-red-500 italic'> - * {qtyPickUp} barang bisa di pickup - </span> */} </DesktopView> {/* ===== MOBILE: grid kiri-kanan, kanan hanya qty ===== */} @@ -263,12 +249,6 @@ const PriceAction = ({ product }: Props) => { </Link> )} </div> - - {/* {qtyPickUp > 0 && ( - <div className='text-[12px] mt-1 text-red-500 italic'> - * {qtyPickUp} barang bisa di pickup - </div> - )} */} </div> {/* Kanan: hanya qty, rata kanan */} @@ -295,9 +275,9 @@ const PriceAction = ({ product }: Props) => { value={quantityInput} onChange={(e) => setQuantityInput(e.target.value)} className='h-11 md:h-12 w-16 md:w-20 text-center text-lg md:text-xl outline-none border-x - [appearance:textfield] - [&::-webkit-outer-spin-button]:appearance-none - [&::-webkit-inner-spin-button]:appearance-none' + [appearance:textfield] + [&::-webkit-outer-spin-button]:appearance-none + [&::-webkit-inner-spin-button]:appearance-none' disabled={!hasPrice} /> @@ -335,11 +315,13 @@ const PriceAction = ({ product }: Props) => { )} </div> <div className='mt-4'> + {/* 2. TERUSKAN onCompare KE SINI */} <AddToQuotation source='buy' products={product} variantId={activeVariantId} quantity={Number(quantityInput)} + onCompare={onCompare} /> </div> </DesktopView> @@ -376,4 +358,4 @@ const PriceAction = ({ product }: Props) => { ); }; -export default PriceAction; +export default PriceAction;
\ No newline at end of file diff --git a/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx new file mode 100644 index 00000000..97f1d101 --- /dev/null +++ b/src-migrate/modules/product-detail/components/ProductComparisonModal.tsx @@ -0,0 +1,492 @@ +import React, { useEffect, useState, useRef } from 'react'; // Tambah useRef +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalCloseButton, + Button, + Text, + Box, + Badge, + Grid, + GridItem, + Image, + Input, + InputGroup, + InputLeftElement, + VStack, + HStack, + IconButton, + Flex, + Icon, + Spinner, + List, + ListItem, + useToast, + Select, + useOutsideClick // Tambah import ini +} from '@chakra-ui/react'; + +import { Search, Trash2 } from 'lucide-react'; + +// --- HELPER FORMATTING --- +const formatPrice = (price: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(price); +}; + +const renderSpecValue = (val: any) => { + if (!val || val === '-') return '-'; + return String(val).replace(/<[^>]*>?/gm, ''); +}; + +type Props = { + isOpen: boolean; + onClose: () => void; + 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[]>([]); + const [isLoadingMatrix, setIsLoadingMatrix] = useState(false); + + // Search State + const [activeSearchSlot, setActiveSearchSlot] = useState<number | null>(null); + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState<any[]>([]); + const [isSearching, setIsSearching] = useState(false); + + // --- REF & OUTSIDE CLICK --- + const searchWrapperRef = useRef<HTMLDivElement>(null); // Ref untuk mendeteksi klik + + 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 + } + }, + }); + + // =========================================================================== + // 1. LOGIC UTAMA: ISI SLOT 1 + // =========================================================================== + useEffect(() => { + if (isOpen && mainProduct) { + let activeItem = selectedVariant; + + if (!activeItem && mainProduct.variants && mainProduct.variants.length > 0) { + activeItem = mainProduct.variants[0]; + } + if (!activeItem) { + 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, + 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 + }); + } + + const productSlot1 = { + id: targetId, + sku: targetId, + realCode: displayCode, + name: mainProduct.name, + 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; + }); + } + }, [isOpen, mainProduct, selectedVariant]); + + // =========================================================================== + // 2. FETCH SPECS + // =========================================================================== + useEffect(() => { + const validProducts = products.filter(p => p !== null); + 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; + + 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); + } + }; + + fetchSpecs(); + }, [products, isOpen]); + + // =========================================================================== + // 3. SEARCH LOGIC + // =========================================================================== + useEffect(() => { + const delayDebounceFn = setTimeout(async () => { + if (searchQuery.length > 0 && searchQuery.length < 3) { + setSearchResults([]); + return; + } + + if (activeSearchSlot === null) return; + + setIsSearching(true); + try { + const attrSetId = selectedVariant?.attribute_set_id || mainProduct?.attribute_set_id; + const queryParam = searchQuery === '' ? '*' : searchQuery; + + const params = new URLSearchParams({ + source: 'compare', + q: queryParam, + limit: '20', + fq: attrSetId ? `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); + } + }, 500); + + 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, + realCode: selectedVar.code, + price: selectedVar.price, + image: selectedVar.image + }; + setProducts(newProducts); + } + }; + + const handleAddProduct = (searchItem: any, slotIndex: number) => { + const newProducts = [...products]; + + const idToAdd = searchItem.id; + const codeToAdd = searchItem.code; + const nameToAdd = searchItem.displayName || searchItem.name; + const imageToAdd = searchItem.image; + const priceToAdd = searchItem.lowestPrice?.price || 0; + + if (newProducts.find(p => p && String(p.id) === String(idToAdd))) { + toast({ title: "Produk sudah ada", status: "warning", position: "top" }); + return; + } + + newProducts[slotIndex] = { + id: idToAdd, + sku: idToAdd, + realCode: codeToAdd, + name: nameToAdd, + price: priceToAdd, + image: imageToAdd, + variants: [{ + id: idToAdd, + code: codeToAdd, + name: nameToAdd, + price: priceToAdd, + image: imageToAdd + }] + }; + + setProducts(newProducts); + setActiveSearchSlot(null); + setSearchQuery(''); + setSearchResults([]); + }; + + const handleRemoveProduct = (index: number) => { + const newProducts = [...products]; + newProducts[index] = null; + setProducts(newProducts); + if (newProducts.every(p => p === null)) setSpecsMatrix([]); + }; + + return ( + <Modal isOpen={isOpen} onClose={onClose} size="6xl" scrollBehavior="inside"> + <ModalOverlay /> + <ModalContent height="90vh"> + <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> + </HStack> + <Text fontSize="sm" color="gray.500" fontWeight="normal" mt={1}> + Detail Spesifikasi Produk yang kamu pilih + </Text> + </ModalHeader> + <ModalCloseButton /> + + <ModalBody p={6} bg="white"> + <Grid templateColumns="200px repeat(4, 1fr)" gap={4}> + <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> + + <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> + ))} + </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> + </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"> + {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={2} borderBottom="1px solid #f0f0f0" + _hover={{ bg: 'red.50', cursor: 'pointer' }} + onClick={() => handleAddProduct(res, index)} + > + <Flex align="center" gap={2}> + <Image src={res.image || '/images/no-image-compare.svg'} boxSize="30px" objectFit="contain" onError={(e) => { (e.target as HTMLImageElement).src = '/images/no-image-compare.svg'; }} /> + <Box> + <Text fontSize="xs" fontWeight="bold" noOfLines={1}>{res.displayName || res.name}</Text> + <Text fontSize="xs" color="red.500"> + {formatPrice(res.lowestPrice?.price || 0)} + </Text> + </Box> + </Flex> + </ListItem> + ))} + </List> + ) : ( + <Box p={3} fontSize="xs" color="gray.500" textAlign="center"> + {searchQuery === '' ? 'Menampilkan rekomendasi...' : 'Tidak ditemukan.'} + </Box> + )} + </Box> + )} + </Box> + + {/* SLOT KOSONG */} + <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> + )} + </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> + </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> + ) : 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" + 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> + )) + ) : ( + <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> + </ModalContent> + </Modal> + ); +}; + +export default ProductComparisonModal;
\ No newline at end of file diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index 05b84260..54a0fb52 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -49,6 +49,9 @@ import SimilarBottom from './SimilarBottom'; import SimilarSide from './SimilarSide'; import dynamic from 'next/dynamic'; +// 1. IMPORT MODAL (Baru) +import ProductComparisonModal from './ProductComparisonModal'; + import { gtagProductDetail } from '@/core/utils/googleTag'; type Props = { @@ -101,6 +104,8 @@ const ProductDetail = ({ product }: Props) => { const { isDesktop, isMobile } = useDevice(); const router = useRouter(); const [auth, setAuth] = useState<any>(null); + + console.log('Render ProductDetail for product ID:', product); // State Data dari Magento const [specsMatrix, setSpecsMatrix] = useState<any[]>([]); @@ -110,6 +115,9 @@ const ProductDetail = ({ product }: Props) => { const [loadingSpecs, setLoadingSpecs] = useState(false); + // 2. STATE MODAL COMPARE (Baru) + const [isCompareOpen, setCompareOpen] = useState(false); + useEffect(() => { try { setAuth(getAuth() ?? null); @@ -398,6 +406,15 @@ const ProductDetail = ({ product }: Props) => { return ( <> + {/* 3. MODAL POPUP DIRENDER DISINI */} + {/* Render di luar layout utama agar tidak tertutup elemen lain */} + <ProductComparisonModal + isOpen={isCompareOpen} + onClose={() => setCompareOpen(false)} + mainProduct={product} + selectedVariant={selectedVariant} + /> + <div className='relative'> {isDesktop && !hasPrice && ( <div className='absolute inset-0 z-[20] flex items-center justify-center pointer-events-none select-none'> @@ -478,8 +495,13 @@ const ProductDetail = ({ product }: Props) => { <div className='md:w-8/12 px-4 md:pl-6'> {!hasPrice && ( <div className='bg-red-50 p-2 py-1.5 rounded-lg border border-red-500 flex gap-1 items-center '> - <AlertTriangle size={18} className='text-red-600 shrink-0 mx-2' /> - <h1 className='text-red-600 font-normal text-h-sm'>Maaf untuk saat ini Produk yang anda cari tidak tersedia</h1> + <AlertTriangle + size={18} + className='text-red-600 shrink-0 mx-2' + /> + <h1 className='text-red-600 font-normal text-h-sm'> + Maaf untuk saat ini Produk yang anda cari tidak tersedia + </h1> </div> )} <div className='h-6 md:h-0' /> @@ -493,8 +515,13 @@ const ProductDetail = ({ product }: Props) => { <div className='md:w-8/12 px-4 md:pl-6 relative'> {!hasPrice && ( <div className='bg-red-50 p-2 py-1.5 border-b border-red-500 flex gap-1 items-center w-screen relative left-1/2 right-1/2 -translate-x-1/2'> - <AlertTriangle size={18} className='text-red-600 shrink-0 mx-2' /> - <h1 className='text-red-600 font-normal text-h-sm'>Maaf untuk saat ini Produk yang anda cari tidak tersedia</h1> + <AlertTriangle + size={18} + className='text-red-600 shrink-0 mx-2' + /> + <h1 className='text-red-600 font-normal text-h-sm'> + Maaf untuk saat ini Produk yang anda cari tidak tersedia + </h1> </div> )} <h1 className={style['title']}>{product.name}</h1> @@ -514,7 +541,10 @@ const ProductDetail = ({ product }: Props) => { <div className='h-2 md:h-10' /> {!!activeVariantId && !isApproval && ( - <ProductPromoSection product={product} productId={activeVariantId} /> + <ProductPromoSection + product={product} + productId={activeVariantId} + /> )} <div className='h-0 md:h-6' /> @@ -523,8 +553,24 @@ const ProductDetail = ({ product }: Props) => { <div className={style['section-card']}> <Tabs variant="unstyled"> <TabList borderBottom="1px solid" borderColor="gray.200"> - <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}>Deskripsi</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}>Spesifikasi</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} + > + Deskripsi + </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} + > + Spesifikasi + </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> </TabList> <TabPanels> @@ -729,13 +775,22 @@ const ProductDetail = ({ product }: Props) => { </div> </div> - {/* ... (Bagian Sidebar & Bottom SAMA) ... */} {isDesktop && ( <div className='md:w-3/12'> - <PriceAction product={product} /> + {/* 4. INTEGRASI: PASSING HANDLER MODAL KE PRICE ACTION */} + <PriceAction + product={product} + onCompare={() => setCompareOpen(true)} + /> + <div className='flex gap-x-5 items-center justify-center py-4'> - {/* ... Buttons ... */} + <Button as={Link} href={askAdminUrl} variant='link' target='_blank' colorScheme='gray' leftIcon={<MessageCircleIcon size={18} />} isDisabled={!hasPrice}>Ask Admin</Button> + <span>|</span> + <div className={hasPrice ? '' : 'opacity-40 pointer-events-none'}><AddToWishlist productId={product.id} /></div> + <span>|</span> + {canShare && (<RWebShare data={{ text: 'Check out this product', title: `${product.name} - Indoteknik.com`, url: (process.env.NEXT_PUBLIC_SELF_HOST || '') + (router?.asPath || '/'), }}><Button variant='link' colorScheme='gray' leftIcon={<Share2Icon size={18} />} isDisabled={!hasPrice}>Share</Button></RWebShare>)} </div> + <div className='h-6' /> <div className={style['heading']}>Produk Serupa</div> <div className='h-4' /> diff --git a/src-migrate/types/productVariant.ts b/src-migrate/types/productVariant.ts index 5144e7c1..31cedf8c 100644 --- a/src-migrate/types/productVariant.ts +++ b/src-migrate/types/productVariant.ts @@ -4,6 +4,9 @@ export interface IProductVariantDetail { code: string; name: string; weight: number; + attribute_set_id: number; + attribute_set_name: string; + search_keywords: string; is_in_bu: boolean; is_flashsale: { remaining_time: number; |
