diff options
Diffstat (limited to 'src-migrate/modules')
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> ) |
