200 lines
5.9 KiB
TypeScript
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>
|
|
)
|
|
} |