| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110 |
- import { useRef, useEffect, useCallback } from 'react'
- import { useTranslation } from 'react-i18next'
- interface CounterProps {
- end: number
- suffix?: string
- prefix?: string
- duration?: number
- decimals?: number
- }
- function Counter(props: CounterProps) {
- const { end, suffix = '', prefix = '', duration = 1600, decimals = 0 } = props
- const ref = useRef<HTMLSpanElement>(null)
- const startedRef = useRef(false)
- const formatValue = useCallback(
- (v: number) =>
- decimals > 0 ? v.toFixed(decimals) : Math.round(v).toLocaleString(),
- [decimals]
- )
- const animate = useCallback(() => {
- const el = ref.current
- if (!el) return
- const start = performance.now()
- const step = (now: number) => {
- const progress = Math.min((now - start) / duration, 1)
- const eased = 1 - Math.pow(1 - progress, 3)
- el.textContent = `${prefix}${formatValue(eased * end)}${suffix}`
- if (progress < 1) requestAnimationFrame(step)
- }
- requestAnimationFrame(step)
- }, [end, duration, prefix, suffix, formatValue])
- useEffect(() => {
- const el = ref.current
- if (!el) return
- const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
- if (mq.matches) {
- el.textContent = `${prefix}${formatValue(end)}${suffix}`
- return
- }
- const observer = new IntersectionObserver(
- ([entry]) => {
- if (entry.isIntersecting && !startedRef.current) {
- startedRef.current = true
- animate()
- observer.unobserve(el)
- }
- },
- { threshold: 0.5 }
- )
- observer.observe(el)
- return () => observer.disconnect()
- }, [animate, end, prefix, suffix, formatValue])
- return (
- <span ref={ref} className='tabular-nums'>
- {prefix}0{suffix}
- </span>
- )
- }
- interface StatsProps {
- className?: string
- }
- interface StatItem {
- end: number
- suffix: string
- label: string
- decimals?: number
- }
- export function Stats(_props: StatsProps) {
- const { t } = useTranslation()
- const stats: StatItem[] = [
- { end: 50, suffix: '+', label: t('upstream services integrated') },
- { end: 100, suffix: '+', label: t('model billing support') },
- { end: 50, suffix: '+', label: t('compatible API routes') },
- { end: 10, suffix: '+', label: t('scheduling controls') },
- ]
- return (
- <div className='border-border/40 bg-muted/10 relative z-10 border-y'>
- <div className='mx-auto max-w-6xl px-6 py-10 md:py-12'>
- <div className='grid grid-cols-2 gap-8 md:grid-cols-4 md:gap-12'>
- {stats.map((s) => (
- <div
- key={s.label}
- className='flex flex-col items-center text-center'
- >
- <span className='text-2xl font-bold tracking-tight md:text-3xl'>
- <Counter end={s.end} suffix={s.suffix} decimals={s.decimals} />
- </span>
- <span className='text-muted-foreground mt-1.5 text-xs'>
- {s.label}
- </span>
- </div>
- ))}
- </div>
- </div>
- </div>
- )
- }
|