ed79b266af
- Created reusable AnimatedNumber component with GSAP - Integrated animated counters in Dashboard (metrics, weather) - Integrated animated counters in Leaderboard (podium, table) - Fixed AdminSchedule dark mode styling with theme-aware classes - Added responsive columns and functional dropdown to Team Schedule - Implemented date range and status filters for Team Schedule - Improved animation smoothness with expo.out easing
117 lines
3.9 KiB
React
117 lines
3.9 KiB
React
import React, { useEffect, useRef } from 'react';
|
|
import { gsap } from 'gsap';
|
|
|
|
/**
|
|
* AnimatedNumber - A reusable component that animates numbers from 0 to their target value using GSAP
|
|
* @param {number|string} value - The target value to animate to
|
|
* @param {number} duration - Animation duration in seconds (default: 2)
|
|
* @param {string} prefix - Optional prefix (e.g., "$")
|
|
* @param {string} suffix - Optional suffix (e.g., "%", "K", "M")
|
|
* @param {number} decimals - Number of decimal places (default: 0)
|
|
* @param {boolean} useLocaleString - Whether to format with commas (default: false)
|
|
* @param {string} className - Additional CSS classes
|
|
*/
|
|
const AnimatedNumber = ({
|
|
value,
|
|
duration = 2,
|
|
prefix = '',
|
|
suffix = '',
|
|
decimals = 0,
|
|
useLocaleString = false,
|
|
className = '',
|
|
delay = 0
|
|
}) => {
|
|
const numberRef = useRef(null);
|
|
const animationRef = useRef(null);
|
|
|
|
useEffect(() => {
|
|
if (!numberRef.current) return;
|
|
|
|
// Extract numeric value if it's a string like "$4.2M"
|
|
let numericValue = value;
|
|
let extractedPrefix = prefix;
|
|
let extractedSuffix = suffix;
|
|
|
|
if (typeof value === 'string') {
|
|
// Check for currency symbols
|
|
if (value.startsWith('$')) {
|
|
extractedPrefix = '$';
|
|
value = value.substring(1);
|
|
}
|
|
|
|
// Check for suffixes like M, K, B
|
|
const lastChar = value.slice(-1);
|
|
if (['K', 'M', 'B'].includes(lastChar.toUpperCase())) {
|
|
extractedSuffix = lastChar.toUpperCase();
|
|
value = value.slice(0, -1);
|
|
}
|
|
|
|
// Parse the numeric part
|
|
numericValue = parseFloat(value.replace(/,/g, ''));
|
|
}
|
|
|
|
// Handle non-numeric values
|
|
if (isNaN(numericValue)) {
|
|
numberRef.current.textContent = value;
|
|
return;
|
|
}
|
|
|
|
// Convert suffix multipliers to actual numbers
|
|
let multiplier = 1;
|
|
if (extractedSuffix === 'K') multiplier = 1000;
|
|
if (extractedSuffix === 'M') multiplier = 1000000;
|
|
if (extractedSuffix === 'B') multiplier = 1000000000;
|
|
|
|
const targetValue = numericValue * multiplier;
|
|
|
|
// Animate from 0 to target value
|
|
const obj = { val: 0 };
|
|
|
|
animationRef.current = gsap.to(obj, {
|
|
val: numericValue,
|
|
duration: duration,
|
|
delay: delay,
|
|
ease: 'expo.out',
|
|
onUpdate: () => {
|
|
if (numberRef.current) {
|
|
let displayValue = obj.val.toFixed(decimals);
|
|
|
|
if (useLocaleString) {
|
|
displayValue = parseFloat(displayValue).toLocaleString(undefined, {
|
|
minimumFractionDigits: decimals,
|
|
maximumFractionDigits: decimals
|
|
});
|
|
}
|
|
|
|
numberRef.current.textContent = `${extractedPrefix}${displayValue}${extractedSuffix}`;
|
|
}
|
|
},
|
|
onComplete: () => {
|
|
// Ensure final value is exact
|
|
if (numberRef.current) {
|
|
let displayValue = numericValue.toFixed(decimals);
|
|
|
|
if (useLocaleString) {
|
|
displayValue = parseFloat(displayValue).toLocaleString(undefined, {
|
|
minimumFractionDigits: decimals,
|
|
maximumFractionDigits: decimals
|
|
});
|
|
}
|
|
|
|
numberRef.current.textContent = `${extractedPrefix}${displayValue}${extractedSuffix}`;
|
|
}
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
if (animationRef.current) {
|
|
animationRef.current.kill();
|
|
}
|
|
};
|
|
}, [value, duration, prefix, suffix, decimals, useLocaleString, delay]);
|
|
|
|
return <span ref={numberRef} className={className}>0</span>;
|
|
};
|
|
|
|
export default AnimatedNumber;
|