532 lines
18 KiB
TypeScript
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>
|
|
);
|
|
} |