SEO/AEO
This commit is contained in:
89
src/hooks/use-countup.ts
Normal file
89
src/hooks/use-countup.ts
Normal 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);
|
||||
};
|
||||
Reference in New Issue
Block a user