MVp
This commit is contained in:
200
components/SocialProof.tsx
Normal file
200
components/SocialProof.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user