x
This commit is contained in:
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>
|
||||
)
|
||||
}
|
||||
@@ -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 */}
|
||||
|
||||
Reference in new issue
Block a user