This commit is contained in:
2025-09-30 23:10:13 +02:00
parent a646542d8f
commit 9938c1f9e2
27 changed files with 2158 additions and 498 deletions

89
src/hooks/use-countup.ts Normal file
View File

@@ -0,0 +1,89 @@
import { useState, useEffect, useRef } from 'react';
interface UseCountUpOptions {
end: number;
duration?: number;
decimals?: number;
startOnInView?: boolean;
}
export const useCountUp = ({ end, duration = 2000, decimals = 0, startOnInView = true }: UseCountUpOptions) => {
const [count, setCount] = useState(0);
const [isVisible, setIsVisible] = useState(false);
const [hasStarted, setHasStarted] = useState(false);
const elementRef = useRef<HTMLElement>(null);
// Intersection Observer for triggering animation when element comes into view
useEffect(() => {
if (!startOnInView) {
setHasStarted(true);
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !hasStarted) {
setIsVisible(true);
setHasStarted(true);
}
},
{ threshold: 0.1 }
);
if (elementRef.current) {
observer.observe(elementRef.current);
}
return () => observer.disconnect();
}, [startOnInView, hasStarted]);
// Counter animation
useEffect(() => {
if (!hasStarted) return;
let startTime: number;
let animationFrame: number;
const animate = (currentTime: number) => {
if (!startTime) startTime = currentTime;
const progress = Math.min((currentTime - startTime) / duration, 1);
// Easing function for smooth animation
const easeOutCubic = 1 - Math.pow(1 - progress, 3);
const currentCount = end * easeOutCubic;
setCount(currentCount);
if (progress < 1) {
animationFrame = requestAnimationFrame(animate);
}
};
animationFrame = requestAnimationFrame(animate);
return () => {
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
};
}, [end, duration, hasStarted]);
const formattedCount = count.toFixed(decimals);
return { count: formattedCount, elementRef };
};
// Utility function to parse numbers from strings like "150+", "99.9%", "<2min"
export const parseNumberFromString = (value: string): number => {
const numericMatch = value.match(/(\d+\.?\d*)/);
return numericMatch ? parseFloat(numericMatch[1]) : 0;
};
// Utility function to format the final value with original suffix/prefix
export const formatWithOriginalString = (originalValue: string, animatedNumber: string): string => {
const numericMatch = originalValue.match(/(\d+\.?\d*)/);
if (!numericMatch) return originalValue;
const originalNumber = numericMatch[1];
return originalValue.replace(originalNumber, animatedNumber);
};