diff options
| author | Miqdad <ahmadmiqdad27@gmail.com> | 2025-08-22 15:30:08 +0700 |
|---|---|---|
| committer | Miqdad <ahmadmiqdad27@gmail.com> | 2025-08-22 15:30:08 +0700 |
| commit | 753be9690f95c288aec2ab92269529131626254d (patch) | |
| tree | 5bf87d90732fcdab13bba6bc3cb025889aa00e67 | |
| parent | 8e2af6c1bf6d17b5409c5eb0c1e25e2da882898f (diff) | |
<Miqdad> Image slider when mobile
| -rw-r--r-- | src-migrate/modules/product-detail/components/ProductDetail.tsx | 177 |
1 files changed, 134 insertions, 43 deletions
diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index 192e1dc3..a6b4e6de 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -2,7 +2,7 @@ import style from '../styles/product-detail.module.css'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState, UIEvent } from 'react'; import { Button } from '@chakra-ui/react'; import { MessageCircleIcon, Share2Icon } from 'lucide-react'; @@ -35,6 +35,7 @@ const ProductDetail = ({ product }: Props) => { const { isDesktop, isMobile } = useDevice(); const router = useRouter(); const auth = getAuth(); + const { setAskAdminUrl, askAdminUrl, @@ -71,70 +72,168 @@ const ProductDetail = ({ product }: Props) => { product?.variants?.find((variant) => variant.is_in_bu) || product?.variants?.[0]; setSelectedVariant(selectedVariant); - // setSelectedVariant(product?.variants[0]) - }, []); + }, []); // eslint-disable-line react-hooks/exhaustive-deps // Gabungkan semua gambar produk (utama + tambahan) - const allImages = product.image_carousel ? [...product.image_carousel] : []; - - if (product.image) { - allImages.unshift(product.image); // Tambahkan gambar utama di awal array - } - console.log(product); + // Gabungkan semua gambar produk (utama + tambahan) + const allImages = (() => { + const arr: string[] = []; + if (product?.image) arr.push(product.image); // selalu masukkan utama, baik mobile maupun desktop + if ( + Array.isArray(product?.image_carousel) && + product.image_carousel.length + ) { + // hindari duplikat jika image utama juga ada di carousel + const set = new Set(arr); + for (const img of product.image_carousel) { + if (!set.has(img)) { + arr.push(img); + set.add(img); + } + } + } + return arr; + })(); const [mainImage, setMainImage] = useState(allImages[0] || ''); + useEffect(() => { + // update mainImage jika sumber gambar berubah dan mainImage tidak ada di daftar + if (!allImages.includes(mainImage)) { + setMainImage(allImages[0] || ''); + } + }, [allImages]); // eslint-disable-line react-hooks/exhaustive-deps + + // ===== Slider mobile (tanpa dependency) ===== + const sliderRef = useRef<HTMLDivElement | null>(null); + const [currentIdx, setCurrentIdx] = useState(0); + + const handleMobileScroll = (e: UIEvent<HTMLDivElement>) => { + const el = e.currentTarget; + if (!el) return; + const idx = Math.round(el.scrollLeft / el.clientWidth); + if (idx !== currentIdx) { + setCurrentIdx(idx); + setMainImage(allImages[idx] || ''); + } + }; + + const scrollToIndex = (i: number) => { + const el = sliderRef.current; + if (!el) return; + el.scrollTo({ left: i * el.clientWidth, behavior: 'smooth' }); + setCurrentIdx(i); + setMainImage(allImages[i] || ''); + }; + // ============================================ + return ( <> <div className='md:flex md:flex-wrap'> <div className='w-full mb-4 md:mb-0 px-4 md:px-0'> <Breadcrumb id={product.id} name={product.name} /> </div> + <div className='md:w-9/12 md:flex md:flex-col md:pr-4 md:pt-6'> <div className='md:flex md:flex-wrap'> + {/* ===== Kolom kiri: gambar ===== */} <div className='md:w-4/12'> - <ProductImage product={{ ...product, image: mainImage }} /> - - {/* Carousel horizontal */} - {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)} - > + {/* === MOBILE: Slider swipeable, tanpa thumbnail carousel === */} + {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', // IE/Edge lama + scrollbarWidth: 'none', // Firefox + }} + > + {allImages.length > 0 ? ( + allImages.map((img, i) => ( <img + key={i} src={img} - alt={`Thumbnail ${index + 1}`} - className='w-full h-full object-cover rounded-sm' - loading='lazy' + alt={`Gambar ${i + 1}`} + className='w-full aspect-square object-cover flex-shrink-0 snap-center' onError={(e) => { (e.target as HTMLImageElement).src = '/path/to/fallback-image.jpg'; }} /> - </div> - ))} + )) + ) : ( + <img + src={mainImage || '/path/to/fallback-image.jpg'} + alt='Gambar produk' + className='w-full aspect-square object-cover flex-shrink-0 snap-center' + /> + )} </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)} + /> + ))} + </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 = + '/path/to/fallback-image.jpg'; + }} + /> + </div> + ))} + </div> + </div> + )} + </> )} </div> + {/* <<=== TUTUP kolom kiri */} + {/* ===== Kolom kanan: info ===== */} <div className='md:w-8/12 px-4 md:pl-6'> <div className='h-6 md:h-0' /> - <h1 className={style['title']}>{product.name}</h1> - <div className='h-3 md:h-0' /> - <Information product={product} /> - <div className='h-6' /> </div> </div> @@ -154,14 +253,6 @@ const ProductDetail = ({ product }: Props) => { /> )} - {/* <div className={style['section-card']}> - <h2 className={style['heading']}> - Variant ({product.variant_total}) - </h2> - <div className='h-4' /> - <VariantList variants={product.variants} /> - </div> */} - <div className='h-0 md:h-6' /> <div className={style['section-card']}> @@ -246,4 +337,4 @@ const ProductDetail = ({ product }: Props) => { ); }; -export default ProductDetail; +export default ProductDetail;
\ No newline at end of file |
