diff --git a/bun.lock b/bun.lock index 3ea41aa..587cfc3 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,6 @@ "dependencies": { "@fontsource/inter": "5.1.1", "@heroicons/react": "2.2.0", - "@number-flow/react": "0.5.5", "@radix-ui/react-accordion": "1.2.3", "@radix-ui/react-checkbox": "1.1.4", "@radix-ui/react-dialog": "1.1.6", @@ -44,7 +43,7 @@ "recharts": "2.15.1", "sonner": "1.7.4", "tailwind-merge": "2.6.0", - "tailwindcss-animate": "1.0.7", + "tailwindcss-animate": "^1.0.7", }, "devDependencies": { "@eslint/js": "9.20.0", @@ -190,8 +189,6 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@number-flow/react": ["@number-flow/react@0.5.5", "", { "dependencies": { "esm-env": "^1.1.4", "number-flow": "0.5.3" }, "peerDependencies": { "react": "^18 || ^19", "react-dom": "^18 || ^19" } }, "sha512-Zdju5n0osxrb+7jbcpUJ9L2VJ2+9ptwjz5+A+2wq9Q32hs3PW/noPJjHtLTrtGINM9mEw76DcDg0ac/dx6j1aA=="], - "@radix-ui/number": ["@radix-ui/number@1.1.0", "", {}, "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="], @@ -530,8 +527,6 @@ "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], - "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], - "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], @@ -678,8 +673,6 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "number-flow": ["number-flow@0.5.3", "", { "dependencies": { "esm-env": "^1.1.4" } }, "sha512-iLKyssImNWQmJ41rza9K7P5lHRZTyishi/9FarWPLQHYY2Ydtl6eiXINEjZ1fa8dHeY0O7+YOD+Py3ZsJddYkg=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], diff --git a/package.json b/package.json index d735459..e4f619c 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "dependencies": { "@fontsource/inter": "5.1.1", "@heroicons/react": "2.2.0", - "@number-flow/react": "0.5.5", "@radix-ui/react-accordion": "1.2.3", "@radix-ui/react-checkbox": "1.1.4", "@radix-ui/react-dialog": "1.1.6", @@ -52,7 +51,7 @@ "recharts": "2.15.1", "sonner": "1.7.4", "tailwind-merge": "2.6.0", - "tailwindcss-animate": "1.0.7" + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@eslint/js": "9.20.0", diff --git a/src/components/AnimatedCount.tsx b/src/components/AnimatedCount.tsx new file mode 100644 index 0000000..fb623cd --- /dev/null +++ b/src/components/AnimatedCount.tsx @@ -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 ( + + {count} + + ) +} + +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 ( +
+ {currentDigits.map((digit, index) => { + const hasChanged = digit !== previousDigits[index] + return ( +
+
+ {previousDigits[index]} +
+
+ {digit} +
+
+ ) + })} +
+ ) +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 53c0173..6510f98 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -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({ + 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 (

👋 {t("overview")}

-
+

{t("whereTheTimeIs")}

- -
- - -

:{time.ss.toString().padStart(2, "0")}

+ {mounted ? ( +
+ + : + + : + + +
- + ) : ( + + )}
) diff --git a/src/index.css b/src/index.css index b6a52bc..f961429 100644 --- a/src/index.css +++ b/src/index.css @@ -150,6 +150,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 { @@ -321,3 +322,53 @@ .scrollbar-hidden::-webkit-scrollbar { display: none; /* Chrome, Safari 和 Opera */ } + +/* 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); + } +}