diff options
Diffstat (limited to 'src-migrate/modules/product-detail/components/ProductDetail.tsx')
| -rw-r--r-- | src-migrate/modules/product-detail/components/ProductDetail.tsx | 385 |
1 files changed, 193 insertions, 192 deletions
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index cfe73628..3beb75b4 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -16,6 +16,8 @@ import { Tbody, Tr, Td, + Th, + Thead, Box, Spinner, Center, @@ -27,6 +29,7 @@ import { AlertTriangle, MessageCircleIcon, Share2Icon, + ExternalLink } from 'lucide-react'; import { LazyLoadComponent } from 'react-lazy-load-image-component'; @@ -65,12 +68,11 @@ const ProductDetail = ({ product }: Props) => { const [auth, setAuth] = useState<any>(null); // State Data dari Magento - const [specs, setSpecs] = useState<{ code: string; label: string; value: string }[]>([]); + const [specsMatrix, setSpecsMatrix] = useState<any[]>([]); const [upsellIds, setUpsellIds] = useState<number[]>([]); const [relatedIds, setRelatedIds] = useState<number[]>([]); const [loadingSpecs, setLoadingSpecs] = useState(false); - const [errorSpecs, setErrorSpecs] = useState(false); useEffect(() => { try { @@ -111,7 +113,7 @@ const ProductDetail = ({ product }: Props) => { }, [router.asPath, product.manufacture.name, product.name, setAskAdminUrl]); // ========================================================================= - // LOGIC INISIALISASI VARIANT (RESET SAAT NAVIGASI) + // 1. LOGIC INISIALISASI VARIANT // ========================================================================= useEffect(() => { if (typeof auth === 'object') { @@ -123,30 +125,32 @@ const ProductDetail = ({ product }: Props) => { setSelectedVariant(variantInit); - // Reset data Magento - setSpecs([]); + setSpecsMatrix([]); setUpsellIds([]); setRelatedIds([]); }, [product, auth]); // ========================================================================= - // LOGIC FETCH: SPECS, UPSELLS, RELATED + // 2. LOGIC FETCH DATA // ========================================================================= useEffect(() => { const fetchMagentoData = async () => { - // Validasi kepemilikan varian - if (!selectedVariant?.id) return; - const isVariantOwner = product.variants.some(v => Number(v.id) === Number(selectedVariant.id)); - if (!isVariantOwner) return; + const allVariantIds = product.variants.map(v => v.id); + + if (allVariantIds.length === 0) return; - const idToFetch = selectedVariant.id; + const mainId = allVariantIds[0]; setLoadingSpecs(true); - setErrorSpecs(false); try { - const endpoint = `/api/magento-product?sku=${encodeURIComponent(String(idToFetch))}`; + 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', @@ -154,7 +158,7 @@ const ProductDetail = ({ product }: Props) => { }); if (!response.ok) { - setSpecs([]); + setSpecsMatrix([]); setUpsellIds([]); setRelatedIds([]); return; @@ -162,67 +166,108 @@ const ProductDetail = ({ product }: Props) => { const data = await response.json(); - // Double Check - if (Number(idToFetch) !== Number(selectedVariant.id)) return; - - // 1. Specs - if (data.specs && Array.isArray(data.specs)) { - setSpecs(data.specs); + // 1. Specs Matrix (Processed Grouping) + if (data.specsMatrix && Array.isArray(data.specsMatrix)) { + const processed = processMatrixData(data.specsMatrix); + setSpecsMatrix(processed); } else { - setSpecs([]); + setSpecsMatrix([]); } - // 2. Upsell - if (data.upsell_ids && Array.isArray(data.upsell_ids)) { - setUpsellIds(data.upsell_ids); - } else { - setUpsellIds([]); - } + // 2. Upsell & Related + if (data.upsell_ids && Array.isArray(data.upsell_ids)) setUpsellIds(data.upsell_ids); + else setUpsellIds([]); - // 3. Related - if (data.related_ids && Array.isArray(data.related_ids)) { - setRelatedIds(data.related_ids); - } else { - setRelatedIds([]); - } + if (data.related_ids && Array.isArray(data.related_ids)) setRelatedIds(data.related_ids); + else setRelatedIds([]); } catch (error) { console.error("Gagal mengambil data Magento:", error); - setErrorSpecs(true); - setSpecs([]); - setUpsellIds([]); - setRelatedIds([]); + setSpecsMatrix([]); } finally { setLoadingSpecs(false); } }; fetchMagentoData(); - }, [selectedVariant, product]); + }, [product.id]); // ========================================================================= - // HELPER: RENDER SPEC VALUE (SIMPLE - NO LINK DETECT) + // HELPER 1: GROUPING DATA BY LABEL (Separator ':') // ========================================================================= - const renderSpecValue = (item: { code: string; label: string; value: string }) => { - const val = item.value; + const processMatrixData = (rawMatrix: any[]) => { + const groups: any = {}; + const result: any[] = []; + + rawMatrix.forEach(item => { + // Cek Label: "Group Name : Sub Label" + 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 // Override label jadi pendek + }); + + } else { + result.push({ ...item, type: 'single' }); + } + }); + + return result; + }; + + + // ========================================================================= + // HELPER 2: RENDER SPEC VALUE + // ========================================================================= + const renderSpecValue = (val: any) => { if (!val) return '-'; - - const cleanVal = val.trim(); + const strVal = String(val).trim(); + + // URL Link + 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> + ); + } - // 1. JIKA HTML (Legacy Data) - // Deteksi tag HTML sederhana - if (cleanVal.includes('<') && cleanVal.includes('>')) { + // HTML + if (strVal.includes('<') && strVal.includes('>')) { return ( <div className="prose prose-sm text-gray-700" - dangerouslySetInnerHTML={{ __html: cleanVal }} + dangerouslySetInnerHTML={{ __html: strVal }} /> ); } - // 2. TEKS BIASA - return cleanVal; + // Teks Biasa + return strVal; }; @@ -310,90 +355,39 @@ const ProductDetail = ({ product }: Props) => { <div className='md:flex md:flex-wrap'> {/* ===== Kolom kiri: gambar ===== */} <div className='md:w-4/12'> - {/* === MOBILE: Slider swipeable === */} + {/* ... 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> - - {/* 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 === */} <ProductImage product={{ ...product, image: mainImage }} /> {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> @@ -408,13 +402,8 @@ 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' /> @@ -428,13 +417,8 @@ 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> @@ -454,110 +438,128 @@ const ProductDetail = ({ product }: Props) => { <div className='h-4 md:h-10' /> {!!activeVariantId && !isApproval && ( - <ProductPromoSection - product={product} - productId={activeVariantId} - /> + <ProductPromoSection product={product} productId={activeVariantId} /> )} <div className='h-0 md:h-6' /> - {/* === SECTION TABS === */} + {/* === 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} - > - 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> + <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'> - <div - className={style['description']} - dangerouslySetInnerHTML={{ - __html: - !product.description || product.description === '<p><br></p>' - ? '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>' - : product.description, - }} - /> + <div className={style['description']} dangerouslySetInnerHTML={{ __html: !product.description || product.description === '<p><br></p>' ? '<p>Lorem ipsum dolor sit amet.</p>' : product.description, }} /> </div> </TabPanel> - {/* SPESIFIKASI */} + {/* SPESIFIKASI (LOGIKA GROUPING + RATA TENGAH) */} <TabPanel px={0} py={2}> - <Box border="1px solid" borderColor="gray.200" borderRadius="sm"> + <Box border="1px solid" borderColor="gray.200" borderRadius="sm" overflowX="auto"> {loadingSpecs ? ( <Center py={6}><Spinner color='red.500' /></Center> - ) : specs.length > 0 ? ( - <Table variant="simple" size="md"> - <Tbody> - {specs.map((item, index) => ( - <Tr key={index}> - <Td fontWeight="bold" width="30%" verticalAlign="top" borderColor="gray.200"> - {item.label} - </Td> - <Td verticalAlign="top" borderColor="gray.200"> - {renderSpecValue(item)} - </Td> - </Tr> - ))} - </Tbody> - </Table> + ) : specsMatrix.length > 0 ? ( + (() => { + const isSingleVariant = product.variants.length === 1; + const globalAlign = isSingleVariant ? "left" : "center"; + + return ( + <Table variant="simple" size="md"> + <Thead bg="red.600"> + <Tr> + <Th width={isSingleVariant ? "30%" : "20%"} borderColor="whiteAlpha.300" color="white" fontSize="sm" textTransform="none" verticalAlign="middle"> + Spesifikasi + </Th> + {product.variants.map(v => ( + <Th key={v.id} borderColor="whiteAlpha.300" color="white" textAlign={globalAlign} fontSize="sm" textTransform="none" verticalAlign="middle"> + {isSingleVariant ? 'Detail' : (v.attributes && v.attributes.length > 0 ? v.attributes.join(' - ') : v.code)} + </Th> + ))} + </Tr> + </Thead> + <Tbody> + {specsMatrix.map((row, i) => { + + // CASE 1: GROUPING (Label punya ' : ') + if (row.type === 'group') { + return ( + <Tr key={i}> + {/* Header Group Kiri */} + <Td fontWeight="bold" borderColor="gray.200" fontSize="sm" verticalAlign="middle">{row.label}</Td> + + {/* Content Group Kanan */} + {product.variants.map(v => ( + <Td key={v.id} borderColor="gray.200" textAlign={globalAlign} fontSize="sm" verticalAlign="middle"> + <div className={`inline-block text-left`}> + <div className="flex flex-col gap-0"> + {row.children.map((child: any, idx: number) => { + const rawVal = child.values[v.id]; + if (!rawVal || rawVal === '-') return null; + + return ( + <div key={idx} className="grid grid-cols-[auto_auto] gap-x-2"> + <span className="font-semibold text-gray-600 whitespace-nowrap"> + {child.label}: + </span> + <span>{renderSpecValue(rawVal)}</span> + </div> + ); + })} + </div> + </div> + </Td> + ))} + </Tr> + ); + } + + // CASE 2: SINGLE ITEM + return ( + <Tr key={i}> + <Td fontWeight="bold" borderColor="gray.200" fontSize="sm" verticalAlign="middle">{row.label}</Td> + {product.variants.map(v => { + const rawValue = row.values[v.id] || '-'; + return ( + <Td key={v.id} borderColor="gray.200" textAlign={globalAlign} fontSize="sm" verticalAlign="middle"> + {renderSpecValue(rawValue)} + </Td> + ); + })} + </Tr> + ); + })} + </Tbody> + </Table> + ); + })() ) : ( - <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> {/* DETAIL LAINNYA */} - <TabPanel px={0} py={6}> - <p className="text-gray-500 text-sm">Informasi tambahan belum tersedia.</p> - </TabPanel> - + <TabPanel px={0} py={6}><p className="text-gray-500 text-sm">Informasi tambahan belum tersedia.</p></TabPanel> </TabPanels> </Tabs> </div> </div> </div> + {/* ... (Bagian Sidebar & Bottom SAMA) ... */} {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> - <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 className='flex gap-x-5 items-center justify-center py-4'> + {/* ... Buttons ... */} </div> - <div className='h-6' /> <div className={style['heading']}>Produk Serupa</div> <div className='h-4' /> @@ -572,7 +574,6 @@ const ProductDetail = ({ product }: Props) => { <SimilarBottom product={product} upsellIds={upsellIds} /> </LazyLoadComponent> </div> - <div className='h-6 md:h-0' /> </div> </> |
