262 lines
11 KiB
TypeScript
262 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import Image from 'next/image';
|
|
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: 'Rahul Mehta',
|
|
role: 'Growth Manager',
|
|
reviewTitle: 'Highly Impressive Results!',
|
|
text: 'The performance insights are extremely clear. We can now adjust campaigns quickly and make confident marketing decisions.',
|
|
image: '/images/home/testimonial-1.webp'
|
|
},
|
|
{
|
|
name: 'Emily Carter',
|
|
role: 'Content Strategist',
|
|
reviewTitle: 'Smooth and Reliable Platform!',
|
|
text: 'Managing posts feels effortless now. Planning content ahead of time has significantly improved our publishing consistency.',
|
|
image: '/images/home/testimonial-2.webp'
|
|
},
|
|
{
|
|
name: 'Arjun Nair',
|
|
role: 'Digital Consultant',
|
|
reviewTitle: 'Excellent Tool for Teams!',
|
|
text: 'Collaboration is seamless and approvals are faster. It has simplified how we handle multiple client accounts.',
|
|
image: '/images/home/testimonial-3.webp'
|
|
},
|
|
{
|
|
name: 'Sophia Williams',
|
|
role: 'Brand Manager',
|
|
reviewTitle: 'Well Designed and Effective!',
|
|
text: 'The interface is clean and easy to navigate. Tracking engagement across platforms has never been this simple.',
|
|
image: '/images/home/testimonial-4.webp'
|
|
},
|
|
{
|
|
name: 'Daniel Rodrigues',
|
|
role: ' Marketing Lead',
|
|
reviewTitle: 'Strong Value for Growth!',
|
|
text: 'The reporting features save us hours every week. Insights are easy to understand and useful for strategy planning.',
|
|
image: '/images/home/testimonial-5.webp'
|
|
},
|
|
];
|
|
|
|
// 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;
|
|
|
|
const [cardWidth, setCardWidth] = useState(352);
|
|
const [containerMaxWidth, setContainerMaxWidth] = useState<number | string>('100%');
|
|
|
|
useEffect(() => {
|
|
const handleResize = () => {
|
|
const width = window.innerWidth;
|
|
|
|
if (width <= 480) {
|
|
// Mobile: Card 260, Gap 16. Container Padding 32 (layout)
|
|
setCardWidth(276); // Stride
|
|
const available = width - 32;
|
|
const stride = 276;
|
|
const gap = 16;
|
|
const count = Math.max(1, Math.floor((available + gap) / stride));
|
|
setContainerMaxWidth(count * stride - gap);
|
|
} else if (width <= 1024) {
|
|
// Tablet: Card 280, Gap 32. Container Padding 32
|
|
setCardWidth(312); // Stride
|
|
const available = width - 32;
|
|
const stride = 312;
|
|
const gap = 32;
|
|
const count = Math.max(1, Math.floor((available + gap) / stride));
|
|
setContainerMaxWidth(count * stride - gap);
|
|
} else {
|
|
// Desktop: Force 2 cards exactly per user request.
|
|
// Card 320, Gap 32.
|
|
// 2 Cards = 320*2 + 32 = 672.
|
|
// Plus padding-left: 6rem (96px) defined in CSS for desktop layout.
|
|
setCardWidth(352); // Stride
|
|
setContainerMaxWidth(768); // 672 + 96
|
|
}
|
|
};
|
|
|
|
// Initial call
|
|
handleResize();
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
return () => window.removeEventListener('resize', handleResize);
|
|
}, []);
|
|
|
|
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 already at totalOriginal, wait for reset effect
|
|
if (prev >= totalOriginal) return prev;
|
|
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;
|
|
if (activeIndex >= totalOriginal) return; // Wait for reset
|
|
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}>
|
|
Unlock Your Growth Opportunities
|
|
</h2>
|
|
<p className={styles.cardDescription}>
|
|
Connect with a global community of marketers and creators who have elevated their digital presence using Social Buddy. Real progress, real outcomes - getting started takes only a moment.
|
|
</p>
|
|
|
|
<div className={styles.miniStats}>
|
|
<div className={styles.statBadge}>
|
|
<Image
|
|
src="/images/home/active-members.webp"
|
|
alt="Unified Message Center"
|
|
width={29}
|
|
height={29}
|
|
/>
|
|
<div>
|
|
<strong>10,000+</strong>
|
|
<span>Active Members</span>
|
|
</div>
|
|
</div>
|
|
<div className={styles.statBadge}>
|
|
<Image
|
|
src="/images/home/user-satisfaction.webp"
|
|
alt="Unified Message Center"
|
|
width={29}
|
|
height={29}
|
|
/>
|
|
<div>
|
|
<strong>4.9/5</strong>
|
|
<span>User Satisfaction</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button className={styles.readMoreBtn}>
|
|
View Customer Stories
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Side Slider - Lower Z-Index, moves behind Left Card */}
|
|
<div
|
|
className={styles.sliderContainer}
|
|
style={{ width: containerMaxWidth, maxWidth: '100%' }} // Use width to force size, maxWidth for safety
|
|
>
|
|
<div
|
|
className={styles.sliderTrack}
|
|
style={{
|
|
transform: `translateX(calc(-${activeIndex * cardWidth}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}>
|
|
<Image
|
|
src={t.image}
|
|
alt="User"
|
|
width={42}
|
|
height={48}
|
|
/>
|
|
</div>
|
|
<div className={styles.userInfo}>
|
|
<h4>{t.name}</h4>
|
|
<span className={styles.userCompany}>{t.role}</span>
|
|
<div className={styles.rating}>⭐⭐⭐⭐⭐</div>
|
|
</div>
|
|
</div>
|
|
<h5 className={styles.reviewTitle}>{t.reviewTitle}</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>
|
|
);
|
|
}
|