diff options
| author | Mqdd <ahmadmiqdad27@gmail.com> | 2026-01-31 23:48:37 +0700 |
|---|---|---|
| committer | Mqdd <ahmadmiqdad27@gmail.com> | 2026-01-31 23:48:37 +0700 |
| commit | dc14a5fd2c97d50b19b043a44d61786d98fda48b (patch) | |
| tree | 585d7fff4f656b510baa8e8e2d8c5ca9df9aaa81 /src-migrate/modules/product-detail/components/ProductDetail.tsx | |
| parent | 8c6f1b3bf6eac52041337b33e746888933e1e34a (diff) | |
<Miqdad> remove console log
Diffstat (limited to 'src-migrate/modules/product-detail/components/ProductDetail.tsx')
| -rw-r--r-- | src-migrate/modules/product-detail/components/ProductDetail.tsx | 741 |
1 files changed, 444 insertions, 297 deletions
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index 35726437..de205c41 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -5,24 +5,24 @@ import { useRouter } from 'next/router'; import { useEffect, useRef, useState, UIEvent, useMemo } from 'react'; // Import komponen Chakra UI -import { - Button, - Tabs, - TabList, - TabPanels, - Tab, - TabPanel, - Table, - Tbody, - Tr, +import { + Button, + Tabs, + TabList, + TabPanels, + Tab, + TabPanel, + Table, + Tbody, + Tr, Td, - Th, + Th, Thead, Box, Spinner, Center, Text, - Stack + Stack, } from '@chakra-ui/react'; // Import Icons @@ -30,7 +30,7 @@ import { AlertTriangle, MessageCircleIcon, Share2Icon, - ExternalLink + ExternalLink, } from 'lucide-react'; import { LazyLoadComponent } from 'react-lazy-load-image-component'; @@ -62,7 +62,7 @@ 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) @@ -88,8 +88,8 @@ const cssScrollbarDesktop = { // 2. STYLE MOBILE (Tipis, Minimalis, Tanpa Border) const cssScrollbarMobile = { '&::-webkit-scrollbar': { - width: '3px', // Sangat tipis vertikal - height: '3px', // Sangat tipis horizontal + width: '3px', // Sangat tipis vertikal + height: '3px', // Sangat tipis horizontal }, '&::-webkit-scrollbar-track': { background: 'transparent', @@ -107,14 +107,16 @@ const ProductDetail = ({ product }: Props) => { const router = useRouter(); const [auth, setAuth] = useState<any>(null); - console.log('Render ProductDetail for product ID:', product); - + // console.log('Render ProductDetail for product ID:', product); + // State Data dari Magento - const [specsMatrix, setSpecsMatrix] = useState<any[]>([]); + const [specsMatrix, setSpecsMatrix] = useState<any[]>([]); const [upsellIds, setUpsellIds] = useState<number[]>([]); - const [relatedIds, setRelatedIds] = useState<number[]>([]); - const [descriptionMap, setDescriptionMap] = useState<Record<string, string>>({}); - + const [relatedIds, setRelatedIds] = useState<number[]>([]); + const [descriptionMap, setDescriptionMap] = useState<Record<string, string>>( + {}, + ); + const [loadingSpecs, setLoadingSpecs] = useState(false); // 2. STATE MODAL COMPARE (Baru) @@ -136,7 +138,7 @@ const ProductDetail = ({ product }: Props) => { activeVariantId, setIsApproval, isApproval, - selectedVariant, + selectedVariant, setSelectedVariant, } = useProductDetail(); @@ -178,75 +180,75 @@ const ProductDetail = ({ product }: Props) => { const variantInit = product?.variants?.find((variant) => variant.is_in_bu) || product?.variants?.[0]; - + setSelectedVariant(variantInit); setSpecsMatrix([]); setUpsellIds([]); setRelatedIds([]); - - }, [product, auth]); + }, [product, auth]); // 2. LOGIC FETCH DATA useEffect(() => { const fetchMagentoData = async () => { - const allVariantIds = product.variants.map(v => v.id); - + 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) + 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' } + method: 'GET', + headers: { 'Content-Type': 'application/json' }, }); if (!response.ok) { - setSpecsMatrix([]); - setUpsellIds([]); - setRelatedIds([]); - return; + 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); + // 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([]); + setSpecsMatrix([]); } - if (data.descriptions){ + if (data.descriptions) { setDescriptionMap(data.descriptions); } // 2. Upsell & Related - if (data.upsell_ids && Array.isArray(data.upsell_ids)) setUpsellIds(data.upsell_ids); + 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); + if (data.related_ids && Array.isArray(data.related_ids)) + setRelatedIds(data.related_ids); else setRelatedIds([]); - } catch (error) { - console.error("Gagal mengambil data Magento:", error); + console.error('Gagal mengambil data Magento:', error); setSpecsMatrix([]); } finally { setLoadingSpecs(false); @@ -254,60 +256,56 @@ const ProductDetail = ({ product }: Props) => { }; fetchMagentoData(); - - }, [product.id]); + }, [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' }); - } - }); + 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]); + } - return result; - }; + 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.') - ); + 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" + <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> @@ -315,39 +313,38 @@ const ProductDetail = ({ product }: Props) => { } 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 ( + <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[] = []; if (product?.image) arr.push(product.image); @@ -389,8 +386,8 @@ 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] || ''); @@ -398,15 +395,17 @@ const ProductDetail = ({ product }: Props) => { 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 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+)?)/); @@ -420,23 +419,31 @@ const ProductDetail = ({ product }: Props) => { return numA - numB; } - return String(labelA).localeCompare(String(labelB), undefined, { - numeric: true, - sensitivity: 'base' + 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; + 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} + <ProductComparisonModal + isOpen={isCompareOpen} onClose={() => setCompareOpen(false)} mainProduct={product} selectedVariant={selectedVariant} @@ -478,23 +485,52 @@ const ProductDetail = ({ product }: Props) => { {/* ... 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'> - <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> {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> )} @@ -506,8 +542,21 @@ const ProductDetail = ({ product }: Props) => { <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> @@ -578,17 +627,37 @@ const ProductDetail = ({ product }: Props) => { {/* === SECTION TABS: DESKRIPSI & SPESIFIKASI === */} <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} + <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} + <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> @@ -603,53 +672,54 @@ const ProductDetail = ({ product }: Props) => { <TabPanels> {/* DESKRIPSI */} <TabPanel px={0} py={6}> - <div className='overflow-x-auto text-sm text-gray-700'> + <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%" /> + <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']} + <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' - } + '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 }} + 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" + <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> + <Center py={6}> + <Spinner color='red.500' /> + </Center> ) : specsMatrix.length > 0 ? ( (() => { const variantCount = sortedVariants.length; @@ -657,47 +727,57 @@ const ProductDetail = ({ product }: Props) => { // === 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> - ); + 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) === @@ -705,51 +785,63 @@ const ProductDetail = ({ product }: Props) => { const subHeaders: any[] = []; const flatSpecs: any[] = []; - specsMatrix.forEach(row => { + 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); - }); + 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); + 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}> + <Table + variant='simple' + size={isMobile ? 'sm' : 'md'} + > + <Thead + bg='red.600' + position='sticky' + top={0} + zIndex={3} + > <Tr> {topHeaders.map((th, idx) => ( - <Th + <Th key={`top-${idx}`} - position={idx === 0 ? "sticky" : "static"} + 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" + 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" + color='white' + textAlign='center' + fontSize={isMobile ? 'xs' : 'sm'} + textTransform='none' + fontWeight='800' + letterSpacing='wide' + verticalAlign='middle' + borderBottom='none' px={isMobile ? 2 : 4} > {th.label} @@ -758,23 +850,32 @@ const ProductDetail = ({ product }: Props) => { </Tr> <Tr> {subHeaders.map((sub, idx) => { - const isFirstHeaderGroup = topHeaders[0]?.type === 'group'; - const shouldSticky = idx === 0 && isFirstHeaderGroup; - return ( - <Th + const isFirstHeaderGroup = + topHeaders[0]?.type === 'group'; + const shouldSticky = + idx === 0 && isFirstHeaderGroup; + return ( + <Th key={`sub-${idx}`} - position={shouldSticky ? "sticky" : "static"} + 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} + 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} @@ -786,29 +887,42 @@ const ProductDetail = ({ product }: Props) => { <Tbody> {sortedVariants.map((v, vIdx) => ( - <Tr key={v.id} bg={vIdx % 2 === 0 ? 'white' : 'gray.50'}> + <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"} + <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"} + 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> @@ -821,7 +935,9 @@ const ProductDetail = ({ product }: Props) => { ); })() ) : ( - <Box p={4} color="gray.500" fontSize="sm"><Text>Spesifikasi teknis belum tersedia.</Text></Box> + <Box p={4} color='gray.500' fontSize='sm'> + <Text>Spesifikasi teknis belum tersedia.</Text> + </Box> )} </Box> </TabPanel> @@ -834,17 +950,48 @@ const ProductDetail = ({ product }: Props) => { {isDesktop && ( <div className='md:w-3/12'> {/* 4. INTEGRASI: PASSING HANDLER MODAL KE PRICE ACTION */} - <PriceAction - product={product} - onCompare={() => setCompareOpen(true)} + <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> + <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' /> @@ -867,4 +1014,4 @@ const ProductDetail = ({ product }: Props) => { ); }; -export default ProductDetail;
\ No newline at end of file +export default ProductDetail; |
