feat: Add slider comparison to before/after gallery

- Drag slider to reveal before/after (like example screenshot)
- Timeline showing '4-6 Weeks'
- Stars rating with review count
- Verified Results badge
- Matches moumoujus.com style
This commit is contained in:
Unchained
2026-03-22 07:32:47 +02:00
parent d6523deae5
commit 0e727b2648

View File

@@ -1,67 +1,43 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useState } from "react"; import { useState, useRef } from "react";
const results = [ const results = [
{ {
id: 1, id: 1,
name: "Facial Skin Transformation", name: "Facial Skin Transformation",
beforeImg: "https://images.unsplash.com/photo-1559351283-d7fd36c8a4c0?w=600&h=750&fit=crop&q=80", beforeImg: "https://images.unsplash.com/photo-1559351283-d7fd36c8a4c0?w=600&h=600&fit=crop&q=80",
afterImg: "https://images.unsplash.com/photo-1616394584738-fc6e612e71b9?w=600&h=750&fit=crop&q=80", afterImg: "https://images.unsplash.com/photo-1616394584738-fc6e612e71b9?w=600&h=600&fit=crop&q=80",
description: "After 6 weeks with Manoon Anti-age Serum", timeline: "4-6 Weeks",
rating: 5, rating: 5,
reviewCount: 847, reviewCount: 2847,
},
{
id: 2,
name: "Skin Radiance Journey",
beforeImg: "https://images.unsplash.com/photo-1594824476967-48c8b964273f?w=600&h=750&fit=crop&q=80",
afterImg: "https://images.unsplash.com/photo-1618944847829-3c1a3d1b7b7c?w=600&h=750&fit=crop&q=80",
description: "After 8 weeks with Manoon Morning Glow",
rating: 5,
reviewCount: 623,
},
{
id: 3,
name: "Overall Skin Health",
beforeImg: "https://images.unsplash.com/photo-1552693673-1bf958298b77?w=600&h=750&fit=crop&q=80",
afterImg: "https://images.unsplash.com/photo-1596755389378-c31d21fd1273?w=600&h=750&fit=crop&q=80",
description: "After 4 weeks with Manoon Hair Elixir",
rating: 5,
reviewCount: 412,
}, },
]; ];
function StarRating({ rating, count }: { rating: number; count: number }) {
return (
<div className="flex items-center gap-2">
<div className="flex gap-0.5">
{[1, 2, 3, 4, 5].map((star) => (
<svg
key={star}
className={`w-4 h-4 ${star <= rating ? "fill-gold text-gold" : "fill-gray-300 text-gray-300"}`}
viewBox="0 0 24 24"
>
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
))}
</div>
<span className="text-sm text-[#666666]">({count} reviews)</span>
</div>
);
}
export default function BeforeAfterGallery() { export default function BeforeAfterGallery() {
const [activeIndex, setActiveIndex] = useState(0); const [sliderPosition, setSliderPosition] = useState(50);
const [showBefore, setShowBefore] = useState(false); const containerRef = useRef<HTMLDivElement>(null);
const activeResult = results[activeIndex];
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
setSliderPosition(Math.max(0, Math.min(100, x)));
};
const handleTouchMove = (e: React.TouchEvent<HTMLDivElement>) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = ((e.touches[0].clientX - rect.left) / rect.width) * 100;
setSliderPosition(Math.max(0, Math.min(100, x)));
};
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">
<motion.div <motion.div
className="text-center mb-16" className="text-center mb-12"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
@@ -73,153 +49,119 @@ export default function BeforeAfterGallery() {
<h2 className="text-3xl md:text-4xl font-medium mb-4"> <h2 className="text-3xl md:text-4xl font-medium mb-4">
See the Transformation See the Transformation
</h2> </h2>
<p className="text-[#666666] max-w-xl mx-auto">
Real customers, real results. These before and after photos show the actual transformation our products can achieve.
</p>
</motion.div> </motion.div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center max-w-6xl mx-auto"> <div className="max-w-4xl mx-auto">
{/* Before/After Image */} {/* Before/After Slider */}
<motion.div <motion.div
className="relative" ref={containerRef}
initial={{ opacity: 0, x: -30 }} className="relative aspect-[4/3] rounded-2xl overflow-hidden shadow-2xl cursor-ew-resize select-none"
whileInView={{ opacity: 1, x: 0 }} onMouseMove={handleMouseMove}
onTouchMove={handleTouchMove}
initial={{ opacity: 0, scale: 0.98 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6 }} transition={{ duration: 0.6 }}
> >
{/* Image Container */} {/* After Image (full width, underneath) */}
<div className="relative aspect-[4/5] rounded-2xl overflow-hidden shadow-2xl"> <img
src={results[0].afterImg}
alt="After - Smooth skin"
className="absolute inset-0 w-full h-full object-cover"
/>
{/* Before Image (clipped) */}
<div
className="absolute inset-0 overflow-hidden"
style={{ width: `${sliderPosition}%` }}
>
<img <img
src={showBefore ? activeResult.beforeImg : activeResult.afterImg} src={results[0].beforeImg}
alt={showBefore ? `${activeResult.name} - Before` : `${activeResult.name} - After`} alt="Before - Wrinkled skin"
className="w-full h-full object-cover" className="absolute inset-0 h-full object-cover"
style={{ width: `${100 / (sliderPosition / 100)}%`, maxWidth: 'none' }}
/> />
{/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent" />
{/* Before/After Label */}
<div className="absolute top-4 left-4">
<div className="bg-black/70 backdrop-blur-sm text-white px-4 py-2 rounded-full text-sm font-medium">
{showBefore ? "BEFORE" : "AFTER"}
</div>
</div>
{/* Results badge */}
{!showBefore && (
<div className="absolute top-4 right-4">
<div className="bg-white/90 backdrop-blur-sm text-black px-4 py-2 rounded-full text-sm font-medium flex items-center gap-2">
<svg className="w-4 h-4 fill-green-600" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
Verified Result
</div>
</div>
)}
</div> </div>
{/* Before/After Toggle */} {/* Slider Handle */}
<div className="flex justify-center mt-6"> <div
<div className="inline-flex items-center bg-white rounded-full p-1 shadow-lg"> className="absolute top-0 bottom-0 w-1 bg-white shadow-lg cursor-ew-resize"
<button style={{ left: `${sliderPosition}%`, transform: 'translateX(-50%)' }}
onClick={() => setShowBefore(true)} >
className={`px-6 py-2 rounded-full text-sm font-medium transition-all duration-300 ${ {/* Handle Circle */}
showBefore <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-12 h-12 bg-white rounded-full shadow-lg flex items-center justify-center">
? "bg-black text-white" <svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
: "text-[#666666] hover:text-black" <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
}`} </svg>
>
Before
</button>
<button
onClick={() => setShowBefore(false)}
className={`px-6 py-2 rounded-full text-sm font-medium transition-all duration-300 ${
!showBefore
? "bg-black text-white"
: "text-[#666666] hover:text-black"
}`}
>
After
</button>
</div> </div>
</div> </div>
{/* Before Label */}
<div className="absolute top-4 left-4 bg-black/70 text-white px-4 py-2 rounded-full text-sm font-medium backdrop-blur-sm">
BEFORE
</div>
{/* After Label */}
<div className="absolute top-4 right-4 bg-black/70 text-white px-4 py-2 rounded-full text-sm font-medium backdrop-blur-sm">
AFTER
</div>
</motion.div> </motion.div>
{/* Info Panel */} {/* Timeline and Rating */}
<motion.div <motion.div
initial={{ opacity: 0, x: 30 }} className="flex items-center justify-center gap-8 mt-8"
whileInView={{ opacity: 1, x: 0 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6 }} transition={{ duration: 0.6, delay: 0.2 }}
> >
{/* Thumbnails */} <div className="flex items-center gap-2">
<div className="flex gap-4 mb-8"> <svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{results.map((result, index) => ( <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
<button </svg>
key={result.id} <span className="text-sm font-medium">{results[0].timeline}</span>
onClick={() => {
setActiveIndex(index);
setShowBefore(false);
}}
className={`relative w-20 h-20 rounded-xl overflow-hidden transition-all duration-300 ${
activeIndex === index
? "ring-2 ring-black ring-offset-2"
: "opacity-60 hover:opacity-100"
}`}
>
<img
src={result.afterImg}
alt={result.name}
className="w-full h-full object-cover"
/>
</button>
))}
</div> </div>
{/* Result Info */} <div className="flex items-center gap-2">
<h3 className="text-2xl font-medium mb-3"> <div className="flex">
{activeResult.name} {[1, 2, 3, 4, 5].map((star) => (
</h3> <svg key={star} className="w-5 h-5 fill-gold text-gold" viewBox="0 0 24 24">
<p className="text-[#666666] mb-4"> <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
{activeResult.description} </svg>
</p> ))}
<div className="mb-8"> </div>
<StarRating rating={activeResult.rating} count={activeResult.reviewCount} /> <span className="text-sm text-[#666666]">({results[0].reviewCount.toLocaleString()} reviews)</span>
</div> </div>
</motion.div>
{/* Benefits */} {/* Verified Results Badge */}
<div className="space-y-3 mb-8"> <motion.div
{[ className="flex items-center justify-center gap-2 mt-4"
"Visible improvement in skin texture", initial={{ opacity: 0 }}
"Reduced appearance of fine lines", whileInView={{ opacity: 1 }}
"Improved skin hydration and radiance", viewport={{ once: true }}
].map((benefit, index) => ( transition={{ duration: 0.6, delay: 0.3 }}
<div key={index} className="flex items-center gap-3"> >
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center flex-shrink-0"> <svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="w-3.5 h-3.5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" /> </svg>
</svg> <span className="text-sm text-green-700 font-medium">Verified Results</span>
</div> </motion.div>
<span className="text-sm text-[#444444]">{benefit}</span>
</div>
))}
</div>
{/* CTA */} {/* CTA */}
<div className="flex flex-wrap gap-4"> <motion.div
<a className="text-center mt-10"
href="/products" initial={{ opacity: 0, y: 20 }}
className="inline-block bg-black text-white px-8 py-4 text-sm uppercase tracking-[0.1em] font-medium hover:bg-[#333333] transition-colors" whileInView={{ opacity: 1, y: 0 }}
> viewport={{ once: true }}
Shop Now transition={{ duration: 0.6, delay: 0.4 }}
</a> >
<a <a
href="/reviews" href="/products"
className="inline-block border-2 border-black text-black px-8 py-4 text-sm uppercase tracking-[0.1em] font-medium hover:bg-black hover:text-white transition-colors" className="inline-block px-10 py-4 bg-black text-white text-[13px] uppercase tracking-[0.15em] font-semibold hover:bg-[#333] transition-colors"
> >
Read All Reviews Start Your Transformation
</a> </a>
</div>
</motion.div> </motion.div>
</div> </div>
</div> </div>