mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 22:26:38 +08:00
144 lines
4.1 KiB
TypeScript
144 lines
4.1 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { type SpringOptions, useMotionValue, useSpring } from 'motion/react';
|
|
|
|
import { useIsInView, type UseIsInViewOptions } from '@/hooks/use-is-in-view';
|
|
|
|
type CountingNumberProps = Omit<React.ComponentProps<'span'>, 'children'> & {
|
|
number: number;
|
|
fromNumber?: number;
|
|
padStart?: boolean;
|
|
decimalSeparator?: string;
|
|
decimalPlaces?: number;
|
|
transition?: SpringOptions;
|
|
delay?: number;
|
|
initiallyStable?: boolean;
|
|
thousandSeparator?: string;
|
|
} & UseIsInViewOptions;
|
|
|
|
function CountingNumber({
|
|
ref,
|
|
number,
|
|
fromNumber = 0,
|
|
padStart = false,
|
|
inView = false,
|
|
inViewMargin = '0px',
|
|
inViewOnce = true,
|
|
decimalSeparator = '.',
|
|
transition = { stiffness: 200, damping: 30 },
|
|
decimalPlaces = 0,
|
|
delay = 0,
|
|
initiallyStable = false,
|
|
thousandSeparator = '',
|
|
...props
|
|
}: CountingNumberProps) {
|
|
const { ref: localRef, isInView } = useIsInView(
|
|
ref as React.Ref<HTMLElement>,
|
|
{
|
|
inView,
|
|
inViewOnce,
|
|
inViewMargin,
|
|
},
|
|
);
|
|
|
|
const numberStr = number.toString();
|
|
const decimals =
|
|
typeof decimalPlaces === 'number'
|
|
? decimalPlaces
|
|
: numberStr.includes('.')
|
|
? (numberStr.split('.')[1]?.length ?? 0)
|
|
: 0;
|
|
|
|
const motionVal = useMotionValue(initiallyStable ? number : fromNumber);
|
|
const springVal = useSpring(motionVal, transition);
|
|
|
|
React.useEffect(() => {
|
|
const timeoutId = setTimeout(() => {
|
|
if (isInView) motionVal.set(number);
|
|
}, delay);
|
|
|
|
return () => clearTimeout(timeoutId);
|
|
}, [isInView, number, motionVal, delay]);
|
|
|
|
React.useEffect(() => {
|
|
const unsubscribe = springVal.on('change', (latest) => {
|
|
if (localRef.current) {
|
|
let formatted =
|
|
decimals > 0
|
|
? latest.toFixed(decimals)
|
|
: Math.round(latest).toString();
|
|
|
|
if (decimals > 0) {
|
|
formatted = formatted.replace('.', decimalSeparator);
|
|
}
|
|
|
|
if (padStart) {
|
|
const finalIntLength = Math.floor(Math.abs(number)).toString().length;
|
|
const [intPart, fracPart] = formatted.split(decimalSeparator);
|
|
const paddedInt = intPart?.padStart(finalIntLength, '0') ?? '';
|
|
formatted = fracPart
|
|
? `${paddedInt}${decimalSeparator}${fracPart}`
|
|
: paddedInt;
|
|
}
|
|
|
|
if (thousandSeparator) {
|
|
const [intPart, fracPart] = formatted.split(decimalSeparator);
|
|
const regex = /\B(?=(\d{3})+(?!\d))/g;
|
|
const separatedInt = intPart.replace(regex, thousandSeparator);
|
|
formatted = fracPart
|
|
? `${separatedInt}${decimalSeparator}${fracPart}`
|
|
: separatedInt;
|
|
}
|
|
|
|
localRef.current.textContent = formatted;
|
|
}
|
|
});
|
|
return () => unsubscribe();
|
|
}, [
|
|
springVal,
|
|
decimals,
|
|
padStart,
|
|
number,
|
|
decimalSeparator,
|
|
localRef,
|
|
thousandSeparator,
|
|
]);
|
|
|
|
const finalIntLength = Math.floor(Math.abs(number)).toString().length;
|
|
|
|
const formatValue = (val: number) => {
|
|
let out = decimals > 0 ? val.toFixed(decimals) : Math.round(val).toString();
|
|
if (decimals > 0) out = out.replace('.', decimalSeparator);
|
|
if (padStart) {
|
|
const [intPart, fracPart] = out.split(decimalSeparator);
|
|
const paddedInt = (intPart ?? '').padStart(finalIntLength, '0');
|
|
out = fracPart ? `${paddedInt}${decimalSeparator}${fracPart}` : paddedInt;
|
|
}
|
|
if (thousandSeparator) {
|
|
const [intPart, fracPart] = out.split(decimalSeparator);
|
|
const regex = /\B(?=(\d{3})+(?!\d))/g;
|
|
const separatedInt = intPart.replace(regex, thousandSeparator);
|
|
out = fracPart
|
|
? `${separatedInt}${decimalSeparator}${fracPart}`
|
|
: separatedInt;
|
|
}
|
|
return out;
|
|
};
|
|
|
|
const zeroText = padStart
|
|
? '0'.padStart(finalIntLength, '0') +
|
|
(decimals > 0 ? decimalSeparator + '0'.repeat(decimals) : '')
|
|
: '0' + (decimals > 0 ? decimalSeparator + '0'.repeat(decimals) : '');
|
|
|
|
const initialText = initiallyStable ? formatValue(number) : zeroText;
|
|
|
|
return (
|
|
<span ref={localRef} data-slot='counting-number' {...props}>
|
|
{initialText}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export { CountingNumber, type CountingNumberProps };
|