feat: use number ticker to display star count (#89)

This commit is contained in:
Henry Li 2025-05-12 23:15:43 +08:00 committed by GitHub
parent d5b6e3cf44
commit cadf6b5bcf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 73 additions and 5 deletions

View File

@ -55,7 +55,7 @@
"lowlight": "^3.3.0",
"lru-cache": "^11.1.0",
"lucide-react": "^0.487.0",
"motion": "^12.6.5",
"motion": "^12.7.4",
"nanoid": "^5.1.5",
"next": "^15.2.3",
"next-themes": "^0.4.6",

2
web/pnpm-lock.yaml generated
View File

@ -123,7 +123,7 @@ importers:
specifier: ^0.487.0
version: 0.487.0(react@19.1.0)
motion:
specifier: ^12.6.5
specifier: ^12.7.4
version: 12.7.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
nanoid:
specifier: ^5.1.5

View File

@ -4,6 +4,7 @@
import { StarFilledIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
import Link from "next/link";
import { NumberTicker } from "~/components/magicui/number-ticker";
import { Button } from "~/components/ui/button";
import { env } from "~/env";
@ -72,9 +73,9 @@ export async function StarCounter() {
return (
<>
<StarFilledIcon className="size-4 transition-colors duration-300 group-hover:text-yellow-500" />
<span className="font-mono tabular-nums">
{stars !== null ? stars.toLocaleString() : "—"}
</span>
{stars && (
<NumberTicker className="font-mono tabular-nums" value={stars} />
)}
</>
);
}

View File

@ -0,0 +1,67 @@
"use client";
import { useInView, useMotionValue, useSpring } from "motion/react";
import { ComponentPropsWithoutRef, useEffect, useRef } from "react";
import { cn } from "~/lib/utils";
interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> {
value: number;
startValue?: number;
direction?: "up" | "down";
delay?: number;
decimalPlaces?: number;
}
export function NumberTicker({
value,
startValue = 0,
direction = "up",
delay = 0,
className,
decimalPlaces = 0,
...props
}: NumberTickerProps) {
const ref = useRef<HTMLSpanElement>(null);
const motionValue = useMotionValue(direction === "down" ? value : startValue);
const springValue = useSpring(motionValue, {
damping: 60,
stiffness: 100,
});
const isInView = useInView(ref, { once: true, margin: "0px" });
useEffect(() => {
if (isInView) {
const timer = setTimeout(() => {
motionValue.set(direction === "down" ? startValue : value);
}, delay * 1000);
return () => clearTimeout(timer);
}
}, [motionValue, isInView, delay, value, direction, startValue]);
useEffect(
() =>
springValue.on("change", (latest) => {
if (ref.current) {
ref.current.textContent = Intl.NumberFormat("en-US", {
minimumFractionDigits: decimalPlaces,
maximumFractionDigits: decimalPlaces,
}).format(Number(latest.toFixed(decimalPlaces)));
}
}),
[springValue, decimalPlaces],
);
return (
<span
ref={ref}
className={cn(
"inline-block tabular-nums tracking-wider text-black dark:text-white",
className,
)}
{...props}
>
{startValue}
</span>
);
}