This commit is contained in:
Your Name committed 2026-06-12 22:36:01 -04:00
1 parent decc6282a6
commit 358c6abd8e
2 files changed
+102 -10

No files matched your search

@@ -0,0 +1,95 @@
import { useState } from 'react'
import type { ProductImage } from '../types'
interface ProductImageGalleryProps {
images: ProductImage[]
fallback: string
alt?: string
badgeText?: string
}
export default function ProductImageGallery({ images, fallback, alt = 'Product Image', badgeText }: ProductImageGalleryProps) {
const [currentIndex, setCurrentIndex] = useState(0)
// 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 hasImages = sortedImages.length > 0
const currentImageUrl = hasImages ? sortedImages[currentIndex].url : fallback
const handleNext = () => {
setCurrentIndex((prev) => (prev === sortedImages.length - 1 ? 0 : prev + 1))
}
const handlePrev = () => {
setCurrentIndex((prev) => (prev === 0 ? sortedImages.length - 1 : prev - 1))
}
return (
<div className="space-y-4">
{/* Main Image */}
<div className="relative rounded-2xl overflow-hidden h-[480px] group bg-white shadow-sm border border-spa-stone/10 flex items-center justify-center">
<img
src={currentImageUrl}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
alt={alt}
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 && (
<div className="absolute top-6 left-6 bg-white/90 backdrop-blur-sm px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-widest text-spa-forest shadow-sm pointer-events-none">
{badgeText}
</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-white/80 backdrop-blur-md rounded-full flex items-center justify-center text-spa-forest opacity-0 group-hover:opacity-100 transition-opacity hover:bg-white 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-white/80 backdrop-blur-md rounded-full flex items-center justify-center text-spa-forest opacity-0 group-hover:opacity-100 transition-opacity hover:bg-white 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>
{/* Thumbnails */}
{sortedImages.length > 1 && (
<div className="flex gap-4 overflow-x-auto pb-2 scrollbar-hide">
{sortedImages.map((image, idx) => (
<button
key={image.id || idx}
onClick={() => setCurrentIndex(idx)}
className={`relative flex-shrink-0 w-24 h-24 rounded-xl overflow-hidden border-2 transition-all ${
currentIndex === idx ? 'border-spa-forest shadow-md' : 'border-transparent hover:border-spa-stone/30'
}`}
>
<img
src={image.url}
alt={`${alt} thumbnail ${idx + 1}`}
className="w-full h-full object-cover"
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>
))}
</div>
)}
</div>
)
}
+7 -10
View File
@@ -4,6 +4,7 @@ import Navbar from '../components/Navbar'
import Footer from '../components/Footer'
import ProductCard from '../components/ProductCard'
import ProductReviews from '../components/ProductReviews'
import ProductImageGallery from '../components/ProductImageGallery'
import DynamicPageRenderer from '../components/DynamicPageRenderer'
import { API_BASE, getCategoryImage, formatPrice, type Category, type Product, type PageSection } from '../types'
import { useCartStore } from '../store/cartStore'
@@ -149,18 +150,14 @@ export default function ProductDetailPage() {
{/* Product Detail */}
<div className="grid md:grid-cols-2 gap-8 md:gap-16 items-start">
{/* Image */}
<div className="relative rounded-2xl overflow-hidden h-[480px] group bg-white shadow-sm border border-spa-stone/10">
<img
src={product.images?.[0]?.url || getCategoryImage(product.category.name)}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
{/* Image Gallery */}
<div className="w-full">
<ProductImageGallery
images={product.images || []}
fallback={getCategoryImage(product.category.name)}
alt={product.scent.name}
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
badgeText={product.category.name}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent" />
<div className="absolute top-6 left-6 bg-white/90 backdrop-blur-sm px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-widest text-spa-forest shadow-sm">
{product.category.name}
</div>
</div>
{/* Info */}