Fix product images and add carousel; add transformation carousel on mobile
Some checks failed
Build and Deploy / build (push) Has been cancelled
Some checks failed
Build and Deploy / build (push) Has been cancelled
This commit is contained in:
@@ -125,6 +125,16 @@ function BeforeAfterSlider({ result }: { result: typeof results[0] }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function BeforeAfterGallery() {
|
export default function BeforeAfterGallery() {
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
const goToPrev = () => {
|
||||||
|
setSelectedIndex(prev => prev === 0 ? results.length - 1 : prev - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToNext = () => {
|
||||||
|
setSelectedIndex(prev => prev === results.length - 1 ? 0 : prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-[#faf9f7]">
|
<section className="py-24 bg-[#faf9f7]">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
@@ -143,8 +153,8 @@ export default function BeforeAfterGallery() {
|
|||||||
</h2>
|
</h2>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Two transformations side by side */}
|
{/* Desktop: Two transformations side by side */}
|
||||||
<div className="flex gap-6 max-w-6xl mx-auto">
|
<div className="hidden md:flex gap-6 max-w-6xl mx-auto">
|
||||||
{results.map((result, index) => (
|
{results.map((result, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={result.id}
|
key={result.id}
|
||||||
@@ -159,6 +169,55 @@ export default function BeforeAfterGallery() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile: Carousel with one transformation at a time */}
|
||||||
|
<div className="md:hidden relative max-w-md mx-auto">
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
key={selectedIndex}
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<BeforeAfterSlider result={results[selectedIndex]} />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Carousel Navigation */}
|
||||||
|
<button
|
||||||
|
onClick={goToPrev}
|
||||||
|
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-2 w-10 h-10 bg-white rounded-full shadow-lg flex items-center justify-center"
|
||||||
|
aria-label="Previous transformation"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 text-gray-700" 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={goToNext}
|
||||||
|
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-2 w-10 h-10 bg-white rounded-full shadow-lg flex items-center justify-center"
|
||||||
|
aria-label="Next transformation"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Dot Indicators */}
|
||||||
|
<div className="flex justify-center gap-2 mt-6">
|
||||||
|
{results.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setSelectedIndex(index)}
|
||||||
|
className={`w-2 h-2 rounded-full transition-all ${
|
||||||
|
selectedIndex === index ? "bg-black w-4" : "bg-gray-300"
|
||||||
|
}`}
|
||||||
|
aria-label={`Go to transformation ${index + 1}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* CTA */}
|
{/* CTA */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="text-center mt-12"
|
className="text-center mt-12"
|
||||||
|
|||||||
@@ -27,14 +27,12 @@ export default function ProductCard({ product, index = 0, locale = "SR" }: Produ
|
|||||||
>
|
>
|
||||||
<Link href={`/products/${localized.slug}`} className="group block">
|
<Link href={`/products/${localized.slug}`} className="group block">
|
||||||
{/* Image Container */}
|
{/* Image Container */}
|
||||||
<div className="relative aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
|
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
|
||||||
{image ? (
|
{image ? (
|
||||||
<Image
|
<img
|
||||||
src={image}
|
src={image}
|
||||||
alt={localized.name}
|
alt={localized.name}
|
||||||
fill
|
className="w-full h-full object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
|
||||||
className="object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
|
|
||||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
|||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.6 }}
|
transition={{ duration: 0.6 }}
|
||||||
className="flex gap-4"
|
className="flex flex-col md:flex-row gap-4"
|
||||||
>
|
>
|
||||||
{/* Thumbnails - Vertical on Desktop, Hidden on Mobile */}
|
{/* Thumbnails - Vertical on Desktop, Hidden on Mobile */}
|
||||||
{images.length > 1 && (
|
{images.length > 1 && (
|
||||||
@@ -167,12 +167,10 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
|||||||
: "border-transparent hover:border-[#999999]"
|
: "border-transparent hover:border-[#999999]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Image
|
<img
|
||||||
src={image.url}
|
src={image.url}
|
||||||
alt={image.alt || localized.name}
|
alt={image.alt || localized.name}
|
||||||
fill
|
className="w-full h-full object-cover"
|
||||||
className="object-cover"
|
|
||||||
sizes="80px"
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -180,16 +178,53 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main Image */}
|
{/* Main Image */}
|
||||||
<div className="relative aspect-square bg-[#f8f9fa] overflow-hidden">
|
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden flex-1">
|
||||||
{images[selectedImage] && (
|
<img
|
||||||
<Image
|
|
||||||
src={images[selectedImage].url}
|
src={images[selectedImage].url}
|
||||||
alt={images[selectedImage].alt || localized.name}
|
alt={images[selectedImage].alt || localized.name}
|
||||||
fill
|
className="w-full h-full object-cover"
|
||||||
className="object-cover"
|
priority="true"
|
||||||
priority
|
|
||||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Carousel Navigation - Mobile Only */}
|
||||||
|
{images.length > 1 && (
|
||||||
|
<>
|
||||||
|
{/* Left Arrow */}
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedImage(prev => prev === 0 ? images.length - 1 : prev - 1)}
|
||||||
|
className="absolute left-2 top-1/2 -translate-y-1/2 w-10 h-10 bg-white/80 hover:bg-white rounded-full flex items-center justify-center shadow-md transition-all hover:scale-110 md:hidden"
|
||||||
|
aria-label="Previous image"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 text-gray-700" 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>
|
||||||
|
|
||||||
|
{/* Right Arrow */}
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedImage(prev => prev === images.length - 1 ? 0 : prev + 1)}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 w-10 h-10 bg-white/80 hover:bg-white rounded-full flex items-center justify-center shadow-md transition-all hover:scale-110 md:hidden"
|
||||||
|
aria-label="Next image"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Dot Indicators - Mobile Only */}
|
||||||
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2 md:hidden">
|
||||||
|
{images.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setSelectedImage(index)}
|
||||||
|
className={`w-2 h-2 rounded-full transition-all ${
|
||||||
|
selectedImage === index ? "bg-white w-4" : "bg-white/50"
|
||||||
|
}`}
|
||||||
|
aria-label={`Go to image ${index + 1}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
Reference in New Issue
Block a user