summaryrefslogtreecommitdiff
path: root/src-migrate
diff options
context:
space:
mode:
Diffstat (limited to 'src-migrate')
-rw-r--r--src-migrate/modules/product-detail/components/AddToQuotation.tsx91
-rw-r--r--src-migrate/modules/product-detail/components/PriceAction.tsx38
-rw-r--r--src-migrate/modules/product-detail/components/ProductComparisonModal.tsx492
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx75
-rw-r--r--src-migrate/types/productVariant.ts3
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;