Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
94 lines
2.2 KiB
TypeScript
94 lines
2.2 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
|
|
|
interface AnimatedNumberProps {
|
|
value: number
|
|
duration?: number
|
|
className?: string
|
|
}
|
|
|
|
export function AnimatedNumber({ value, duration = 2000, className = '' }: AnimatedNumberProps) {
|
|
const [displayValue, setDisplayValue] = useState(0)
|
|
const [isVisible, setIsVisible] = useState(false)
|
|
const elementRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting && !isVisible) {
|
|
setIsVisible(true)
|
|
}
|
|
})
|
|
},
|
|
{ threshold: 0.1 }
|
|
)
|
|
|
|
if (elementRef.current) {
|
|
observer.observe(elementRef.current)
|
|
}
|
|
|
|
return () => {
|
|
if (elementRef.current) {
|
|
observer.unobserve(elementRef.current)
|
|
}
|
|
}
|
|
}, [isVisible])
|
|
|
|
useEffect(() => {
|
|
if (!isVisible || value === 0) {
|
|
setDisplayValue(0)
|
|
return
|
|
}
|
|
|
|
const startTime = Date.now()
|
|
const startValue = 0
|
|
const endValue = value
|
|
|
|
const animate = () => {
|
|
const now = Date.now()
|
|
const elapsed = now - startTime
|
|
const progress = Math.min(elapsed / duration, 1)
|
|
|
|
// Easing function for bounce effect (ease-out-bounce)
|
|
const easeOutBounce = (t: number): number => {
|
|
if (t < 1 / 2.75) {
|
|
return 7.5625 * t * t
|
|
} else if (t < 2 / 2.75) {
|
|
return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75
|
|
} else if (t < 2.5 / 2.75) {
|
|
return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375
|
|
} else {
|
|
return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375
|
|
}
|
|
}
|
|
|
|
const easedProgress = easeOutBounce(progress)
|
|
const currentValue = Math.floor(startValue + (endValue - startValue) * easedProgress)
|
|
|
|
setDisplayValue(currentValue)
|
|
|
|
if (progress < 1) {
|
|
requestAnimationFrame(animate)
|
|
} else {
|
|
setDisplayValue(endValue)
|
|
}
|
|
}
|
|
|
|
const frameId = requestAnimationFrame(animate)
|
|
return () => cancelAnimationFrame(frameId)
|
|
}, [isVisible, value, duration])
|
|
|
|
return (
|
|
<div ref={elementRef} className={className}>
|
|
{displayValue}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|