import { useState, useEffect, useRef } from "react"; const Stats = () => { const [stats, setStats] = useState(null); const [error, setError] = useState(false); const [count, setCount] = useState(0); const [isVisible, setIsVisible] = useState(false); const [skipAnim, setSkipAnim] = useState(false); const hasAnimated = useRef(false); const sectionRef = useRef(null); // Fetch data on mount useEffect(() => { fetch("/api/wakatime/alltime") .then((res) => { if (!res.ok) throw new Error("API error"); return res.json(); }) .then((data) => setStats(data.data)) .catch(() => setError(true)); }, []); // Observe visibility — skip animation if already in view on mount useEffect(() => { const el = sectionRef.current; if (!el) return; const rect = el.getBoundingClientRect(); const inView = rect.top < window.innerHeight && rect.bottom > 0; const isReload = performance.getEntriesByType?.("navigation")?.[0]?.type === "reload"; if (inView && isReload) { setSkipAnim(true); setIsVisible(true); return; } if (inView) { requestAnimationFrame(() => setIsVisible(true)); return; } const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsVisible(true); observer.disconnect(); } }, { threshold: 0.3 } ); observer.observe(el); return () => observer.disconnect(); }, []); // Start counter when both visible and data is ready useEffect(() => { if (!isVisible || !stats || hasAnimated.current) return; hasAnimated.current = true; const totalSeconds = stats.total_seconds; const duration = 2000; const steps = 60; let currentStep = 0; const timer = setInterval(() => { currentStep += 1; if (currentStep >= steps) { setCount(totalSeconds); clearInterval(timer); return; } const progress = 1 - Math.pow(1 - currentStep / steps, 4); setCount(Math.floor(totalSeconds * progress)); }, duration / steps); return () => clearInterval(timer); }, [isVisible, stats]); if (error) return null; if (!stats) return
; const hours = Math.floor(count / 3600); const formattedHours = hours.toLocaleString("en-US", { minimumIntegerDigits: 4, useGrouping: true, }); return (
I've spent
{formattedHours} hours
writing code & building apps
since {stats.range.start_text}
); }; export default Stats;