summaryrefslogtreecommitdiff
path: root/src-migrate/modules/product-detail/components/ProductDetail.tsx
diff options
context:
space:
mode:
authorIT Fixcomart <it@fixcomart.co.id>2026-01-31 16:22:14 +0000
committerIT Fixcomart <it@fixcomart.co.id>2026-01-31 16:22:14 +0000
commit8c6f1b3bf6eac52041337b33e746888933e1e34a (patch)
tree0787627b48ef1b1db8e6d76066eb3400afb71123 /src-migrate/modules/product-detail/components/ProductDetail.tsx
parentec7ab4c654fc5b29b277d42ad84986f4c1220134 (diff)
parent99aa3500fc5bbb3bb24d73461639e6fc88042a85 (diff)
Merged in magento-v2.1 (pull request #472)
Magento v2.1
Diffstat (limited to 'src-migrate/modules/product-detail/components/ProductDetail.tsx')
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx717
1 files changed, 575 insertions, 142 deletions
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx
index 129ca8de..35726437 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 { Button } from '@chakra-ui/react';
+import { useEffect, useRef, useState, UIEvent, useMemo } from '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;
@@ -39,12 +65,61 @@ const RWebShare = dynamic(
{ 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 +136,8 @@ const ProductDetail = ({ product }: Props) => {
activeVariantId,
setIsApproval,
isApproval,
+ selectedVariant,
setSelectedVariant,
- setSla,
} = useProductDetail();
useEffect(() => {
@@ -95,15 +170,183 @@ 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[] = [];
@@ -146,15 +389,59 @@ const ProductDetail = ({ product }: Props) => {
};
const scrollToIndex = (i: number) => {
- const el = sliderRef.current;
- if (!el) return;
+ const el = sliderRef.current;
+ if (!el) return;
el.scrollTo({ left: i * el.clientWidth, behavior: 'smooth' });
setCurrentIdx(i);
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,93 +475,39 @@ 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
- ref={sliderRef}
- onScroll={handleMobileScroll}
- className='flex overflow-x-auto snap-x snap-mandatory scroll-smooth no-scrollbar'
- style={{
- scrollBehavior: 'smooth',
- msOverflowStyle: 'none',
- scrollbarWidth: 'none',
- }}
- >
+ <div ref={sliderRef} onScroll={handleMobileScroll} className='flex overflow-x-auto snap-x snap-mandatory scroll-smooth no-scrollbar' style={{ scrollBehavior: 'smooth', msOverflowStyle: 'none', scrollbarWidth: 'none' }}>
{allImages.length > 0 ? (
allImages.map((img, i) => (
- <div
- key={i}
- className='w-full flex-shrink-0 snap-center flex justify-center items-center'
- >
- {/* gambar diperkecil */}
- <img
- src={img}
- alt={`Gambar ${i + 1}`}
- className='w-[85%] aspect-square object-contain'
- onError={(e) => {
- (e.target as HTMLImageElement).src =
- '/images/noimage.jpeg';
- }}
- />
+ <div key={i} className='w-full flex-shrink-0 snap-center flex justify-center items-center'>
+ <img src={img} alt={`Gambar ${i + 1}`} className='w-[85%] aspect-square object-contain' onError={(e) => { (e.target as HTMLImageElement).src = '/images/noimage.jpeg'; }} />
</div>
))
) : (
<div className='w-full flex-shrink-0 snap-center flex justify-center items-center'>
- <img
- src={mainImage || '/images/noimage.jpeg'}
- alt='Gambar produk'
- className='w-[85%] aspect-square object-contain'
- />
+ <img src={mainImage || '/images/noimage.jpeg'} alt='Gambar produk' className='w-[85%] aspect-square object-contain' />
</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'
- }`}
- onClick={() => scrollToIndex(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'}`} onClick={() => scrollToIndex(i)} />
))}
</div>
)}
</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'
- }`}
- onClick={() => setMainImage(img)}
- >
- <img
- src={img}
- alt={`Thumbnail ${index + 1}`}
- className='w-full h-full object-cover rounded-sm'
- loading='lazy'
- onError={(e) => {
- (e.target as HTMLImageElement).src =
- '/images/noimage.jpeg';
- }}
- />
+ <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'}`} onClick={() => setMainImage(img)}>
+ <img src={img} alt={`Thumbnail ${index + 1}`} className='w-full h-full object-cover rounded-sm' loading='lazy' onError={(e) => { (e.target as HTMLImageElement).src = '/images/noimage.jpeg'; }} />
</div>
))}
</div>
@@ -283,7 +516,6 @@ const ProductDetail = ({ product }: Props) => {
</>
)}
</div>
- {/* <<=== TUTUP kolom kiri */}
{/* ===== Kolom kanan: info ===== */}
{isDesktop && (
@@ -294,9 +526,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 +546,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,94 +576,295 @@ 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'>
- <Button
- as={Link}
- href={askAdminUrl}
- variant='link'
- target='_blank'
- colorScheme='gray'
- leftIcon={<MessageCircleIcon size={18} />}
- isDisabled={!hasPrice}
- >
- Ask Admin
- </Button>
-
+ {/* 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} 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>
-
+ <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>
- )}
+ {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' />
-
- <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>
</>
);
};
-export default ProductDetail;
+export default ProductDetail; \ No newline at end of file