summaryrefslogtreecommitdiff
path: root/src-migrate/modules
diff options
context:
space:
mode:
Diffstat (limited to 'src-migrate/modules')
-rw-r--r--src-migrate/modules/product-detail/components/Information.tsx114
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx444
-rw-r--r--src-migrate/modules/product-detail/components/SimilarBottom.tsx55
-rw-r--r--src-migrate/modules/product-detail/components/SimilarSide.tsx74
4 files changed, 491 insertions, 196 deletions
diff --git a/src-migrate/modules/product-detail/components/Information.tsx b/src-migrate/modules/product-detail/components/Information.tsx
index a7a58cbc..b7d3401e 100644
--- a/src-migrate/modules/product-detail/components/Information.tsx
+++ b/src-migrate/modules/product-detail/components/Information.tsx
@@ -11,12 +11,11 @@ import Link from 'next/link';
import { useEffect, useRef, useState } from 'react';
import currencyFormat from '@/core/utils/currencyFormat';
-import { InputGroup, InputRightElement, Spinner } from '@chakra-ui/react';
+import { InputGroup, InputRightElement, SimpleGrid, Flex, Text, Box } from '@chakra-ui/react';
import { ChevronDownIcon } from '@heroicons/react/24/outline';
import Image from 'next/image';
import { formatToShortText } from '~/libs/formatNumber';
import { createSlug } from '~/libs/slug';
-import { getVariantSLA } from '~/services/productVariant';
import { IProductDetail } from '~/types/product';
import { useProductDetail } from '../stores/useProductDetail';
import useVariant from '../hook/useVariant';
@@ -30,7 +29,7 @@ type Props = {
};
const Information = ({ product }: Props) => {
- const { selectedVariant, setSelectedVariant, setSla, setActive, sla } =
+ const { selectedVariant, setSelectedVariant, setSla, sla } =
useProductDetail();
const [inputValue, setInputValue] = useState<string | null>(
@@ -45,15 +44,6 @@ const Information = ({ product }: Props) => {
const variantId = selectedVariant?.id;
const { slaVariant, isLoading } = useVariant({ variantId });
- // let variantOptions = product?.variants;
-
- // const querySLA = useQuery<IProductVariantSLA>({
- // queryKey: ['variant-sla', selectedVariant?.id],
- // queryFn: () => getVariantSLA(selectedVariant?.id),
- // enabled: !!selectedVariant?.id,
- // });
- // const sla = querySLA?.data;
-
useEffect(() => {
if (selectedVariant) {
setInputValue(
@@ -66,14 +56,13 @@ const Information = ({ product }: Props) => {
}, [selectedVariant]);
useEffect(() => {
- if (isLoading){
+ if (isLoading) {
setSla(null);
}
if (slaVariant) {
setSla(slaVariant);
}
- }, [slaVariant, isLoading]);
-
+ }, [slaVariant, isLoading, setSla]);
const handleOnChange = (vals: any) => {
setDisableFilter(true);
@@ -98,6 +87,15 @@ const Information = ({ product }: Props) => {
setInputValue(e.target.value);
};
+ // STYLE CUSTOM UNTUK BARIS (Item Code, dll)
+ const rowStyle = {
+ backgroundColor: '#ffffff',
+ fontSize: '13px',
+ borderBottom: '1px dashed #e2e8f0',
+ padding: '8px 0',
+ marginBottom: '0px'
+ };
+
return (
<div className={style['wrapper']}>
<div className='realtive mb-5'>
@@ -183,12 +181,15 @@ const Information = ({ product }: Props) => {
</AutoComplete>
</div>
- <div className={style['row']}>
- <div className={style['label']}>Item Code</div>
+ {/* ITEM CODE */}
+ <div className={style['row']} style={rowStyle}>
+ <div className={style['label']} style={{ color: '#6b7280' }}>Item Code</div>
<div className={style['value']}>{selectedVariant?.code}</div>
</div>
- <div className={style['row']}>
- <div className={style['label']}>Manufacture</div>
+
+ {/* MANUFACTURE */}
+ <div className={style['row']} style={rowStyle}>
+ <div className={style['label']} style={{ color: '#6b7280' }}>Manufacture</div>
<div className={style['value']}>
{!!product.manufacture.name ? (
<Link
@@ -217,29 +218,78 @@ const Information = ({ product }: Props) => {
)}
</div>
</div>
- <div className={style['row']}>
- <div className={style['label']}>Berat Barang</div>
+
+ {/* BERAT BARANG */}
+ <div className={style['row']} style={rowStyle}>
+ <div className={style['label']} style={{ color: '#6b7280' }}>Berat Barang</div>
<div className={style['value']}>
{selectedVariant?.weight > 0 ? `${selectedVariant?.weight} Kg` : '-'}
</div>
</div>
- <div className={style['row']}>
- <div className={style['label']}>Terjual</div>
+
+ {/* TERJUAL */}
+ <div className={style['row']} style={{ ...rowStyle, borderBottom: 'none' }}>
+ <div className={style['label']} style={{ color: '#6b7280' }}>Terjual</div>
<div className={style['value']}>
{product.qty_sold > 0 ? formatToShortText(product.qty_sold) : '-'}
</div>
</div>
- <div className={style['row']}>
- <div className={style['label']}>Persiapan Barang</div>
- {isLoading && (
- <div className={style['value']}>
- <Skeleton height={5} width={100} />
- </div>
- )}
- {!isLoading && <div className={style['value']}>{sla?.sla_date}</div>}
+
+ {/* === DETAIL INFORMASI PRODUK (Updated Layout) === */}
+ <div className="mt-6 border-t pt-4">
+ <h2 className="font-bold text-gray-800 text-sm mb-4">Detail Informasi Produk</h2>
+
+ {/* Perubahan: Spacing diperbesar menjadi 10 agar estimasi bergeser ke kanan */}
+ <SimpleGrid columns={{ base: 1, md: 3 }} spacing={10}>
+ {/* 1. Distributor Resmi */}
+ <Flex align="center" className="gap-3">
+ <img
+ src="/images/produk_asli.svg"
+ alt="Distributor Resmi"
+ className="w-10 h-10 shrink-0"
+ />
+ <Box>
+ <Text fontSize="11px" color="gray.500" lineHeight="short" mb="1px">Distributor Resmi</Text>
+ <Text fontSize="12px" fontWeight="bold" color="gray.800" lineHeight="short">Jaminan Produk Asli</Text>
+ </Box>
+ </Flex>
+
+ {/* 2. Estimasi Penyiapan */}
+ <Flex align="center" className="gap-3">
+ <img
+ src="/images/estimasi.svg"
+ alt="Estimasi Penyiapan"
+ className="w-9 h-9 shrink-0"
+ />
+ <Box>
+ <Text fontSize="11px" color="gray.500" lineHeight="short" mb="1px">Estimasi Penyiapan</Text>
+ {isLoading ? (
+ <Skeleton height="12px" width="60px" mt="2px" />
+ ) : (
+ <Text fontSize="12px" fontWeight="bold" color="gray.800" lineHeight="short">
+ {sla?.sla_date || '3 - 7 Hari'}
+ </Text>
+ )}
+ </Box>
+ </Flex>
+
+ {/* 3. Garansi Produk */}
+ <Flex align="center" className="gap-3">
+ <img
+ src="/images/garansi.svg"
+ alt="Garansi Produk"
+ className="w-10 h-10 shrink-0"
+ />
+ <Box>
+ <Text fontSize="11px" color="gray.500" lineHeight="short" mb="1px">Garansi Produk</Text>
+ <Text fontSize="12px" fontWeight="bold" color="gray.800" lineHeight="short">24 Bulan</Text>
+ </Box>
+ </Flex>
+ </SimpleGrid>
</div>
+
</div>
);
};
-export default Information;
+export default Information; \ No newline at end of file
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx
index 085bbb1c..fab5ecf3 100644
--- a/src-migrate/modules/product-detail/components/ProductDetail.tsx
+++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx
@@ -4,13 +4,34 @@ import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useRef, useState, UIEvent } 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
+} 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';
@@ -45,6 +66,14 @@ const ProductDetail = ({ product }: Props) => {
const { isDesktop, isMobile } = useDevice();
const router = useRouter();
const [auth, setAuth] = useState<any>(null);
+
+ // State Data dari Magento
+ const [specsMatrix, setSpecsMatrix] = useState<any[]>([]);
+ const [upsellIds, setUpsellIds] = useState<number[]>([]);
+ const [relatedIds, setRelatedIds] = useState<number[]>([]);
+
+ const [loadingSpecs, setLoadingSpecs] = useState(false);
+
useEffect(() => {
try {
setAuth(getAuth() ?? null);
@@ -61,8 +90,8 @@ const ProductDetail = ({ product }: Props) => {
activeVariantId,
setIsApproval,
isApproval,
+ selectedVariant,
setSelectedVariant,
- setSla,
} = useProductDetail();
useEffect(() => {
@@ -83,15 +112,164 @@ const ProductDetail = ({ product }: Props) => {
setAskAdminUrl(createdAskUrl);
}, [router.asPath, product.manufacture.name, product.name, setAskAdminUrl]);
+ // =========================================================================
+ // 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 processed = processMatrixData(data.specsMatrix);
+ setSpecsMatrix(processed);
+ } else {
+ setSpecsMatrix([]);
+ }
+
+ // 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 (Separator ':')
+ // =========================================================================
+ 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 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>
+ );
+ }
+
+ // HTML
+ if (strVal.includes('<') && strVal.includes('>')) {
+ return (
+ <div
+ className="prose prose-sm text-gray-700"
+ dangerouslySetInnerHTML={{ __html: strVal }}
+ />
+ );
+ }
+
+ // Teks Biasa
+ return strVal;
+ };
+
const allImages = (() => {
const arr: string[] = [];
@@ -136,7 +314,8 @@ const ProductDetail = ({ product }: Props) => {
const scrollToIndex = (i: number) => {
const el = sliderRef.current;
if (!el) return;
- el.scrollTo({ left: i * el.clientWidth, behavior: 'smooth' });
+ const elRef = sliderRef.current;
+ elRef.scrollTo({ left: i * elRef.clientWidth, behavior: 'smooth' });
setCurrentIdx(i);
setMainImage(allImages[i] || '');
};
@@ -176,93 +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, 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>
@@ -271,7 +396,6 @@ const ProductDetail = ({ product }: Props) => {
</>
)}
</div>
- {/* <<=== TUTUP kolom kiri */}
{/* ===== Kolom kanan: info ===== */}
{isDesktop && (
@@ -324,102 +448,146 @@ 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: 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'>
+ <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 (LOGIKA GROUPING + RATA TENGAH) */}
+ <TabPanel px={0} py={2}>
+ <Box border="1px solid" borderColor="gray.200" borderRadius="sm" overflowX="auto">
+ {loadingSpecs ? (
+ <Center py={6}><Spinner color='red.500' /></Center>
+ ) : 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>
+ </TabPanel>
+
+ {/* DETAIL LAINNYA */}
+ <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' />
-
- <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
diff --git a/src-migrate/modules/product-detail/components/SimilarBottom.tsx b/src-migrate/modules/product-detail/components/SimilarBottom.tsx
index 40d4dd82..d3957f4b 100644
--- a/src-migrate/modules/product-detail/components/SimilarBottom.tsx
+++ b/src-migrate/modules/product-detail/components/SimilarBottom.tsx
@@ -1,23 +1,58 @@
import { Skeleton } from '@chakra-ui/react'
-import useProductSimilar from '~/modules/product-similar/hooks/useProductSimilar'
+import { useQuery } from 'react-query'
import ProductSlider from '~/modules/product-slider'
+import { getProductSimilar, getProductsByIds } from '~/services/product'
import { IProductDetail } from '~/types/product'
type Props = {
- product: IProductDetail
+ product: IProductDetail;
+ upsellIds?: number[];
}
-const SimilarBottom = ({ product }: Props) => {
- const productSimilar = useProductSimilar({
- name: product.name,
- except: { productId: product.id }
- })
+const SimilarBottom = ({ product, upsellIds = [] }: Props) => {
+
+ const hasUpsell = upsellIds.length > 0;
- const products = productSimilar.data?.products || []
+ // Query 1: Upsell
+ const upsellQuery = useQuery({
+ queryKey: ['product-upsell', upsellIds],
+ queryFn: () => getProductsByIds({ ids: upsellIds }),
+ enabled: hasUpsell,
+ staleTime: 1000 * 60 * 5,
+ });
+
+ // Query 2: Similar Biasa
+ const similarQuery = useQuery({
+ queryKey: ['product-similar', product.name],
+ queryFn: () => getProductSimilar({
+ name: product.name,
+ except: { productId: product.id }
+ }),
+ enabled: !hasUpsell,
+ staleTime: 1000 * 60 * 5,
+ });
+
+ let products = [];
+ let isLoading = false;
+
+ // ==========================================
+ // PERBAIKAN DI SINI
+ // ==========================================
+ if (hasUpsell) {
+ // Salah: products = upsellQuery.data || [];
+ // Benar: Ambil properti .products di dalamnya
+ products = (upsellQuery.data as any)?.products || [];
+ isLoading = upsellQuery.isLoading;
+ } else {
+ products = similarQuery.data?.products || [];
+ isLoading = similarQuery.isLoading;
+ }
+
+ if (!isLoading && products.length === 0) return null;
return (
<Skeleton
- isLoaded={!productSimilar.isLoading}
+ isLoaded={!isLoading}
rounded='lg'
className='h-[350px]'
>
@@ -26,4 +61,4 @@ const SimilarBottom = ({ product }: Props) => {
);
}
-export default SimilarBottom \ No newline at end of file
+export default SimilarBottom; \ No newline at end of file
diff --git a/src-migrate/modules/product-detail/components/SimilarSide.tsx b/src-migrate/modules/product-detail/components/SimilarSide.tsx
index d70a314d..51d9eff7 100644
--- a/src-migrate/modules/product-detail/components/SimilarSide.tsx
+++ b/src-migrate/modules/product-detail/components/SimilarSide.tsx
@@ -1,33 +1,75 @@
import { Skeleton } from '@chakra-ui/react'
+import { useQuery } from 'react-query'
import ProductCard from '~/modules/product-card'
-import useProductSimilar from '~/modules/product-similar/hooks/useProductSimilar'
-import { IProductDetail } from '~/types/product'
+// Import service
+import { getProductSimilar, getProductsByIds } from '~/services/product'
+// TAMBAHKAN 'IProduct' DISINI
+import { IProduct, IProductDetail } from '~/types/product'
type Props = {
product: IProductDetail
+ relatedIds?: number[]
}
-const SimilarSide = ({ product }: Props) => {
- const productSimilar = useProductSimilar({
- name: product.name,
- except: { productId: product.id, manufactureId: product.manufacture.id },
- })
+const SimilarSide = ({ product, relatedIds = [] }: Props) => {
+
+ const hasRelated = relatedIds.length > 0;
- const products = productSimilar.data?.products || []
+ // 1. Fetch Related by ID
+ const relatedQuery = useQuery({
+ queryKey: ['product-related', relatedIds],
+ queryFn: () => getProductsByIds({ ids: relatedIds }),
+ enabled: hasRelated,
+ staleTime: 1000 * 60 * 5,
+ });
+
+ // 2. Fetch Similar Biasa
+ const similarQuery = useQuery({
+ queryKey: ['product-similar-side', product.name],
+ queryFn: () => getProductSimilar({
+ name: product.name,
+ except: {
+ productId: product.id,
+ manufactureId: product.manufacture?.id
+ }
+ }),
+ enabled: !hasRelated,
+ staleTime: 1000 * 60 * 5,
+ });
+
+ // ============================================================
+ // PERBAIKAN: Definisikan tipe array secara eksplisit (IProduct[])
+ // ============================================================
+ let products: IProduct[] = [];
+ let isLoading = false;
+
+ if (hasRelated) {
+ // Cast ke any dulu jika tipe return service belum sempurna terdeteksi, lalu ambil products
+ // Atau jika getProductsByIds me-return { products: IProduct[] }, ambil .products
+ // Sesuai kode service terakhir, getProductsByIds me-return GetProductSimilarRes yg punya .products
+ products = (relatedQuery.data as any)?.products || [];
+ isLoading = relatedQuery.isLoading;
+ } else {
+ products = similarQuery.data?.products || [];
+ isLoading = similarQuery.isLoading;
+ }
+
+ if (!isLoading && products.length === 0) return null;
return (
<Skeleton
- isLoaded={!productSimilar.isLoading}
- className="h-[500px] overflow-auto grid grid-cols-1 gap-y-4 divide-y divide-gray-300 border border-gray-300 rounded-lg"
+ isLoaded={!isLoading}
+ className="h-[500px] overflow-auto grid grid-cols-1 gap-y-4 divide-y divide-gray-300 border border-gray-300 rounded-lg p-2"
rounded='lg'
>
- {products.map((product) => (
- <ProductCard
- key={product.id}
- product={product}
- layout='horizontal'
- />
+ {products.map((item) => (
+ <div key={item.id} className="pt-2 first:pt-0">
+ <ProductCard
+ product={item}
+ layout='horizontal'
+ />
+ </div>
))}
</Skeleton>
)