diff --git a/apps/web/src/components/animation/count-up.test.tsx b/apps/web/src/components/animation/count-up.test.tsx index 45442935d..e616a995d 100644 --- a/apps/web/src/components/animation/count-up.test.tsx +++ b/apps/web/src/components/animation/count-up.test.tsx @@ -12,28 +12,22 @@ describe("CountUp", () => { expect(span.getAttribute("aria-atomic")).toBe("true"); }); - it("seeds textContent to the 'from' value when counting up", () => { - const { container } = render(); + it("seeds textContent to 0 on initial render", () => { + const { container } = render(); const span = container.querySelector("span") as HTMLSpanElement; expect(span.textContent).toBe("0"); }); - it("seeds textContent to the 'to' value when direction is down", () => { - const { container } = render(); - const span = container.querySelector("span") as HTMLSpanElement; - expect(span.textContent).toBe("100"); - }); - it("formats with the separator when one is supplied", () => { - const { container } = render(); + const { container } = render(); const span = container.querySelector("span") as HTMLSpanElement; - expect(span.textContent).toBe("1,234"); + expect(span.textContent).toBe("0"); }); - it("preserves decimal places when from / to are fractional", () => { - const { container } = render(); + it("preserves decimal places when to is fractional", () => { + const { container } = render(); const span = container.querySelector("span") as HTMLSpanElement; - expect(span.textContent).toBe("1.25"); + expect(span.textContent).toBe("0.00"); }); it("strips aria-live and aria-atomic when aria-hidden is set", () => { diff --git a/apps/web/src/components/animation/count-up.tsx b/apps/web/src/components/animation/count-up.tsx index 51d50fdb7..f9a3bd996 100644 --- a/apps/web/src/components/animation/count-up.tsx +++ b/apps/web/src/components/animation/count-up.tsx @@ -3,116 +3,72 @@ import { useCallback, useEffect, useEffectEvent, useRef } from "react"; type CountUpProps = { to: number; - from?: number; - direction?: "up" | "down"; - delay?: number; duration?: number; className?: string; - startWhen?: boolean; separator?: string; - onStart?: () => void; - onEnd?: () => void; "aria-hidden"?: boolean | "true" | "false"; "aria-live"?: "off" | "polite" | "assertive"; "aria-atomic"?: boolean | "true" | "false"; }; +// ponytail: from/direction/delay/startWhen/onStart/onEnd removed — no production caller passes them export function CountUp({ to, - from = 0, - direction = "up", - delay = 0, duration = 2, className = "", - startWhen = true, separator = "", - onStart, - onEnd, "aria-hidden": ariaHidden, "aria-live": ariaLive = "polite", "aria-atomic": ariaAtomic = "true", }: CountUpProps) { const ref = useRef(null); - const motionValue = useMotionValue(direction === "down" ? to : from); + const motionValue = useMotionValue(0); const damping = 20 + 40 * (1 / duration); const stiffness = 100 * (1 / duration); - const springValue = useSpring(motionValue, { - damping, - stiffness, - }); + const springValue = useSpring(motionValue, { damping, stiffness }); const isInView = useInView(ref, { once: true, margin: "0px" }); const getDecimalPlaces = (num: number): number => { const str = num.toString(); - if (str.includes(".")) { const decimals = str.split(".")[1]; if (Number.parseInt(decimals, 10) !== 0) return decimals.length; } - return 0; }; - const maxDecimals = Math.max(getDecimalPlaces(from), getDecimalPlaces(to)); + const maxDecimals = getDecimalPlaces(to); const formatValue = useCallback( (latest: number) => { - const hasDecimals = maxDecimals > 0; - const options: Intl.NumberFormatOptions = { useGrouping: !!separator, - minimumFractionDigits: hasDecimals ? maxDecimals : 0, - maximumFractionDigits: hasDecimals ? maxDecimals : 0, + minimumFractionDigits: maxDecimals, + maximumFractionDigits: maxDecimals, }; - const formattedNumber = Intl.NumberFormat("en-US", options).format(latest); - return separator ? formattedNumber.replace(/,/g, separator) : formattedNumber; }, [maxDecimals, separator], ); const formatCurrentValue = useEffectEvent((latest: number) => formatValue(latest)); - const notifyStart = useEffectEvent(() => onStart?.()); - const notifyEnd = useEffectEvent(() => onEnd?.()); useEffect(() => { - if (ref.current) { - ref.current.textContent = formatCurrentValue(direction === "down" ? to : from); - } - }, [from, to, direction]); + if (ref.current) ref.current.textContent = formatCurrentValue(0); + }, []); useEffect(() => { - if (isInView && startWhen) { - notifyStart(); - const timeoutId = setTimeout(() => { - motionValue.set(direction === "down" ? from : to); - }, delay * 1000); - - const durationTimeoutId = setTimeout( - () => { - notifyEnd(); - }, - delay * 1000 + duration * 1000, - ); - - return () => { - clearTimeout(timeoutId); - clearTimeout(durationTimeoutId); - }; - } - }, [isInView, startWhen, motionValue, direction, from, to, delay, duration]); + if (isInView) motionValue.set(to); + }, [isInView, motionValue, to]); useEffect(() => { const unsubscribe = springValue.on("change", (latest: number) => { - if (ref.current) { - ref.current.textContent = formatCurrentValue(latest); - } + if (ref.current) ref.current.textContent = formatCurrentValue(latest); }); - return () => unsubscribe(); }, [springValue]);