Fertig
This commit is contained in:
86
components/count-up-stat.tsx
Normal file
86
components/count-up-stat.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { animate, useInView, useReducedMotion } from "framer-motion";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
type CountUpStatProps = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type ParsedValue = {
|
||||
prefix: string;
|
||||
target: number;
|
||||
suffix: string;
|
||||
};
|
||||
|
||||
const smoothEase = [0.22, 1, 0.36, 1] as const;
|
||||
|
||||
function parseValue(value: string): ParsedValue | null {
|
||||
const match = value.match(/^([^0-9]*)([\d,]+)(.*)$/);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [, prefix, rawNumber, suffix] = match;
|
||||
const target = Number.parseInt(rawNumber.replace(/,/g, ""), 10);
|
||||
|
||||
if (Number.isNaN(target)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { prefix, target, suffix };
|
||||
}
|
||||
|
||||
function formatValue(parsed: ParsedValue, current: number) {
|
||||
return `${parsed.prefix}${new Intl.NumberFormat("en-US").format(current)}${parsed.suffix}`;
|
||||
}
|
||||
|
||||
export function CountUpStat({ value, label }: CountUpStatProps) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const isInView = useInView(ref, { once: true, amount: 0.45 });
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const parsed = useMemo(() => parseValue(value), [value]);
|
||||
const [displayValue, setDisplayValue] = useState(() =>
|
||||
parsed ? formatValue(parsed, 0) : value,
|
||||
);
|
||||
const hasAnimated = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!parsed) {
|
||||
setDisplayValue(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isInView || hasAnimated.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasAnimated.current = true;
|
||||
|
||||
if (shouldReduceMotion) {
|
||||
setDisplayValue(formatValue(parsed, parsed.target));
|
||||
return;
|
||||
}
|
||||
|
||||
const controls = animate(0, parsed.target, {
|
||||
duration: 1.4,
|
||||
ease: smoothEase,
|
||||
onUpdate(latest) {
|
||||
setDisplayValue(formatValue(parsed, Math.round(latest)));
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
controls.stop();
|
||||
};
|
||||
}, [isInView, parsed, shouldReduceMotion, value]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="stat-item">
|
||||
<span className="stat-value">{displayValue}</span>
|
||||
<span className="stat-label">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user