feat: replace NumberFlow with custom AnimatedCount component for time display

This commit is contained in:
hamster1963 2025-02-18 17:57:18 +08:00
parent 084f71e4a6
commit 69ff5365d5
6 changed files with 156 additions and 19 deletions

View File

@ -1,11 +1,11 @@
"use client"
import AnimateCountClient from "@/components/AnimatedCount"
import { LanguageSwitcher } from "@/components/LanguageSwitcher"
import { ModeToggle } from "@/components/ThemeSwitcher"
import { Separator } from "@/components/ui/separator"
import getEnv from "@/lib/env-entry"
import NumberFlow, { NumberFlowGroup } from "@number-flow/react"
import { DateTime } from "luxon"
import { useTranslations } from "next-intl"
import { useRouter } from "next/navigation"
@ -75,24 +75,15 @@ const Overview = memo(function Overview() {
return (
<section className={"mt-10 flex flex-col md:mt-16"}>
<p className="text-base font-semibold">{t("p_2277-2331_Overview")}</p>
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1">
<p className="text-sm font-medium opacity-50">{t("p_2390-2457_wherethetimeis")}</p>
<NumberFlowGroup>
<div
style={{ fontVariantNumeric: "tabular-nums" }}
className="flex text-sm font-medium mt-0.5"
>
<NumberFlow trend={1} value={time.hh} format={{ minimumIntegerDigits: 2 }} />
<NumberFlow
prefix=":"
trend={1}
value={time.mm}
digits={{ 1: { max: 5 } }}
format={{ minimumIntegerDigits: 2 }}
/>
<p className="mt-[0.5px]">:{time.ss.toString().padStart(2, "0")}</p>
<div className="flex items-center text-sm font-medium">
<AnimateCountClient count={time.hh} minDigits={2} />
<span className="text-sm mb-[1px] font-medium opacity-50">:</span>
<AnimateCountClient count={time.mm} minDigits={2} />
<span className="text-sm mb-[1px] font-medium opacity-50">:</span>
<span className="text-sm font-medium">{time.ss.toString().padStart(2, "0")}</span>
</div>
</NumberFlowGroup>
</div>
</section>
)

View File

BIN
bun.lockb

Binary file not shown.

View File

@ -0,0 +1,96 @@
import { cn } from "@/lib/utils"
import { useEffect, useState } from "react"
export default function AnimateCountClient({
count,
className,
minDigits,
}: {
count: number
className?: string
minDigits?: number
}) {
const [previousCount, setPreviousCount] = useState(count)
useEffect(() => {
if (count !== previousCount) {
setTimeout(() => {
setPreviousCount(count)
}, 300)
}
}, [count])
return (
<AnimateCount
key={count}
preCount={previousCount}
className={cn("inline-flex items-center leading-none", className)}
minDigits={minDigits}
data-issues-count-animation
>
{count}
</AnimateCount>
)
}
export function AnimateCount({
children: count,
className,
preCount,
minDigits = 1,
...props
}: {
children: number
className?: string
preCount?: number
minDigits?: number
}) {
const currentDigits = count.toString().split("")
const previousDigits = (
preCount !== undefined ? preCount.toString() : count - 1 >= 0 ? (count - 1).toString() : "0"
).split("")
// Ensure both numbers meet the minimum length requirement and maintain the same length for animation
const maxLength = Math.max(previousDigits.length, currentDigits.length, minDigits)
while (previousDigits.length < maxLength) {
previousDigits.unshift("0")
}
while (currentDigits.length < maxLength) {
currentDigits.unshift("0")
}
return (
<div {...props} className={cn("flex h-[1em] items-center", className)}>
{currentDigits.map((digit, index) => {
const hasChanged = digit !== previousDigits[index]
return (
<div
key={`${index}-${digit}`}
className={cn("relative h-full flex items-center min-w-[0.6em] text-center", {
"min-w-[0.2em]": digit === ".",
})}
>
<div
aria-hidden
data-issues-count-exit
className={cn(
"absolute inset-0 flex items-center justify-center",
hasChanged ? "animate" : "opacity-0",
)}
>
{previousDigits[index]}
</div>
<div
data-issues-count-enter
className={cn(
"absolute inset-0 flex items-center justify-center",
hasChanged && "animate",
)}
>
{digit}
</div>
</div>
)
})}
</div>
)
}

View File

@ -15,7 +15,6 @@
"dependencies": {
"@ducanh2912/next-pwa": "^10.2.9",
"@heroicons/react": "^2.2.0",
"@number-flow/react": "^0.5.5",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",

View File

@ -146,6 +146,7 @@
--chart-8: 252 50% 50%;
--chart-9: 288 50% 50%;
--chart-10: 324 50% 50%;
--timing: cubic-bezier(0.4, 0, 0.2, 1);
}
.dark {
@ -334,3 +335,53 @@
filter: blur(0px);
}
}
/* Thanks to next.js. */
[data-issues-count-animation] {
display: flex;
justify-content: center;
align-items: center;
}
[data-issues-count-animation] > div {
text-align: center;
}
[data-issues-count-exit].animate {
animation: fadeOut 300ms var(--timing) forwards;
}
[data-issues-count-enter].animate {
animation: fadeIn 300ms var(--timing) forwards;
}
[data-issues-count-plural] {
display: inline-block;
animation: fadeIn 300ms var(--timing) forwards;
}
@keyframes fadeIn {
0% {
opacity: 0;
filter: blur(2px);
transform: translateY(8px);
}
100% {
opacity: 1;
filter: blur(0px);
transform: translateY(0);
}
}
@keyframes fadeOut {
0% {
opacity: 1;
filter: blur(0px);
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-12px);
filter: blur(2px);
}
}