116 lines
4.8 KiB
TypeScript
116 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import styles from "@/app/channels/[slug]/channel-page.module.css";
|
|
import SafeImage from "./SafeImage";
|
|
import { Star, Quote } from "lucide-react";
|
|
|
|
interface Testimonial {
|
|
quote: string;
|
|
author: string;
|
|
role: string;
|
|
rating: number;
|
|
image: string;
|
|
}
|
|
|
|
export default function ChannelTestimonialSlider({ testimonials, staticImage }: { testimonials: Testimonial[], staticImage?: string }) {
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
const [isAnimating, setIsAnimating] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
handleNext();
|
|
}, 5000); // 5 seconds per slide
|
|
return () => clearInterval(interval);
|
|
}, [testimonials.length]);
|
|
|
|
const handleNext = () => {
|
|
setIsAnimating(true);
|
|
setTimeout(() => {
|
|
setCurrentIndex((prev) => (prev + 1) % testimonials.length);
|
|
setIsAnimating(false);
|
|
}, 300);
|
|
};
|
|
|
|
const current = testimonials[currentIndex];
|
|
|
|
// Determine what image to show on the left
|
|
// If staticImage is provided, use it always.
|
|
// If not, use the current testimonial's image.
|
|
const leftImageSrc = staticImage || current.image;
|
|
|
|
// Only animate the left image if it is changing (i.e., no staticImage)
|
|
const leftImageClass = staticImage
|
|
? styles.sliderLeftImage
|
|
: `${styles.sliderLeftImage} ${isAnimating ? styles.fadeOut : styles.fadeIn}`;
|
|
|
|
return (
|
|
<div className={styles.channelTestimonialContainer}>
|
|
{/* Left Side: Large Image */}
|
|
<div className={leftImageClass}>
|
|
<div className={styles.sliderImageWrapper}>
|
|
<SafeImage
|
|
src={leftImageSrc}
|
|
alt={staticImage ? "Testimonial" : current.author}
|
|
className={styles.sliderMainImage}
|
|
fallbackSrc={`https://placehold.co/600x600/png?text=${encodeURIComponent(current.author)}`}
|
|
/>
|
|
{/* "Trusted Clients" Pill could go here, but user said "bottom img that no need"?
|
|
I'll add a simple name tag overlay if needed, but sticking to clean image first.
|
|
*/}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Side: Content Box */}
|
|
<div className={styles.sliderRightContent}>
|
|
<div className={styles.testimonialCard}>
|
|
<div className={styles.testCardHeader}>
|
|
<Quote size={40} className={styles.quoteIcon} />
|
|
<div className={styles.testStars}>
|
|
{[...Array(5)].map((_, i) => (
|
|
<Star
|
|
key={i}
|
|
fill={i < current.rating ? "#fff" : "none"}
|
|
stroke={i < current.rating ? "#fff" : "rgba(255,255,255,0.4)"}
|
|
size={20}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<blockquote className={`${styles.testQuoteText} ${isAnimating ? styles.fadeOutText : styles.fadeInText}`}>
|
|
"{current.quote}"
|
|
</blockquote>
|
|
|
|
<div className={`${styles.testAuthorRow} ${isAnimating ? styles.fadeOutText : styles.fadeInText}`}>
|
|
<div className={styles.testAuthorAvatarWrapper}>
|
|
<SafeImage
|
|
key={currentIndex}
|
|
src={current.image}
|
|
alt={current.author}
|
|
className={styles.testSmallAvatar}
|
|
fallbackSrc={`https://placehold.co/100x100/png?text=${current.author.charAt(0)}`}
|
|
/>
|
|
</div>
|
|
<div className={styles.testAuthorInfoWhite}>
|
|
<h4>{current.author}</h4>
|
|
<span>{current.role}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.sliderDotsWhite}>
|
|
{testimonials.map((_, idx) => (
|
|
<button
|
|
key={idx}
|
|
className={`${styles.dotWhite} ${idx === currentIndex ? styles.activeDotWhite : ""}`}
|
|
onClick={() => setCurrentIndex(idx)}
|
|
aria-label={`Go to slide ${idx + 1}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|