|
| 1 | +'use client'; |
| 2 | + |
| 3 | +import { Testimonial } from '@/components/testimonials/data/testimonial'; |
| 4 | +import { useRef, useEffect } from 'react'; |
| 5 | +import { Linkedin } from 'lucide-react'; |
| 6 | +import { BoundlessButton } from '../buttons'; |
| 7 | +import { gsap } from 'gsap'; |
| 8 | +import TestimonialCard from './TestimonialCard'; |
| 9 | + |
| 10 | +type TestimonialsWallProps = { |
| 11 | + testimonials: Testimonial[]; |
| 12 | +}; |
| 13 | + |
| 14 | +export default function TestimonialsSection({ |
| 15 | + testimonials, |
| 16 | +}: TestimonialsWallProps) { |
| 17 | + const scrollContainerRef = useRef<HTMLDivElement>(null); |
| 18 | + const columnRefs = useRef<HTMLDivElement[]>([]); |
| 19 | + const animations = useRef<gsap.core.Tween[]>([]); |
| 20 | + |
| 21 | + useEffect(() => { |
| 22 | + // Kill previous animations |
| 23 | + animations.current.forEach(anim => anim.kill()); |
| 24 | + animations.current = []; |
| 25 | + |
| 26 | + columnRefs.current.forEach((column, index) => { |
| 27 | + if (!column) return; |
| 28 | + |
| 29 | + const wrapper = column.querySelector<HTMLDivElement>('.scroll-wrapper'); |
| 30 | + if (!wrapper) return; |
| 31 | + |
| 32 | + const children = Array.from(wrapper.children) as HTMLElement[]; |
| 33 | + const totalHeight = children.reduce( |
| 34 | + (acc, child) => acc + child.offsetHeight + 32, |
| 35 | + 0 |
| 36 | + ); |
| 37 | + |
| 38 | + // Clone and append to create the continuous loop |
| 39 | + const clone = wrapper.cloneNode(true) as HTMLElement; |
| 40 | + wrapper.appendChild(clone); |
| 41 | + |
| 42 | + gsap.set(wrapper, { y: 0, force3D: true, willChange: 'transform' }); |
| 43 | + |
| 44 | + const anim = gsap.to(wrapper, { |
| 45 | + y: -totalHeight, |
| 46 | + duration: 30, // Slower for better readability |
| 47 | + ease: 'none', // Linear for consistent speed |
| 48 | + repeat: -1, // Infinite repeat |
| 49 | + // Advanced performance optimizations |
| 50 | + force3D: true, // Enable hardware acceleration |
| 51 | + transformOrigin: 'center center', |
| 52 | + // Use GSAP's built-in performance features |
| 53 | + immediateRender: false, // Don't render immediately |
| 54 | + lazy: false, // Don't use lazy rendering for smooth animation |
| 55 | + delay: index * 0.5, |
| 56 | + }); |
| 57 | + |
| 58 | + animations.current.push(anim); |
| 59 | + }); |
| 60 | + |
| 61 | + const container = scrollContainerRef.current; |
| 62 | + if (!container) return; |
| 63 | + |
| 64 | + const handleMouseEnter = () => |
| 65 | + animations.current.forEach(anim => anim.pause()); |
| 66 | + const handleMouseLeave = () => |
| 67 | + animations.current.forEach(anim => anim.resume()); |
| 68 | + |
| 69 | + container.addEventListener('mouseenter', handleMouseEnter); |
| 70 | + container.addEventListener('mouseleave', handleMouseLeave); |
| 71 | + |
| 72 | + return () => { |
| 73 | + container.removeEventListener('mouseenter', handleMouseEnter); |
| 74 | + container.removeEventListener('mouseleave', handleMouseLeave); |
| 75 | + animations.current.forEach(anim => anim.kill()); |
| 76 | + }; |
| 77 | + }, [testimonials]); |
| 78 | + |
| 79 | + return ( |
| 80 | + <section |
| 81 | + className='relative py-20 text-white overflow-hidden bg-[#030303]' |
| 82 | + aria-labelledby='testimonials-heading' |
| 83 | + > |
| 84 | + <div className='w-full'> |
| 85 | + <h2 id='testimonials-heading' className='sr-only'> |
| 86 | + What our users say |
| 87 | + </h2> |
| 88 | + |
| 89 | + <div className='relative overflow-hidden shadow-2xl backdrop-blur-sm max-h-[600px]'> |
| 90 | + <div className='absolute inset-0 bg-[#CCFF95] opacity-95'></div> |
| 91 | + <div className='absolute inset-0 overflow-hidden pointer-events-none'> |
| 92 | + <div className='absolute top-10 left-10 w-2 h-2 bg-[#CCFF95] rounded-full opacity-60 animate-pulse'></div> |
| 93 | + <div className='absolute top-20 right-20 w-1 h-1 bg-primary rounded-full opacity-40 animate-ping'></div> |
| 94 | + <div className='absolute bottom-16 left-1/3 w-1.5 h-1.5 bg-white rounded-full opacity-30 animate-pulse'></div> |
| 95 | + <div className='absolute bottom-32 right-1/4 w-1 h-1 bg-[#CCFF95] rounded-full opacity-50 animate-ping'></div> |
| 96 | + </div> |
| 97 | + |
| 98 | + <div |
| 99 | + className='absolute top-0 left-0 w-full h-[150px] z-10 pointer-events-none' |
| 100 | + style={{ |
| 101 | + background: |
| 102 | + 'linear-gradient(0deg, rgba(3, 3, 3, 0.00) 0%, #030303 100%)', |
| 103 | + }} |
| 104 | + /> |
| 105 | + <div |
| 106 | + className='absolute bottom-0 left-0 w-full h-[150px] z-10 pointer-events-none' |
| 107 | + style={{ |
| 108 | + background: |
| 109 | + 'linear-gradient(180deg, rgba(3, 3, 3, 0.00) 0%, #030303 100%)', |
| 110 | + }} |
| 111 | + /> |
| 112 | + |
| 113 | + <div |
| 114 | + ref={scrollContainerRef} |
| 115 | + className='relative grid grid-cols-1 md:grid-cols-4 gap-8 p-6 z-10' |
| 116 | + style={{ |
| 117 | + willChange: 'transform', |
| 118 | + backfaceVisibility: 'hidden', |
| 119 | + perspective: '1000px', |
| 120 | + }} |
| 121 | + > |
| 122 | + {[0, 1, 2, 3].map(colIdx => ( |
| 123 | + <div |
| 124 | + key={colIdx} |
| 125 | + ref={el => { |
| 126 | + if (el) columnRefs.current[colIdx] = el; |
| 127 | + }} |
| 128 | + className='flex flex-col gap-8 overflow-hidden' |
| 129 | + > |
| 130 | + <div className='scroll-wrapper flex flex-col gap-8'> |
| 131 | + {testimonials.slice(colIdx * 5, colIdx * 5 + 5).map(t => ( |
| 132 | + <TestimonialCard |
| 133 | + key={t.id} |
| 134 | + avatarSrc={t.avatarUrl || '/avatar-placeholder.png'} |
| 135 | + avatarFallback={t.name.charAt(0)} |
| 136 | + name={t.name} |
| 137 | + username={t.username || 'username'} |
| 138 | + content={t.message} |
| 139 | + icon={ |
| 140 | + <Linkedin |
| 141 | + size={16} |
| 142 | + className={ |
| 143 | + colIdx % 2 === 0 ? 'text-[#CCFF95]' : 'text-primary' |
| 144 | + } |
| 145 | + /> |
| 146 | + } |
| 147 | + /> |
| 148 | + ))} |
| 149 | + </div> |
| 150 | + </div> |
| 151 | + ))} |
| 152 | + </div> |
| 153 | + </div> |
| 154 | + <div className='absolute lg:top-[70%] top-24 lg:mt-0 mt-20 left-1/2 -translate-x-1/2 w-[90%] md:w-full max-w-6xl bg-primary rounded-xl shadow-lg p-8 md:p-12 text-black z-40'> |
| 155 | + <div className='flex flex-col md:flex-row items-center md:justify-between gap-6'> |
| 156 | + <div> |
| 157 | + <h2 className='text-3xl md:text-4xl font-bold leading-tight md:text-left text-center mb-2'> |
| 158 | + Shape the Future <br /> With Us |
| 159 | + </h2> |
| 160 | + </div> |
| 161 | + <div className='flex flex-col gap-4'> |
| 162 | + <p className='text-base md:text-left text-center font-normal text-gray-800 max-w-md'> |
| 163 | + We're building a future where ideas are truly boundless, |
| 164 | + unlocking opportunities for innovators. Be part of it! |
| 165 | + </p> |
| 166 | + <div className='flex md:flex-row flex-col gap-2'> |
| 167 | + <BoundlessButton |
| 168 | + variant='secondary' |
| 169 | + className='bg-background hover:text-background' |
| 170 | + size='xl' |
| 171 | + fullWidth |
| 172 | + aria-label='Explore projects' |
| 173 | + > |
| 174 | + Explore Projects |
| 175 | + </BoundlessButton> |
| 176 | + <BoundlessButton |
| 177 | + variant='ghost' |
| 178 | + size='xl' |
| 179 | + fullWidth |
| 180 | + className='bg-transparent border-2 border-black' |
| 181 | + aria-label='Subscribe for updates' |
| 182 | + > |
| 183 | + Subscribe for Updates |
| 184 | + </BoundlessButton> |
| 185 | + </div> |
| 186 | + </div> |
| 187 | + </div> |
| 188 | + </div> |
| 189 | + </div> |
| 190 | + </section> |
| 191 | + ); |
| 192 | +} |
0 commit comments