michaelpeskov/components/SocialProof.tsx

200 lines
5.9 KiB
TypeScript

'use client'
import { useEffect, useRef } from 'react'
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
if (typeof window !== 'undefined') {
gsap.registerPlugin(ScrollTrigger)
}
const stats = [
{ number: 5, suffix: '/5', label: 'Star Rating' },
{ number: 100, suffix: '%', label: 'Recommendation' },
{ number: 20, suffix: '+', label: 'Bookings' },
{ number: 1000, suffix: '+', label: 'KM Travel Radius' }
]
const logos = [
'SAT.1',
'WDR',
'ZDF',
'Amazon Prime Video',
'Mercedes-Benz AG',
'Materna TMT',
'IHK',
'Lexus'
]
export default function SocialProof() {
const containerRef = useRef<HTMLDivElement>(null)
const statsRef = useRef<HTMLDivElement>(null)
const logosRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const container = containerRef.current
const statsContainer = statsRef.current
const logosContainer = logosRef.current
if (!container || !statsContainer || !logosContainer) return
// Check for reduced motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const ctx = gsap.context(() => {
// Stats animation
const statElements = gsap.utils.toArray<HTMLElement>('.stat-item')
statElements.forEach((stat, i) => {
const numberEl = stat.querySelector('.stat-number') as HTMLElement
const labelEl = stat.querySelector('.stat-label') as HTMLElement
if (prefersReducedMotion) {
// Simple fade for reduced motion
gsap.from(stat, {
opacity: 0,
y: 20,
duration: 0.6,
delay: i * 0.1,
scrollTrigger: {
trigger: statsContainer,
start: 'top 80%',
toggleActions: 'play none none reverse'
}
})
} else {
// Count-up animation
const targetNumber = parseInt(numberEl.textContent || '0')
const suffix = numberEl.dataset.suffix || ''
gsap.set(stat, { opacity: 0, y: 30 })
const tl = gsap.timeline({
scrollTrigger: {
trigger: statsContainer,
start: 'top 70%',
toggleActions: 'play none none reverse'
}
})
tl.to(stat, {
opacity: 1,
y: 0,
duration: 0.6,
delay: i * 0.1,
ease: 'power2.out'
})
.to({ value: 0 }, {
value: targetNumber,
duration: 1.5,
ease: 'power2.out',
onUpdate: function() {
const currentValue = Math.round(this.targets()[0].value)
numberEl.textContent = currentValue + suffix
}
}, '-=0.3')
}
})
// Logos animation
const logoElements = gsap.utils.toArray<HTMLElement>('.logo-item')
if (prefersReducedMotion) {
gsap.from(logoElements, {
opacity: 0,
duration: 0.8,
stagger: 0.1,
scrollTrigger: {
trigger: logosContainer,
start: 'top 80%',
toggleActions: 'play none none reverse'
}
})
} else {
gsap.from(logoElements, {
opacity: 0,
x: -30,
duration: 0.6,
stagger: 0.1,
ease: 'power2.out',
scrollTrigger: {
trigger: logosContainer,
start: 'top 80%',
toggleActions: 'play none none reverse'
}
})
// Continuous marquee effect
const marqueeWidth = logosContainer.scrollWidth
const containerWidth = logosContainer.offsetWidth
if (marqueeWidth > containerWidth) {
gsap.to('.logos-track', {
x: -(marqueeWidth - containerWidth),
duration: 20,
ease: 'none',
repeat: -1,
yoyo: true
})
}
}
}, container)
return () => ctx.revert()
}, [])
return (
<section ref={containerRef} className="py-32 px-6 bg-slate-900">
<div className="max-w-7xl mx-auto">
{/* Stats */}
<div ref={statsRef} className="mb-20">
<div className="text-center mb-16">
<h2 className="text-5xl md:text-6xl font-bold text-white mb-6">
Trusted by Many
</h2>
<p className="text-xl text-gray-300">
Numbers that speak for themselves
</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
{stats.map((stat, i) => (
<div key={i} className="stat-item text-center">
<div
className="stat-number text-4xl md:text-5xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400 mb-2"
data-suffix={stat.suffix}
>
0{stat.suffix}
</div>
<div className="stat-label text-gray-300 text-sm md:text-base">
{stat.label}
</div>
</div>
))}
</div>
</div>
{/* Logos */}
<div ref={logosRef} className="overflow-hidden">
<div className="text-center mb-12">
<h3 className="text-2xl font-semibold text-white mb-4">
As Seen On & Trusted By
</h3>
</div>
<div className="relative">
<div className="logos-track flex items-center justify-center gap-12 md:gap-16">
{logos.map((logo, i) => (
<div
key={i}
className="logo-item flex-shrink-0 text-gray-400 hover:text-white transition-colors duration-300 text-lg md:text-xl font-medium"
>
{logo}
</div>
))}
</div>
</div>
</div>
</div>
</section>
)
}