111 lines
3.0 KiB
TypeScript
Vendored
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>
|
|
)
|
|
}
|