209 lines
8.4 KiB
TypeScript
209 lines
8.4 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import styles from './Testimonials.module.css';
|
|
|
|
export default function Testimonials() {
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
const [isResetting, setIsResetting] = useState(false);
|
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const testimonials = [
|
|
{
|
|
name: 'Sarah Johnson',
|
|
role: 'Marketing Director',
|
|
company: 'TechFlow Inc.',
|
|
text: 'SocialBuddy has completely transformed how we manage our social media. The scheduling features and analytics are game-changers.',
|
|
image: '👩💼'
|
|
},
|
|
{
|
|
name: 'Michael Chen',
|
|
role: 'Content Creator',
|
|
company: 'Digital Nomad',
|
|
text: 'As a solo creator managing multiple platforms, SocialBuddy saves me hours every week. The AI content suggestions are incredibly helpful.',
|
|
image: '👨💻'
|
|
},
|
|
{
|
|
name: 'Emily Rodriguez',
|
|
role: 'Social Media Manager',
|
|
company: 'BrandBoost Agency',
|
|
text: 'Managing 20+ client accounts used to be overwhelming. SocialBuddy\'s team collaboration features have streamlined our process.',
|
|
image: '👩🎨'
|
|
},
|
|
{
|
|
name: 'David Park',
|
|
role: 'CEO',
|
|
company: 'StartupHub',
|
|
text: 'The analytics and reporting features are outstanding. We can now make data-driven decisions about our social media strategy.',
|
|
image: '👨💼'
|
|
},
|
|
{
|
|
name: 'Lisa Thompson',
|
|
role: 'Influencer',
|
|
company: 'Lifestyle & Travel',
|
|
text: 'I love how intuitive SocialBuddy is! The content calendar helps me plan my posts weeks in advance.',
|
|
image: '👩🦰'
|
|
},
|
|
{
|
|
name: 'James Wilson',
|
|
role: 'E-commerce Manager',
|
|
company: 'ShopSmart',
|
|
text: 'SocialBuddy has helped us maintain a consistent brand presence across all platforms. The bulk scheduling feature is amazing.',
|
|
image: '👨🏫'
|
|
}
|
|
];
|
|
|
|
// Clone the first few items to create the infinite illusion
|
|
// We need enough clones to fill the visible area. 4 is safe.
|
|
const extendedTestimonials = [...testimonials, ...testimonials.slice(0, 4)];
|
|
const totalOriginal = testimonials.length;
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
handleNext();
|
|
}, 3000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [activeIndex, isResetting]); // Re-create interval if state changes to avoid stale closures, though functional update handles it.
|
|
|
|
const handleNext = () => {
|
|
if (isResetting) return;
|
|
|
|
setActiveIndex((prev) => {
|
|
// If we are at the end of the original list (showing the first clone)
|
|
// We animate TO the clone.
|
|
return prev + 1;
|
|
});
|
|
};
|
|
|
|
// Handle seamless reset
|
|
useEffect(() => {
|
|
if (activeIndex === totalOriginal) {
|
|
// We have just slid TO the first cloned item (visually identical to index 0)
|
|
// Wait for transition to finish, then snap back to real 0
|
|
timeoutRef.current = setTimeout(() => {
|
|
setIsResetting(true); // Disable transition
|
|
setActiveIndex(0); // Jump to 0
|
|
|
|
// Re-enable transition after a brief moment to allow DOM update
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
setIsResetting(false);
|
|
});
|
|
});
|
|
}, 500); // Must match CSS transition duration
|
|
}
|
|
|
|
return () => {
|
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
};
|
|
}, [activeIndex, totalOriginal]);
|
|
|
|
const slideNext = () => {
|
|
if (isResetting) return;
|
|
setActiveIndex(prev => prev + 1);
|
|
};
|
|
|
|
const slidePrev = () => {
|
|
if (isResetting) return;
|
|
if (activeIndex === 0) {
|
|
// Jump to end clone without transition, then slide back
|
|
setIsResetting(true);
|
|
setActiveIndex(totalOriginal);
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
setIsResetting(false);
|
|
setActiveIndex(totalOriginal - 1);
|
|
});
|
|
});
|
|
} else {
|
|
setActiveIndex(prev => prev - 1);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<section className={styles.testimonialSection} id="testimonials">
|
|
<div className={styles.layoutContainer}>
|
|
{/* Left Side Static Card - High Z-Index */}
|
|
<div className={styles.leftCard}>
|
|
<div className={styles.cardContent}>
|
|
<h2 className={styles.cardTitle}>
|
|
Discover your <br />
|
|
entrepreneurial potential
|
|
</h2>
|
|
<p className={styles.cardDescription}>
|
|
Join countless marketers and influencers who have transformed their social media presence with SocialBuddy.
|
|
Success is just a click away.
|
|
</p>
|
|
|
|
<div className={styles.miniStats}>
|
|
<div className={styles.statBadge}>
|
|
<span className={styles.statIcon}>👥</span>
|
|
<div>
|
|
<strong>10,000+</strong>
|
|
<span>Happy Users</span>
|
|
</div>
|
|
</div>
|
|
<div className={styles.statBadge}>
|
|
<span className={styles.statIcon}>⭐</span>
|
|
<div>
|
|
<strong>4.9/5</strong>
|
|
<span>Ratings</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button className={styles.readMoreBtn}>
|
|
Read Success Stories
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Side Slider - Lower Z-Index, moves behind Left Card */}
|
|
<div className={styles.sliderContainer}>
|
|
<div
|
|
className={styles.sliderTrack}
|
|
style={{
|
|
transform: `translateX(calc(-${activeIndex * 352}px))`,
|
|
transition: isResetting ? 'none' : 'transform 0.5s cubic-bezier(0.2, 0.8, 0.2, 1)'
|
|
}}
|
|
>
|
|
{extendedTestimonials.map((t, i) => (
|
|
<div key={i} className={styles.sliderCard}>
|
|
<div className={styles.cardHeader}>
|
|
<div className={styles.userImage}>{t.image}</div>
|
|
<div className={styles.userInfo}>
|
|
<h4>{t.name}</h4>
|
|
<span className={styles.userCompany}>{t.company}</span>
|
|
</div>
|
|
<div className={styles.rating}>⭐⭐⭐⭐⭐</div>
|
|
</div>
|
|
<h5 className={styles.reviewTitle}>Amazing Experience!</h5>
|
|
<p className={styles.reviewText}>{t.text}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Navigation Arrows */}
|
|
<div className={styles.sliderNav}>
|
|
<button
|
|
className={styles.navBtn}
|
|
onClick={slidePrev}
|
|
aria-label="Previous testimonial"
|
|
>
|
|
←
|
|
</button>
|
|
<button
|
|
className={styles.navBtn}
|
|
onClick={slideNext}
|
|
aria-label="Next testimonial"
|
|
>
|
|
→
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|