146 lines
5.6 KiB
TypeScript
146 lines
5.6 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
|
|
interface Testimonial {
|
|
id: number;
|
|
name: string;
|
|
role: string;
|
|
company: string;
|
|
content: string;
|
|
avatar?: string;
|
|
rating: number;
|
|
}
|
|
|
|
interface TestimonialSliderProps {
|
|
testimonials: Testimonial[];
|
|
autoPlayInterval?: number;
|
|
}
|
|
|
|
export const TestimonialSlider: React.FC<TestimonialSliderProps> = ({
|
|
testimonials,
|
|
autoPlayInterval = 5000
|
|
}) => {
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
const [isAnimating, setIsAnimating] = useState(false);
|
|
|
|
const goToNext = useCallback(() => {
|
|
if (isAnimating) return;
|
|
setIsAnimating(true);
|
|
setCurrentIndex((prev) => (prev + 1) % testimonials.length);
|
|
setTimeout(() => setIsAnimating(false), 500);
|
|
}, [isAnimating, testimonials.length]);
|
|
|
|
const goToPrev = useCallback(() => {
|
|
if (isAnimating) return;
|
|
setIsAnimating(true);
|
|
setCurrentIndex((prev) => (prev - 1 + testimonials.length) % testimonials.length);
|
|
setTimeout(() => setIsAnimating(false), 500);
|
|
}, [isAnimating, testimonials.length]);
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(goToNext, autoPlayInterval);
|
|
return () => clearInterval(interval);
|
|
}, [goToNext, autoPlayInterval]);
|
|
|
|
const currentTestimonial = testimonials[currentIndex];
|
|
|
|
return (
|
|
<div className="relative max-w-4xl mx-auto">
|
|
{/* Background decoration */}
|
|
<div className="absolute -top-4 -left-4 text-8xl text-zsl-primary/10 font-serif select-none">"</div>
|
|
<div className="absolute -bottom-4 -right-4 text-8xl text-zsl-primary/10 font-serif rotate-180 select-none">"</div>
|
|
|
|
{/* Main card */}
|
|
<div className="relative bg-zsl-card/80 backdrop-blur-md border border-zsl-primary/20 rounded-2xl p-8 md:p-12">
|
|
{/* Content */}
|
|
<div className={`transition-all duration-500 ${isAnimating ? 'opacity-0 translate-y-4' : 'opacity-100 translate-y-0'}`}>
|
|
{/* Stars */}
|
|
<div className="flex gap-1 mb-6">
|
|
{[...Array(5)].map((_, i) => (
|
|
<svg
|
|
key={i}
|
|
className={`w-5 h-5 ${i < currentTestimonial.rating ? 'text-zsl-accent' : 'text-zsl-muted/30'}`}
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
>
|
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
|
</svg>
|
|
))}
|
|
</div>
|
|
|
|
{/* Quote */}
|
|
<p className="text-lg md:text-xl text-white leading-relaxed mb-8">
|
|
"{currentTestimonial.content}"
|
|
</p>
|
|
|
|
{/* Author */}
|
|
<div className="flex items-center gap-4">
|
|
{/* Avatar */}
|
|
<div className="w-14 h-14 rounded-full bg-linear-to-br from-zsl-primary to-zsl-accent p-0.5">
|
|
<div className="w-full h-full rounded-full bg-zsl-card flex items-center justify-center text-xl font-bold text-zsl-primary">
|
|
{currentTestimonial.name.charAt(0)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Info */}
|
|
<div>
|
|
<div className="font-semibold text-white">{currentTestimonial.name}</div>
|
|
<div className="text-sm text-zsl-muted">
|
|
{currentTestimonial.role} • {currentTestimonial.company}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<div className="flex items-center justify-between mt-8 pt-6 border-t border-zsl-primary/10">
|
|
{/* Arrows */}
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={goToPrev}
|
|
className="w-10 h-10 rounded-full border border-zsl-primary/30 flex items-center justify-center text-zsl-primary hover:bg-zsl-primary hover:text-zsl-bg transition-all duration-300"
|
|
aria-label="Previous testimonial"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={goToNext}
|
|
className="w-10 h-10 rounded-full border border-zsl-primary/30 flex items-center justify-center text-zsl-primary hover:bg-zsl-primary hover:text-zsl-bg transition-all duration-300"
|
|
aria-label="Next testimonial"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Dots */}
|
|
<div className="flex gap-2">
|
|
{testimonials.map((_, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => {
|
|
if (!isAnimating) {
|
|
setIsAnimating(true);
|
|
setCurrentIndex(index);
|
|
setTimeout(() => setIsAnimating(false), 500);
|
|
}
|
|
}}
|
|
className={`w-2 h-2 rounded-full transition-all duration-300 ${
|
|
index === currentIndex
|
|
? 'bg-zsl-primary w-6'
|
|
: 'bg-zsl-muted/30 hover:bg-zsl-muted/50'
|
|
}`}
|
|
aria-label={`Go to testimonial ${index + 1}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|