summaryrefslogtreecommitdiff
path: root/src-migrate/modules/product-detail/components/ProductDetail.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src-migrate/modules/product-detail/components/ProductDetail.tsx')
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx696
1 files changed, 638 insertions, 58 deletions
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx
index 129ca8de..de205c41 100644
--- a/src-migrate/modules/product-detail/components/ProductDetail.tsx
+++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx
@@ -2,15 +2,37 @@ import style from '../styles/product-detail.module.css';
import Link from 'next/link';
import { useRouter } from 'next/router';
-import { useEffect, useRef, useState, UIEvent } from 'react';
+import { useEffect, useRef, useState, UIEvent, useMemo } from 'react';
-import { Button } from '@chakra-ui/react';
+// Import komponen Chakra UI
+import {
+ Button,
+ Tabs,
+ TabList,
+ TabPanels,
+ Tab,
+ TabPanel,
+ Table,
+ Tbody,
+ Tr,
+ Td,
+ Th,
+ Thead,
+ Box,
+ Spinner,
+ Center,
+ Text,
+ Stack,
+} from '@chakra-ui/react';
+
+// Import Icons
import {
- AlertCircle,
AlertTriangle,
MessageCircleIcon,
Share2Icon,
+ ExternalLink,
} from 'lucide-react';
+
import { LazyLoadComponent } from 'react-lazy-load-image-component';
import useDevice from '@/core/hooks/useDevice';
@@ -28,7 +50,11 @@ 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';
+import Skeleton from 'react-loading-skeleton';
type Props = {
product: IProductDetail;
@@ -36,15 +62,66 @@ type Props = {
const RWebShare = dynamic(
() => import('react-web-share').then((m) => m.RWebShare),
- { ssr: false }
+ { ssr: false },
);
+// 1. STYLE DESKTOP (Tebal, Jelas, dengan Border/Padding)
+const cssScrollbarDesktop = {
+ '&::-webkit-scrollbar': {
+ width: '10px',
+ height: '10px',
+ },
+ '&::-webkit-scrollbar-track': {
+ background: '#f1f1f1',
+ borderRadius: '4px',
+ },
+ '&::-webkit-scrollbar-thumb': {
+ backgroundColor: '#9ca3af', // Gray-400
+ borderRadius: '6px',
+ border: '2px solid #f1f1f1', // Efek padding
+ },
+ '&::-webkit-scrollbar-thumb:hover': {
+ backgroundColor: '#6b7280',
+ },
+};
+
+// 2. STYLE MOBILE (Tipis, Minimalis, Tanpa Border)
+const cssScrollbarMobile = {
+ '&::-webkit-scrollbar': {
+ width: '3px', // Sangat tipis vertikal
+ height: '3px', // Sangat tipis horizontal
+ },
+ '&::-webkit-scrollbar-track': {
+ background: 'transparent',
+ },
+ '&::-webkit-scrollbar-thumb': {
+ backgroundColor: '#cbd5e1', // Gray-300
+ borderRadius: '3px',
+ },
+};
+
const SELF_HOST = process.env.NEXT_PUBLIC_SELF_HOST;
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[]>([]);
+ const [upsellIds, setUpsellIds] = useState<number[]>([]);
+ const [relatedIds, setRelatedIds] = useState<number[]>([]);
+ const [descriptionMap, setDescriptionMap] = useState<Record<string, string>>(
+ {},
+ );
+
+ const [loadingSpecs, setLoadingSpecs] = useState(false);
+
+ // 2. STATE MODAL COMPARE (Baru)
+ const [isCompareOpen, setCompareOpen] = useState(false);
+
useEffect(() => {
try {
setAuth(getAuth() ?? null);
@@ -61,8 +138,8 @@ const ProductDetail = ({ product }: Props) => {
activeVariantId,
setIsApproval,
isApproval,
+ selectedVariant,
setSelectedVariant,
- setSla,
} = useProductDetail();
useEffect(() => {
@@ -95,15 +172,178 @@ const ProductDetail = ({ product }: Props) => {
// });
// }, [product?.id]);
+ // 1. LOGIC INISIALISASI VARIANT
useEffect(() => {
if (typeof auth === 'object') {
setIsApproval(auth?.feature?.soApproval);
}
- const selectedVariant =
+ const variantInit =
product?.variants?.find((variant) => variant.is_in_bu) ||
product?.variants?.[0];
- setSelectedVariant(selectedVariant);
- }, []);
+
+ setSelectedVariant(variantInit);
+
+ setSpecsMatrix([]);
+ setUpsellIds([]);
+ setRelatedIds([]);
+ }, [product, auth]);
+
+ // 2. LOGIC FETCH DATA
+ useEffect(() => {
+ const fetchMagentoData = async () => {
+ const allVariantIds = product.variants.map((v) => v.id);
+
+ if (allVariantIds.length === 0) return;
+
+ const mainId = allVariantIds[0];
+
+ setLoadingSpecs(true);
+
+ try {
+ const params = new URLSearchParams({
+ skus: allVariantIds.join(','),
+ main_sku: String(mainId),
+ });
+
+ const endpoint = `/api/magento-product?${params.toString()}`;
+
+ const response = await fetch(endpoint, {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+ if (!response.ok) {
+ setSpecsMatrix([]);
+ setUpsellIds([]);
+ setRelatedIds([]);
+ return;
+ }
+
+ const data = await response.json();
+
+ // 1. Specs Matrix (Processed Grouping)
+ if (data.specsMatrix && Array.isArray(data.specsMatrix)) {
+ // const filteredMatrix = data.specsMatrix.filter((item: any) => {
+ // const code = item.code || '';
+ // return !code.includes('z_brand');
+ // });
+ const processed = processMatrixData(data.specsMatrix);
+ setSpecsMatrix(processed);
+ // const processed = processMatrixData(filteredMatrix);
+ // setSpecsMatrix(processed);
+ } else {
+ setSpecsMatrix([]);
+ }
+
+ if (data.descriptions) {
+ setDescriptionMap(data.descriptions);
+ }
+
+ // 2. Upsell & Related
+ if (data.upsell_ids && Array.isArray(data.upsell_ids))
+ setUpsellIds(data.upsell_ids);
+ else setUpsellIds([]);
+
+ if (data.related_ids && Array.isArray(data.related_ids))
+ setRelatedIds(data.related_ids);
+ else setRelatedIds([]);
+ } catch (error) {
+ console.error('Gagal mengambil data Magento:', error);
+ setSpecsMatrix([]);
+ } finally {
+ setLoadingSpecs(false);
+ }
+ };
+
+ fetchMagentoData();
+ }, [product.id]);
+
+ // HELPER 1: GROUPING DATA BY LABEL
+ const processMatrixData = (rawMatrix: any[]) => {
+ const groups: any = {};
+ const result: any[] = [];
+
+ rawMatrix.forEach((item) => {
+ if (item.label && item.label.includes(' : ')) {
+ const parts = item.label.split(' : ');
+ const groupName = parts[0].trim();
+ const childLabel = parts.slice(1).join(' : ').trim();
+
+ if (!groups[groupName]) {
+ groups[groupName] = {
+ type: 'group',
+ label: groupName,
+ children: [],
+ };
+ result.push(groups[groupName]);
+ }
+
+ groups[groupName].children.push({
+ ...item,
+ label: childLabel,
+ });
+ } else {
+ result.push({ ...item, type: 'single' });
+ }
+ });
+
+ return result;
+ };
+
+ // HELPER 2: RENDER SPEC VALUE
+ const renderSpecValue = (val: any) => {
+ if (!val) return '-';
+ const strVal = String(val).trim();
+
+ const isUrl =
+ !strVal.includes(' ') &&
+ (strVal.startsWith('http') || strVal.startsWith('www.'));
+ if (isUrl) {
+ const href = strVal.startsWith('http') ? strVal : `https://${strVal}`;
+ return (
+ <a
+ href={href}
+ target='_blank'
+ rel='noopener noreferrer'
+ className='text-red-600 hover:underline inline-flex items-center gap-1'
+ >
+ <ExternalLink size={14} /> Link
+ </a>
+ );
+ }
+
+ if (strVal.includes('<') && strVal.includes('>')) {
+ return (
+ <Box
+ className='prose prose-sm text-gray-700'
+ sx={{
+ '& ul, & ol': {
+ paddingLeft: '1.2rem',
+ margin: 0,
+ textAlign: 'left',
+ },
+ '& li': {
+ fontWeight: 'normal',
+ marginBottom: '4px',
+ textAlign: 'left',
+ },
+ '& strong': {
+ display: 'block',
+ marginBottom: '2px',
+ fontWeight: 'bold',
+ },
+ '& p': {
+ margin: 0,
+ textAlign: 'left',
+ },
+ }}
+ dangerouslySetInnerHTML={{ __html: strVal }}
+ />
+ );
+ }
+
+ return strVal;
+ };
const allImages = (() => {
const arr: string[] = [];
@@ -153,8 +393,62 @@ const ProductDetail = ({ product }: Props) => {
setMainImage(allImages[i] || '');
};
+ const sortedVariants = useMemo(() => {
+ if (!product?.variants) return [];
+
+ return [...product.variants].sort((a, b) => {
+ const labelA =
+ a.attributes && a.attributes.length > 0
+ ? a.attributes.join(' - ')
+ : a.code || '';
+
+ const labelB =
+ b.attributes && b.attributes.length > 0
+ ? b.attributes.join(' - ')
+ : b.code || '';
+
+ const getNumber = (str: string) => {
+ const match = String(str).match(/(\d+(\.\d+)?)/);
+ return match ? parseFloat(match[0]) : null;
+ };
+
+ const numA = getNumber(labelA);
+ const numB = getNumber(labelB);
+
+ if (numA !== null && numB !== null && numA !== numB) {
+ return numA - numB;
+ }
+
+ return String(labelA).localeCompare(String(labelB), undefined, {
+ numeric: true,
+ sensitivity: 'base',
+ });
+ });
+ }, [product.variants]);
+
+ const activeMagentoDesc = selectedVariant?.id
+ ? descriptionMap[String(selectedVariant.id)]
+ : '';
+ const finalDescription =
+ activeMagentoDesc ||
+ product.description ||
+ 'Deskripsi produk tidak tersedia.';
+ const cleanDescription =
+ finalDescription === '<p><br></p>'
+ ? 'Deskripsi produk tidak tersedia.'
+ : finalDescription;
+
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'>
@@ -188,7 +482,7 @@ const ProductDetail = ({ product }: Props) => {
<div className='md:flex md:flex-wrap'>
{/* ===== Kolom kiri: gambar ===== */}
<div className='md:w-4/12'>
- {/* === MOBILE: Slider swipeable, tanpa thumbnail carousel === */}
+ {/* ... Image Slider ... */}
{isMobile ? (
<div className='relative'>
<div
@@ -207,7 +501,6 @@ const ProductDetail = ({ product }: Props) => {
key={i}
className='w-full flex-shrink-0 snap-center flex justify-center items-center'
>
- {/* gambar diperkecil */}
<img
src={img}
alt={`Gambar ${i + 1}`}
@@ -229,17 +522,13 @@ const ProductDetail = ({ product }: Props) => {
</div>
)}
</div>
-
- {/* Dots indicator */}
{allImages.length > 1 && (
<div className='absolute bottom-2 left-0 right-0 flex justify-center gap-2'>
{allImages.map((_, i) => (
<button
key={i}
aria-label={`Ke slide ${i + 1}`}
- className={`w-2 h-2 rounded-full ${
- currentIdx === i ? 'bg-gray-800' : 'bg-gray-300'
- }`}
+ className={`w-2 h-2 rounded-full ${currentIdx === i ? 'bg-gray-800' : 'bg-gray-300'}`}
onClick={() => scrollToIndex(i)}
/>
))}
@@ -248,21 +537,14 @@ const ProductDetail = ({ product }: Props) => {
</div>
) : (
<>
- {/* === DESKTOP: Tetap seperti sebelumnya === */}
<ProductImage product={{ ...product, image: mainImage }} />
-
- {/* Carousel horizontal (thumbnail) – hanya desktop */}
{allImages.length > 0 && (
<div className='mt-4 overflow-x-auto'>
<div className='flex space-x-3 pb-3'>
{allImages.map((img, index) => (
<div
key={index}
- className={`flex-shrink-0 w-16 h-16 cursor-pointer border-2 rounded-md transition-colors ${
- mainImage === img
- ? 'border-red-500 ring-2 ring-red-200'
- : 'border-gray-200 hover:border-gray-300'
- }`}
+ className={`flex-shrink-0 w-16 h-16 cursor-pointer border-2 rounded-md transition-colors ${mainImage === img ? 'border-red-500 ring-2 ring-red-200' : 'border-gray-200 hover:border-gray-300'}`}
onClick={() => setMainImage(img)}
>
<img
@@ -283,7 +565,6 @@ const ProductDetail = ({ product }: Props) => {
</>
)}
</div>
- {/* <<=== TUTUP kolom kiri */}
{/* ===== Kolom kanan: info ===== */}
{isDesktop && (
@@ -294,9 +575,9 @@ const ProductDetail = ({ product }: Props) => {
size={18}
className='text-red-600 shrink-0 mx-2'
/>
- <div className='text-red-600 font-normal text-h-sm p-2'>
+ <h1 className='text-red-600 font-normal text-h-sm'>
Maaf untuk saat ini Produk yang anda cari tidak tersedia
- </div>
+ </h1>
</div>
)}
<div className='h-6 md:h-0' />
@@ -314,27 +595,27 @@ const ProductDetail = ({ product }: Props) => {
size={18}
className='text-red-600 shrink-0 mx-2'
/>
- <div className='text-red-600 font-normal text-h-sm p-2'>
+ <h1 className='text-red-600 font-normal text-h-sm'>
Maaf untuk saat ini Produk yang anda cari tidak tersedia
- </div>
+ </h1>
</div>
)}
<h1 className={style['title']}>{product.name}</h1>
<div className='h-3 md:h-0' />
<Information product={product} />
- <div className='h-6' />
+ <div className='h-2' />
</div>
)}
</div>
<div className='h-full'>
{isMobile && (
- <div className='px-4 pt-6'>
+ <div className='px-4 pt-2'>
<PriceAction product={product} />
</div>
)}
- <div className='h-4 md:h-10' />
+ <div className='h-2 md:h-10' />
{!!activeVariantId && !isApproval && (
<ProductPromoSection
product={product}
@@ -344,29 +625,337 @@ const ProductDetail = ({ product }: Props) => {
<div className='h-0 md:h-6' />
+ {/* === SECTION TABS: DESKRIPSI & SPESIFIKASI === */}
<div className={style['section-card']}>
- <h2 className={style['heading']}>Informasi Produk</h2>
- <div className='h-4' />
- <div className='overflow-x-auto'>
- <div
- className={style['description']}
- dangerouslySetInnerHTML={{
- __html:
- !product.description ||
- product.description == '<p><br></p>'
- ? 'Belum ada deskripsi'
- : product.description,
- }}
- />
- </div>
+ <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}
+ >
+ Detail Lainnya
+ </Tab> */}
+ </TabList>
+
+ <TabPanels>
+ {/* DESKRIPSI */}
+ <TabPanel px={0} py={6}>
+ <div className='overflow-x-auto text-sm text-gray-700'>
+ {loadingSpecs ? (
+ <Stack spacing={4}>
+ <Skeleton height='20px' width='100%' />
+ <Skeleton height='20px' width='90%' />
+ <Skeleton height='20px' width='95%' />
+ <Skeleton height='20px' width='70%' />
+ </Stack>
+ ) : (
+ <Box
+ className={style['description']}
+ sx={{
+ 'ul, ol': {
+ marginTop: '0.5em !important',
+ marginBottom: '1em !important',
+ marginLeft: '0 !important',
+ listStylePosition: 'outside !important',
+ paddingLeft: '1.5em !important',
+ },
+ ul: { listStyleType: 'disc !important' },
+ ol: { listStyleType: 'decimal !important' },
+ li: {
+ marginBottom: '0.4em !important',
+ paddingLeft: '0.3em !important',
+ lineHeight: '1.6 !important',
+ },
+ }}
+ dangerouslySetInnerHTML={{ __html: cleanDescription }}
+ />
+ )}
+ </div>
+ </TabPanel>
+
+ {/* SPESIFIKASI */}
+ <TabPanel px={0} py={2}>
+ <Box
+ border='1px solid'
+ borderColor='gray.200'
+ borderRadius='sm'
+ overflowX='auto'
+ overflowY='auto'
+ maxHeight='500px'
+ css={isMobile ? cssScrollbarMobile : cssScrollbarDesktop}
+ >
+ {loadingSpecs ? (
+ <Center py={6}>
+ <Spinner color='red.500' />
+ </Center>
+ ) : specsMatrix.length > 0 ? (
+ (() => {
+ const variantCount = sortedVariants.length;
+ const isSingleVariant = variantCount === 1;
+
+ // === LOGIC 1: SINGLE VARIANT (VERTICAL TABLE) ===
+ if (isSingleVariant) {
+ const singleVariantId = sortedVariants[0].id;
+ // Flatten data untuk list vertical
+ const rows: any[] = [];
+ specsMatrix.forEach((row) => {
+ if (row.type === 'group') {
+ row.children.forEach((child: any) =>
+ rows.push(child),
+ );
+ } else {
+ rows.push(row);
+ }
+ });
+
+ return (
+ <Table
+ variant='simple'
+ size={isMobile ? 'sm' : 'md'}
+ >
+ <Tbody>
+ {rows.map((row, idx) => (
+ <Tr
+ key={idx}
+ bg={idx % 2 === 0 ? 'white' : 'gray.50'}
+ >
+ {/* Kolom Label (Kiri) */}
+ <Td
+ width='40%'
+ fontWeight='bold'
+ color='gray.600'
+ borderColor='gray.200'
+ verticalAlign='top'
+ py={3}
+ >
+ {row.label}
+ </Td>
+ {/* Kolom Value (Kanan) */}
+ <Td
+ color='gray.800'
+ borderColor='gray.200'
+ verticalAlign='top'
+ py={3}
+ >
+ {renderSpecValue(
+ row.values[singleVariantId],
+ )}
+ </Td>
+ </Tr>
+ ))}
+ </Tbody>
+ </Table>
+ );
+ }
+
+ // === LOGIC 2: MULTIPLE VARIANTS (MATRIX TABLE HORIZONTAL) ===
+ const topHeaders: any[] = [];
+ const subHeaders: any[] = [];
+ const flatSpecs: any[] = [];
+
+ specsMatrix.forEach((row) => {
+ if (row.type === 'group') {
+ topHeaders.push({
+ label: row.label,
+ type: 'group',
+ colSpan: row.children.length,
+ rowSpan: 1,
+ });
+ row.children.forEach((child: any) => {
+ subHeaders.push(child);
+ flatSpecs.push(child);
+ });
+ } else {
+ topHeaders.push({
+ label: row.label,
+ type: 'single',
+ colSpan: 1,
+ rowSpan: 2,
+ });
+ flatSpecs.push(row);
+ }
+ });
+
+ return (
+ <Table
+ variant='simple'
+ size={isMobile ? 'sm' : 'md'}
+ >
+ <Thead
+ bg='red.600'
+ position='sticky'
+ top={0}
+ zIndex={3}
+ >
+ <Tr>
+ {topHeaders.map((th, idx) => (
+ <Th
+ key={`top-${idx}`}
+ position={idx === 0 ? 'sticky' : 'static'}
+ left={idx === 0 ? 0 : undefined}
+ zIndex={idx === 0 ? 4 : 3}
+ boxShadow={
+ idx === 0
+ ? '2px 0 5px -2px rgba(0,0,0,0.2)'
+ : 'none'
+ }
+ bg='red.600'
+ colSpan={th.colSpan}
+ rowSpan={th.rowSpan}
+ color='white'
+ textAlign='center'
+ fontSize={isMobile ? 'xs' : 'sm'}
+ textTransform='none'
+ fontWeight='800'
+ letterSpacing='wide'
+ verticalAlign='middle'
+ borderBottom='none'
+ px={isMobile ? 2 : 4}
+ >
+ {th.label}
+ </Th>
+ ))}
+ </Tr>
+ <Tr>
+ {subHeaders.map((sub, idx) => {
+ const isFirstHeaderGroup =
+ topHeaders[0]?.type === 'group';
+ const shouldSticky =
+ idx === 0 && isFirstHeaderGroup;
+ return (
+ <Th
+ key={`sub-${idx}`}
+ position={
+ shouldSticky ? 'sticky' : 'static'
+ }
+ left={shouldSticky ? 0 : undefined}
+ zIndex={shouldSticky ? 4 : 1}
+ boxShadow={
+ shouldSticky
+ ? '2px 0 5px -2px rgba(0,0,0,0.2)'
+ : 'none'
+ }
+ color='white'
+ textAlign='center'
+ fontSize='xs'
+ textTransform='none'
+ verticalAlign='middle'
+ whiteSpace='nowrap'
+ bg='red.600'
+ pt={1}
+ pb={1}
+ px={isMobile ? 2 : 4}
+ >
+ {sub.label}
+ </Th>
+ );
+ })}
+ </Tr>
+ </Thead>
+
+ <Tbody>
+ {sortedVariants.map((v, vIdx) => (
+ <Tr
+ key={v.id}
+ bg={vIdx % 2 === 0 ? 'white' : 'gray.50'}
+ >
+ {flatSpecs.map((spec, sIdx) => {
+ const rawValue = spec.values[v.id] || '-';
+ const isFirstCol = sIdx === 0;
+ return (
+ <Td
+ key={sIdx}
+ position={
+ isFirstCol ? 'sticky' : 'static'
+ }
+ left={isFirstCol ? 0 : undefined}
+ zIndex={isFirstCol ? 2 : 1}
+ bg={
+ vIdx % 2 === 0 ? 'white' : 'gray.50'
+ }
+ boxShadow={
+ isFirstCol
+ ? '2px 0 5px -2px rgba(0,0,0,0.1)'
+ : 'none'
+ }
+ borderColor='gray.200'
+ textAlign='center'
+ fontSize={isMobile ? 'xs' : 'sm'}
+ verticalAlign='middle'
+ px={isMobile ? 1 : 2}
+ py={3}
+ minW={isMobile ? '100px' : '120px'}
+ maxW='200px'
+ whiteSpace='normal'
+ overflowWrap='break-word'
+ fontWeight={
+ isFirstCol ? 'bold' : 'normal'
+ }
+ >
+ {renderSpecValue(rawValue)}
+ </Td>
+ );
+ })}
+ </Tr>
+ ))}
+ </Tbody>
+ </Table>
+ );
+ })()
+ ) : (
+ <Box p={4} color='gray.500' fontSize='sm'>
+ <Text>Spesifikasi teknis belum tersedia.</Text>
+ </Box>
+ )}
+ </Box>
+ </TabPanel>
+ </TabPanels>
+ </Tabs>
</div>
</div>
</div>
{isDesktop && (
<div className='md:w-3/12'>
- <PriceAction product={product} />
- <div className='flex gap-x-5 items-center justify-center'>
+ {/* 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'>
<Button
as={Link}
href={askAdminUrl}
@@ -378,15 +967,11 @@ const ProductDetail = ({ product }: Props) => {
>
Ask Admin
</Button>
-
<span>|</span>
-
<div className={hasPrice ? '' : 'opacity-40 pointer-events-none'}>
<AddToWishlist productId={product.id} />
</div>
-
<span>|</span>
-
{canShare && (
<RWebShare
data={{
@@ -411,23 +996,18 @@ const ProductDetail = ({ product }: Props) => {
<div className='h-6' />
<div className={style['heading']}>Produk Serupa</div>
-
<div className='h-4' />
-
- <SimilarSide product={product} />
+ <SimilarSide product={product} relatedIds={relatedIds} />
</div>
)}
<div className='md:w-full pt-4 md:py-10 px-4 md:px-0'>
<div className={style['heading']}>Kamu Mungkin Juga Suka</div>
-
<div className='h-6' />
-
<LazyLoadComponent>
- <SimilarBottom product={product} />
+ <SimilarBottom product={product} upsellIds={upsellIds} />
</LazyLoadComponent>
</div>
-
<div className='h-6 md:h-0' />
</div>
</>