Files
KLC-Website-Frontend/src/components/TestimonialsSection.tsx
2026-03-27 12:43:34 +05:30

532 lines
18 KiB
TypeScript

import { motion } from "motion/react";
import { ImageWithFallback } from "./figma/ImageWithFallback";
import { Play, X, ChevronLeft, ChevronRight, Star } from "lucide-react";
import { useState, useRef, useEffect } from "react";
import { BrandedTag } from "./about/BrandedTag";
interface Testimonial {
id?: number | string;
name: string;
role: string;
company?: string;
avatar?: string;
image?: string;
quote: string;
rating: number;
isVideo?: boolean;
videoThumbnail?: string;
videoUrl?: string;
designation?: string;
content?: string;
video_url?: string;
profile_xid?: string;
}
// Default testimonials as fallback
const defaultTestimonialsData: Testimonial[] = [
{
id: 1,
name: "Sarah Chen",
role: "Chief Executive Officer",
company: "TechCorp Solutions",
avatar: "https://images.unsplash.com/photo-1494790108755-2616b612b786?w=400&h=400&fit=crop&crop=face",
quote: "KLC has revolutionized how we approach leadership development. The AI-powered insights are incredibly precise and have transformed our management effectiveness across our entire organization.",
rating: 5,
isVideo: true,
videoThumbnail: "https://images.unsplash.com/photo-1552664730-d307ca884978?w=600&h=300&fit=crop",
videoUrl: "https://example.com/testimonial-video-1.mp4"
},
{
id: 2,
name: "Michael Rodriguez",
role: "VP of Operations",
company: "Global Industries",
avatar: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&h=400&fit=crop&crop=face",
quote: "The strategic leadership programs have equipped our team with the tools needed to navigate complex business challenges with confidence and clarity. The transformation has been remarkable.",
rating: 5,
isVideo: false
},
{
id: 3,
name: "Jennifer Park",
role: "Director of Human Resources",
company: "Innovation Labs",
avatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&h=400&fit=crop&crop=face",
quote: "KLC's approach to leadership development is refreshingly practical. Our managers have shown remarkable improvement in team engagement and decision-making capabilities.",
rating: 4,
isVideo: true,
videoThumbnail: "https://images.unsplash.com/photo-1560472355-109703aa3edc?w=600&h=300&fit=crop",
videoUrl: "https://example.com/testimonial-video-2.mp4"
}
];
// Star Rating Component
function StarRating({ rating }: { rating: number }) {
return (
<div className="flex gap-1 mb-3">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
size={16}
className={`${
star <= rating
? 'fill-current'
: 'text-gray-300'
}`}
style={{
color: star <= rating ? 'var(--color-accent)' : '#D1D5DB'
}}
/>
))}
</div>
);
}
// Video Modal Component
function VideoModal({ isOpen, onClose, videoUrl }: {
isOpen: boolean;
onClose: () => void;
videoUrl: string;
}) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="relative w-full max-w-4xl aspect-video bg-black rounded-xl overflow-hidden">
<button
onClick={onClose}
className="absolute top-4 right-4 z-10 w-10 h-10 bg-black/50 hover:bg-black/70 rounded-full flex items-center justify-center text-white transition-all duration-200"
>
<X size={20} />
</button>
<div className="w-full h-full flex items-center justify-center text-white">
<div className="text-center">
<Play size={64} className="mx-auto mb-4 opacity-60" />
<p className="text-lg opacity-80">Video Testimonial</p>
<p className="text-sm opacity-60 mt-2">{videoUrl}</p>
</div>
</div>
</div>
</div>
);
}
// Individual Testimonial Card
function TestimonialCard({ testimonial, onPlayVideo }: {
testimonial: Testimonial;
onPlayVideo: (videoUrl: string) => void;
}) {
const avatarSrc = testimonial.avatar || testimonial.image;
const isVideo = testimonial.isVideo || !!testimonial.video_url;
const videoUrl = testimonial.videoUrl || testimonial.video_url || "";
const role = testimonial.role || testimonial.designation || "";
const quote = testimonial.quote || testimonial.content || "";
const name = testimonial.name || "";
const rating = testimonial.rating || 5;
return (
<motion.div
className="testimonial-card bg-white rounded-xl border transition-all duration-300 flex-shrink-0 w-[350px] h-[300px] card-hover-lift"
style={{
borderColor: 'rgba(0, 0, 0, 0.1)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)'
}}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
whileHover={{
boxShadow: '0 8px 25px rgba(0, 0, 0, 0.15)',
transform: 'translateY(-4px)'
}}
>
{/* Video Testimonials */}
{isVideo ? (
<div
className="relative h-full cursor-pointer overflow-hidden group rounded-xl"
onClick={() => onPlayVideo(videoUrl)}
>
<ImageWithFallback
src={testimonial.videoThumbnail || avatarSrc || "https://images.unsplash.com/photo-1552664730-d307ca884978?w=600&h=300&fit=crop"}
alt={`${name} video testimonial`}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
{/* Video Overlay with Gradient */}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent group-hover:from-black/70 transition-all duration-300" />
{/* Play Button - Compact Design */}
<div className="absolute inset-0 flex items-center justify-center">
<motion.div
className="flex items-center justify-center w-16 h-16 bg-white/90 backdrop-blur-sm rounded-full shadow-lg"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
>
<Play
className="w-7 h-7 ml-1 text-blue-600"
fill="currentColor"
/>
</motion.div>
</div>
{/* Video Label - Compact Style */}
<div className="absolute top-4 left-4">
<div className="px-3 py-1 rounded-full text-xs font-medium text-white bg-blue-600">
🎥 Video
</div>
</div>
{/* Profile Info - Bottom Section */}
<div className="absolute bottom-0 left-0 right-0 p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full overflow-hidden bg-white shadow-lg flex-shrink-0">
<ImageWithFallback
src={avatarSrc || ""}
alt={name}
className="w-full h-full object-cover"
/>
</div>
<div className="min-w-0 flex-1">
<h4 className="font-semibold text-white mb-1 text-sm">
{name}
</h4>
<p className="text-xs text-white/80 truncate">
{role}
{testimonial.company && `${testimonial.company}`}
</p>
</div>
{/* Star Rating */}
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
size={14}
className={star <= rating ? 'fill-current text-yellow-400' : 'text-white/40'}
/>
))}
</div>
</div>
</div>
</div>
) : (
/* Text Testimonials - Compact Design */
<div className="h-full flex flex-col p-6">
{/* Header Section */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full overflow-hidden bg-gray-100 flex-shrink-0">
<ImageWithFallback
src={avatarSrc || ""}
alt={name}
className="w-full h-full object-cover"
/>
</div>
<div className="min-w-0">
<h4 className="font-semibold text-black mb-1 text-sm">
{name}
</h4>
<p className="text-xs text-gray-600">
{role}
</p>
{testimonial.company && (
<p className="text-xs text-gray-500 font-medium">
{testimonial.company}
</p>
)}
</div>
</div>
{/* Star Rating - Top Right */}
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={star}
size={14}
className={star <= rating ? 'fill-current text-yellow-400' : 'text-gray-300'}
/>
))}
</div>
</div>
{/* Quote Section - Compact Typography */}
<blockquote className="flex-1 mb-4">
<div className="text-sm leading-relaxed text-black relative">
<span className="text-4xl absolute -top-1 -left-1 leading-none opacity-20 text-blue-600">
"
</span>
<span className="relative z-10">
{quote}
</span>
</div>
</blockquote>
{/* Bottom Accent Line */}
<div className="w-12 h-1 rounded-full bg-blue-600" />
</div>
)}
</motion.div>
);
}
export function TestimonialsSection({
customTestimonials,
title = "What Our Leaders Say",
subtitle = "Hear from executives and managers who have transformed their leadership approach through our comprehensive development programs.",
tagText = "Success Stories"
}: {
customTestimonials?: Testimonial[];
title?: string;
subtitle?: string;
tagText?: string;
}) {
const [isVideoModalOpen, setIsVideoModalOpen] = useState(false);
const [currentVideoUrl, setCurrentVideoUrl] = useState("");
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(true);
const [isDragging, setIsDragging] = useState(false);
const scrollContainerRef = useRef<HTMLDivElement>(null);
// Use custom testimonials if provided, otherwise use default
const testimonialsData = customTestimonials || defaultTestimonialsData;
// Handle scroll state
const handleScroll = () => {
if (scrollContainerRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current;
setCanScrollLeft(scrollLeft > 0);
setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 10);
}
};
// Scroll to direction - Updated for compact card width
const scrollToDirection = (direction: 'left' | 'right') => {
if (scrollContainerRef.current) {
const scrollAmount = 390; // Adjusted for compact card width (350px + 32px gap)
const currentScroll = scrollContainerRef.current.scrollLeft;
const targetScroll = direction === 'left'
? Math.max(0, currentScroll - scrollAmount)
: currentScroll + scrollAmount;
scrollContainerRef.current.scrollTo({
left: targetScroll,
behavior: 'smooth'
});
}
};
// Mouse drag functionality
const [dragStart, setDragStart] = useState({ x: 0, scrollLeft: 0 });
const handleMouseDown = (e: React.MouseEvent) => {
setIsDragging(true);
if (scrollContainerRef.current) {
setDragStart({
x: e.pageX,
scrollLeft: scrollContainerRef.current.scrollLeft
});
}
};
const handleMouseMove = (e: React.MouseEvent) => {
if (!isDragging || !scrollContainerRef.current) return;
e.preventDefault();
const x = e.pageX;
const walk = (x - dragStart.x) * 2;
scrollContainerRef.current.scrollLeft = dragStart.scrollLeft - walk;
};
const handleMouseUp = () => {
setIsDragging(false);
};
// Touch functionality
const [touchStart, setTouchStart] = useState({ x: 0, scrollLeft: 0 });
const handleTouchStart = (e: React.TouchEvent) => {
if (scrollContainerRef.current) {
setTouchStart({
x: e.touches[0].clientX,
scrollLeft: scrollContainerRef.current.scrollLeft
});
}
};
const handleTouchMove = (e: React.TouchEvent) => {
if (!scrollContainerRef.current) return;
const x = e.touches[0].clientX;
const walk = (x - touchStart.x) * 2;
scrollContainerRef.current.scrollLeft = touchStart.scrollLeft - walk;
};
const handleTouchEnd = () => {
// Touch ended
};
const handlePlayVideo = (videoUrl: string) => {
setCurrentVideoUrl(videoUrl);
setIsVideoModalOpen(true);
};
const handleCloseModal = () => {
setIsVideoModalOpen(false);
setCurrentVideoUrl("");
};
// Initialize scroll state and keyboard navigation
useEffect(() => {
const timer = setTimeout(() => {
handleScroll();
}, 100);
// Keyboard navigation support
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowLeft' && canScrollLeft) {
e.preventDefault();
scrollToDirection('left');
} else if (e.key === 'ArrowRight' && canScrollRight) {
e.preventDefault();
scrollToDirection('right');
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
clearTimeout(timer);
document.removeEventListener('keydown', handleKeyDown);
};
}, [canScrollLeft, canScrollRight]);
return (
<section
className="py-24"
style={{ backgroundColor: '#FFFFFF' }}
aria-labelledby="testimonials-heading"
>
<div className="max-w-7xl mx-auto section-margin-x" data-layout="testimonials-v1">
{/* Section Header */}
<div className="text-center mb-16">
{/* Branded Tag */}
<div className="flex justify-center">
<BrandedTag text={tagText} />
</div>
<h2 id="testimonials-heading" className="text-h2 mb-6">{title}</h2>
<p className="text-body-lg text-muted max-w-3xl mx-auto">{subtitle}</p>
</div>
{/* Testimonials Cards Area */}
<div className="relative">
{/* Compact Navigation Controls */}
<div className="flex justify-end gap-3 mb-8 relative z-20">
<button
className={`w-12 h-12 flex items-center justify-center rounded-lg border-2 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
!canScrollLeft
? 'opacity-40 cursor-not-allowed bg-gray-50 border-gray-200'
: 'bg-white border-blue-600 hover:bg-blue-600 hover:text-white'
}`}
onClick={() => scrollToDirection('left')}
disabled={!canScrollLeft}
aria-label="Scroll testimonials left"
>
<ChevronLeft size={18} />
</button>
<button
className={`w-12 h-12 flex items-center justify-center rounded-lg border-2 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
!canScrollRight
? 'opacity-40 cursor-not-allowed bg-gray-50 border-gray-200'
: 'bg-white border-blue-600 hover:bg-blue-600 hover:text-white'
}`}
onClick={() => scrollToDirection('right')}
disabled={!canScrollRight}
aria-label="Scroll testimonials right"
>
<ChevronRight size={18} />
</button>
</div>
{/* Cards Container with Enhanced Design */}
<div className="relative">
{/* Scrollable Cards Container */}
<div
ref={scrollContainerRef}
className={`${isDragging ? 'cursor-grabbing' : 'cursor-grab'} flex overflow-x-auto gap-6 py-4 pb-6 scrollbar-hide`}
onScroll={handleScroll}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{
userSelect: 'none',
WebkitUserSelect: 'none',
msUserSelect: 'none',
MozUserSelect: 'none',
scrollbarWidth: 'none',
msOverflowStyle: 'none',
scrollBehavior: 'smooth'
}}
>
<div className="flex gap-6 px-2">
{testimonialsData.map((testimonial, index) => (
<TestimonialCard
key={testimonial.id || index}
testimonial={testimonial}
onPlayVideo={handlePlayVideo}
/>
))}
</div>
</div>
{/* Left Side White Fade Overlay */}
<div
className="absolute top-0 left-0 bottom-0 w-20 pointer-events-none z-5"
style={{
background: 'linear-gradient(to right, #FFFFFF, transparent)'
}}
/>
{/* Right Side Fade Gradient Overlay - Now properly positioned */}
<div
className="absolute top-0 right-0 bottom-0 w-20 pointer-events-none z-5"
style={{
background: 'linear-gradient(to right, transparent, #FFFFFF)'
}}
/>
</div>
</div>
</div>
{/* Video Modal */}
<VideoModal
isOpen={isVideoModalOpen}
onClose={handleCloseModal}
videoUrl={currentVideoUrl}
/>
<style>{`
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Enhanced testimonial card animations */
.testimonial-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.testimonial-card:hover {
transform: translateY(-4px);
}
`}</style>
</section>
);
}