Spaces:
Sleeping
Sleeping
| import React, { useEffect, useRef, useState } from 'react'; | |
| export default function StatsCounter({ value, label, duration = 1200, suffix = '', accent = false, decimals = 0 }) { | |
| const [display, setDisplay] = useState(0); | |
| const elRef = useRef(null); | |
| const startedRef = useRef(false); | |
| useEffect(() => { | |
| const el = elRef.current; | |
| if (!el) return; | |
| const onEnter = (entries) => { | |
| entries.forEach((entry) => { | |
| if (entry.isIntersecting && !startedRef.current) { | |
| startedRef.current = true; | |
| const start = performance.now(); | |
| const target = Number(value); | |
| const step = (t) => { | |
| const p = Math.min(1, (t - start) / duration); | |
| const raw = p * target; | |
| if (decimals > 0) { | |
| setDisplay(Number(raw.toFixed(decimals))); | |
| } else { | |
| setDisplay(Math.floor(raw)); | |
| } | |
| if (p < 1) requestAnimationFrame(step); | |
| }; | |
| requestAnimationFrame(step); | |
| } | |
| }); | |
| }; | |
| const obs = new IntersectionObserver(onEnter, { threshold: 0.4 }); | |
| obs.observe(el); | |
| return () => obs.disconnect(); | |
| }, [value, duration]); | |
| const formatNumber = (n) => { | |
| if (decimals > 0) { | |
| // Keep fixed decimals but still use locale for thousands if ever needed | |
| return Number(n).toLocaleString(undefined, { | |
| minimumFractionDigits: decimals, | |
| maximumFractionDigits: decimals | |
| }); | |
| } | |
| return Number(n).toLocaleString(); | |
| }; | |
| const counterContent = ( | |
| <span className="text-4xl font-extrabold text-brand-700"> | |
| {formatNumber(display)}{suffix} | |
| </span> | |
| ); | |
| return ( | |
| <div ref={elRef} className="text-center"> | |
| {accent ? ( | |
| <div className="inline-block rounded-full bg-accent/20 px-6 py-2 shadow-[0_0_18px_rgba(211,155,35,0.35)] backdrop-blur-sm"> | |
| {counterContent} | |
| </div> | |
| ) : ( | |
| counterContent | |
| )} | |
| <div className="mt-3 text-sm text-slate-600">{label}</div> | |
| </div> | |
| ); | |
| } | |