feat: implement animated count component and update header for real-time display

This commit is contained in:
hamster1963
2025-12-28 16:11:52 +08:00
parent b4f2abb885
commit 42f99a6f84
5 changed files with 179 additions and 32 deletions
+79
View File
@@ -0,0 +1,79 @@
import { cn } from "@/lib/utils"
import { useEffect, useState } from "react"
export 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 default AnimateCountClient
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 flex h-full min-w-[0.6em] items-center 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>
)
}
+47 -22
View File
@@ -5,7 +5,6 @@ import { useBackground } from "@/hooks/use-background"
import { useWebSocketContext } from "@/hooks/use-websocket-context"
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api"
import { cn } from "@/lib/utils"
import NumberFlow, { NumberFlowGroup } from "@number-flow/react"
import { useQuery } from "@tanstack/react-query"
import { AnimatePresence, m } from "framer-motion"
import { ImageMinus } from "lucide-react"
@@ -14,11 +13,41 @@ import { useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import AnimateCountClient from "./AnimatedCount"
import { LanguageSwitcher } from "./LanguageSwitcher"
import { SearchButton } from "./SearchButton"
import { Loader, LoadingSpinner } from "./loading/Loader"
import { Button } from "./ui/button"
interface TimeState {
hh: number
mm: number
ss: number
}
const useCurrentTime = () => {
const [time, setTime] = useState<TimeState>({
hh: DateTime.now().setLocale("en-US").hour,
mm: DateTime.now().setLocale("en-US").minute,
ss: DateTime.now().setLocale("en-US").second,
})
useEffect(() => {
const intervalId = setInterval(() => {
const now = DateTime.now().setLocale("en-US")
setTime({
hh: now.hour,
mm: now.minute,
ss: now.second,
})
}, 1000)
return () => clearInterval(intervalId)
}, [])
return time
}
function Header() {
const { t } = useTranslation()
const navigate = useNavigate()
@@ -269,35 +298,31 @@ function DashboardLink() {
function Overview() {
const { t } = useTranslation()
const [time, setTime] = useState({
hh: DateTime.now().setLocale("en-US").hour,
mm: DateTime.now().setLocale("en-US").minute,
ss: DateTime.now().setLocale("en-US").second,
})
const time = useCurrentTime()
const [mounted, setMounted] = useState(false)
useEffect(() => {
const timer = setInterval(() => {
setTime({
hh: DateTime.now().setLocale("en-US").hour,
mm: DateTime.now().setLocale("en-US").minute,
ss: DateTime.now().setLocale("en-US").second,
})
}, 1000)
return () => clearInterval(timer)
setMounted(true)
}, [])
return (
<section className={"mt-10 flex flex-col md:mt-16 header-timer"}>
<p className="text-base font-semibold">👋 {t("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("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>
{mounted ? (
<div className="flex items-center font-medium text-sm">
<AnimateCountClient count={time.hh} minDigits={2} />
<span className="mb-px font-medium text-sm opacity-50">:</span>
<AnimateCountClient count={time.mm} minDigits={2} />
<span className="mb-px font-medium text-sm opacity-50">:</span>
<span className="font-medium text-sm">
<AnimateCountClient count={time.ss} minDigits={2} />
</span>
</div>
</NumberFlowGroup>
) : (
<Skeleton className="h-[21px] w-16 animate-none rounded-[5px] bg-muted-foreground/10" />
)}
</div>
</section>
)