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.tsx385
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>
</>