111 lines
3.0 KiB
TypeScript
Vendored

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>
)
}