x
This commit is contained in:
1 parent
07f04e2065
commit
5d111b2866
2 files changed
+32
-49
No files matched your search
@@ -12,71 +12,40 @@ export default function ProductImageGallery({ images, fallback, alt = 'Product I
|
|||||||
const [currentIndex, setCurrentIndex] = useState(0)
|
const [currentIndex, setCurrentIndex] = useState(0)
|
||||||
|
|
||||||
// Sort images by order if they exist, else just use the array
|
// Sort images by order if they exist, else just use the array
|
||||||
const sortedImages = images?.length ? [...images].sort((a, b) => a.order - b.order) : []
|
const sortedImages = images?.length ? [...images].sort((a, b) => (a.order || 0) - (b.order || 0)) : []
|
||||||
const hasImages = sortedImages.length > 0
|
|
||||||
|
|
||||||
const currentImageUrl = hasImages ? sortedImages[currentIndex].url : fallback
|
if (sortedImages.length === 0) return null
|
||||||
|
|
||||||
const handleNext = () => {
|
|
||||||
setCurrentIndex((prev) => (prev === sortedImages.length - 1 ? 0 : prev + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePrev = () => {
|
|
||||||
setCurrentIndex((prev) => (prev === 0 ? sortedImages.length - 1 : prev - 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// If there's only one image, don't show thumbnails
|
||||||
|
if (sortedImages.length === 1) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="relative aspect-square w-full flex-1 overflow-hidden rounded-3xl bg-pop-bg shadow-sm border border-pop-black/10 flex items-center justify-center">
|
||||||
{/* Main Image */}
|
|
||||||
<div className="relative rounded-2xl overflow-hidden h-[480px] group bg-pop-bg shadow-sm border border-pop-black/10 flex items-center justify-center">
|
|
||||||
<img
|
<img
|
||||||
src={currentImageUrl}
|
src={sortedImages[0].url}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
className="w-full h-full object-cover"
|
||||||
alt={alt}
|
alt={alt}
|
||||||
onError={(e) => { (e.target as HTMLImageElement).src = fallback }}
|
onError={(e) => { (e.target as HTMLImageElement).src = fallback }}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent pointer-events-none" />
|
|
||||||
|
|
||||||
{badgeText && (
|
{badgeText && (
|
||||||
<div className="absolute top-6 left-6 bg-pop-bg backdrop-blur-sm px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-widest text-pop-pink shadow-sm pointer-events-none">
|
<div className="absolute top-6 left-6 bg-pop-bg backdrop-blur-sm px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-widest text-pop-pink shadow-sm pointer-events-none z-10">
|
||||||
{badgeText}
|
{badgeText}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Navigation Arrows */}
|
|
||||||
{sortedImages.length > 1 && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={handlePrev}
|
|
||||||
className="absolute left-4 top-1/2 -translate-y-1/2 w-10 h-10 bg-pop-bg backdrop-blur-md rounded-full flex items-center justify-center text-pop-black opacity-0 group-hover:opacity-100 transition-opacity hover:bg-pop-bg hover:text-pop-pink hover:scale-110 shadow-sm"
|
|
||||||
aria-label="Previous image"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleNext}
|
|
||||||
className="absolute right-4 top-1/2 -translate-y-1/2 w-10 h-10 bg-pop-bg backdrop-blur-md rounded-full flex items-center justify-center text-pop-black opacity-0 group-hover:opacity-100 transition-opacity hover:bg-pop-bg hover:text-pop-pink hover:scale-110 shadow-sm"
|
|
||||||
aria-label="Next image"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col-reverse md:flex-row items-start gap-4 w-full">
|
||||||
{/* Thumbnails */}
|
{/* Thumbnails */}
|
||||||
{sortedImages.length > 1 && (
|
<div className="flex flex-row md:flex-col gap-3 overflow-x-auto md:overflow-y-auto w-full md:w-20 lg:w-24 flex-none hide-scrollbar snap-x md:snap-y snap-mandatory pb-2 md:pb-0 md:pr-2">
|
||||||
<div className="flex gap-4 overflow-x-auto pb-2 scrollbar-hide">
|
|
||||||
{sortedImages.map((image, idx) => (
|
{sortedImages.map((image, idx) => (
|
||||||
<button
|
<button
|
||||||
key={image.id || idx}
|
key={image.id || idx}
|
||||||
|
type="button"
|
||||||
onClick={() => setCurrentIndex(idx)}
|
onClick={() => setCurrentIndex(idx)}
|
||||||
className={`relative flex-shrink-0 w-16 h-16 rounded-xl overflow-hidden border-2 transition-all ${
|
className={`relative aspect-square w-20 md:w-full flex-none overflow-hidden rounded-2xl border-2 transition-all snap-center ${
|
||||||
currentIndex === idx ? 'border-pop-black shadow-md' : 'border-transparent hover:border-pop-black/30'
|
currentIndex === idx ? 'border-pop-pink shadow-md' : 'border-transparent opacity-60 hover:opacity-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -85,11 +54,25 @@ export default function ProductImageGallery({ images, fallback, alt = 'Product I
|
|||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
onError={(e) => { (e.target as HTMLImageElement).src = fallback }}
|
onError={(e) => { (e.target as HTMLImageElement).src = fallback }}
|
||||||
/>
|
/>
|
||||||
<div className={`absolute inset-0 bg-black/10 transition-opacity ${currentIndex === idx ? 'opacity-0' : 'opacity-100 group-hover:opacity-0'}`} />
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Main Image */}
|
||||||
|
<div className="relative aspect-square w-full flex-1 overflow-hidden rounded-3xl bg-pop-bg shadow-sm border border-pop-black/10 flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
key={currentIndex}
|
||||||
|
src={sortedImages[currentIndex].url}
|
||||||
|
className="w-full h-full object-cover animate-fade-in"
|
||||||
|
alt={alt}
|
||||||
|
onError={(e) => { (e.target as HTMLImageElement).src = fallback }}
|
||||||
|
/>
|
||||||
|
{badgeText && (
|
||||||
|
<div className="absolute top-6 left-6 bg-pop-bg backdrop-blur-sm px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-widest text-pop-pink shadow-sm pointer-events-none z-10">
|
||||||
|
{badgeText}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ export default function ProductDetailPage() {
|
|||||||
|
|
||||||
<Navbar categories={categories} />
|
<Navbar categories={categories} />
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto px-6 pt-24 md:pt-32 pb-24 space-y-12 md:space-y-24">
|
<div className="max-w-7xl mx-auto px-6 pt-24 md:pt-28 pb-24 space-y-8">
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
<div className="flex items-center flex-wrap gap-2 text-[10px] md:text-xs text-spa-stone/60 uppercase tracking-widest font-semibold mb-6 md:mb-0">
|
<div className="flex items-center flex-wrap gap-2 text-[10px] md:text-xs text-spa-stone/60 uppercase tracking-widest font-semibold mb-6 md:mb-0">
|
||||||
<Link to="/" className="hover:text-spa-forest transition-colors">{t('common.home')}</Link>
|
<Link to="/" className="hover:text-spa-forest transition-colors">{t('common.home')}</Link>
|
||||||
|
|||||||
Reference in new issue
Block a user