Merge upstream v2 with Domain tracking

This commit is contained in:
2026-04-16 11:57:15 +08:00
124 changed files with 13179 additions and 10039 deletions
+1
View File
@@ -34,6 +34,7 @@ jobs:
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
draft: ${{ contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-rc') }}
files: dist.zip files: dist.zip
- name: Changelog - name: Changelog
+38
View File
@@ -0,0 +1,38 @@
name: CI
on:
push:
branches:
- "**"
pull_request:
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build Check
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run build
run: pnpm run build
-12
View File
@@ -1,12 +0,0 @@
{
"semi": false,
"singleQuote": false,
"printWidth": 150,
"tabWidth": 2,
"trailingComma": "all",
"importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss", "@trivago/prettier-plugin-sort-imports"]
}
+1 -1
View File
@@ -3,5 +3,5 @@
> 交流 > 交流
> [!NOTE] > [!NOTE]
> 此项目为 nezha-dash 的官方实现,作为哪吒监控 V1 版本的默认前端,功能上可能与 nezha-dash 有所不同。 > 此项目为 nezha-dash 的官方实现,作为哪吒监控的默认前端,功能上可能与 nezha-dash 有所不同。
> https://github.com/hamster1963/nezha-dash > https://github.com/hamster1963/nezha-dash
+52
View File
@@ -0,0 +1,52 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!!**/dist"]
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noArrayIndexKey": "off",
"noExplicitAny": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
"a11y": {
"useButtonType": "off",
"noSvgWithoutTitle": "off",
"useKeyWithClickEvents": "off",
"noStaticElementInteractions": "off"
}
}
},
"css": {
"parser": {
"tailwindDirectives": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
+709
View File
@@ -0,0 +1,709 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "nazha-dashboard-vite",
"dependencies": {
"@fontsource/inter": "5.1.1",
"@heroicons/react": "2.2.0",
"@radix-ui/react-accordion": "1.2.3",
"@radix-ui/react-checkbox": "1.1.4",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "2.1.6",
"@radix-ui/react-label": "2.1.2",
"@radix-ui/react-popover": "1.1.6",
"@radix-ui/react-progress": "1.1.2",
"@radix-ui/react-select": "2.1.6",
"@radix-ui/react-separator": "1.1.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-switch": "1.1.3",
"@radix-ui/react-tooltip": "1.1.8",
"@tanstack/react-query": "5.66.7",
"@tanstack/react-query-devtools": "5.66.7",
"@tanstack/react-table": "8.21.2",
"@types/d3-geo": "3.1.0",
"@types/luxon": "3.4.2",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"country-flag-icons": "1.5.18",
"d3-geo": "3.1.1",
"dayjs": "1.11.13",
"framer-motion": "12.23.26",
"i18n-iso-countries": "7.14.0",
"i18next": "24.2.2",
"lucide-react": "0.460.0",
"luxon": "3.5.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-i18next": "15.4.1",
"react-router-dom": "^7.13.0",
"recharts": "2.15.1",
"sonner": "1.7.4",
"tailwind-merge": "2.6.0",
"tailwindcss-animate": "^1.0.7",
},
"devDependencies": {
"@biomejs/biome": "2.3.10",
"@tailwindcss/postcss": "^4.1.18",
"@types/node": "22.13.4",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@vitejs/plugin-react-swc": "3.8.0",
"globals": "15.15.0",
"postcss": "8.5.3",
"tailwindcss": "^4.1.18",
"typescript": "~5.6.3",
"vite": "6.4.1",
},
},
},
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@babel/runtime": ["@babel/runtime@7.26.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw=="],
"@biomejs/biome": ["@biomejs/biome@2.3.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.10", "@biomejs/cli-darwin-x64": "2.3.10", "@biomejs/cli-linux-arm64": "2.3.10", "@biomejs/cli-linux-arm64-musl": "2.3.10", "@biomejs/cli-linux-x64": "2.3.10", "@biomejs/cli-linux-x64-musl": "2.3.10", "@biomejs/cli-win32-arm64": "2.3.10", "@biomejs/cli-win32-x64": "2.3.10" }, "bin": { "biome": "bin/biome" } }, "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@floating-ui/core": ["@floating-ui/core@1.6.8", "", { "dependencies": { "@floating-ui/utils": "^0.2.8" } }, "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA=="],
"@floating-ui/dom": ["@floating-ui/dom@1.6.11", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.8" } }, "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.8", "", {}, "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="],
"@fontsource/inter": ["@fontsource/inter@5.1.1", "", {}, "sha512-weN3E+rq0Xb3Z93VHJ+Rc7WOQX9ETJPTAJ+gDcaMHtjft67L58sfS65rAjC5tZUXQ2FdZ/V1/sSzCwZ6v05kJw=="],
"@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.5", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@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=="],
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collapsible": "1.1.3", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-RIQ15mrcvqIkDARJeERSuXSry2N8uYnxkdDetpfmalT/+0ntOXLkFOsh9iwlAsCv+qcmhZjbdJogIm6WBa6c4A=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw=="],
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw=="],
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q=="],
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-menu": "2.1.6", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA=="],
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="],
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw=="],
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-roving-focus": "1.1.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-callback-ref": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg=="],
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.2", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA=="],
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="],
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="],
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.2", "", { "dependencies": { "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA=="],
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.1.6", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg=="],
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ=="],
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="],
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="],
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="],
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og=="],
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.0", "", { "dependencies": { "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ=="],
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw=="],
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q=="],
"@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
"@swc/core": ["@swc/core@1.10.15", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.17" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.10.15", "@swc/core-darwin-x64": "1.10.15", "@swc/core-linux-arm-gnueabihf": "1.10.15", "@swc/core-linux-arm64-gnu": "1.10.15", "@swc/core-linux-arm64-musl": "1.10.15", "@swc/core-linux-x64-gnu": "1.10.15", "@swc/core-linux-x64-musl": "1.10.15", "@swc/core-win32-arm64-msvc": "1.10.15", "@swc/core-win32-ia32-msvc": "1.10.15", "@swc/core-win32-x64-msvc": "1.10.15" }, "peerDependencies": { "@swc/helpers": "*" }, "optionalPeers": ["@swc/helpers"] }, "sha512-/iFeQuNaGdK7mfJbQcObhAhsMqLT7qgMYl7jX2GEIO+VDTejESpzAyKwaMeYXExN8D6e5BRHBCe7M5YlsuzjDA=="],
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.10.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zFdZ6/yHqMCPk7OhLFqHy/MQ1EqJhcZMpNHd1gXYT7VRU3FaqvvKETrUlG3VYl65McPC7AhMRfXPyJ0JO/jARQ=="],
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.10.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-8g4yiQwbr8fxOOjKXdot0dEkE5zgE8uNZudLy/ZyAhiwiZ8pbJ8/wVrDOu6dqbX7FBXAoDnvZ7fwN1jk4C8jdA=="],
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.10.15", "", { "os": "linux", "cpu": "arm" }, "sha512-rl+eVOltl2+7WXOnvmWBpMgh6aO13G5x0U0g8hjwlmD6ku3Y9iRcThpOhm7IytMEarUp5pQxItNoPq+VUGjVHg=="],
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.10.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-qxWEQeyAJMWJqjaN4hi58WMpPdt3Tn0biSK9CYRegQtvZWCbewr6v2agtSu5AZ2rudeH6OfCWAMDQQeSgn6PJQ=="],
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.10.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-QcELd9/+HjZx0WCxRrKcyKGWTiQ0485kFb5w8waxcSNd0d9Lgk4EFfWWVyvIb5gIHpDQmhrgzI/yRaWQX4YSZQ=="],
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.10.15", "", { "os": "linux", "cpu": "x64" }, "sha512-S1+ZEEn3+a/MiMeQqQypbwTGoBG8/sPoCvpNbk+uValyygT+jSn3U0xVr45FbukpmMB+NhBMqfedMLqKA0QnJA=="],
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.10.15", "", { "os": "linux", "cpu": "x64" }, "sha512-qW+H9g/2zTJ4jP7NDw4VAALY0ZlNEKzYsEoSj/HKi7k3tYEHjMzsxjfsY9I8WZCft23bBdV3RTCPoxCshaj1CQ=="],
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.10.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-AhRB11aA6LxjIqut+mg7qsu/7soQDmbK6MKR9nP3hgBszpqtXbRba58lr24xIbBCMr+dpo6kgEapWt+t5Po6Zg=="],
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.10.15", "", { "os": "win32", "cpu": "ia32" }, "sha512-UGdh430TQwbDn6KjgvRTg1fO022sbQ4yCCHUev0+5B8uoBwi9a89qAz3emy2m56C8TXxUoihW9Y9OMfaRwPXUw=="],
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.10.15", "", { "os": "win32", "cpu": "x64" }, "sha512-XJzBCqO1m929qbJsOG7FZXQWX26TnEoMctS3QjuCoyBmkHxxQmZsy78KjMes1aomTcKHCyFYgrRGWgVmk7tT4Q=="],
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
"@swc/types": ["@swc/types@0.1.17", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="],
"@tanstack/query-core": ["@tanstack/query-core@5.66.4", "", {}, "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.65.0", "", {}, "sha512-g5y7zc07U9D3esMdqUfTEVu9kMHoIaVBsD0+M3LPdAdD710RpTcLiNvJY1JkYXqkq9+NV+CQoemVNpQPBXVsJg=="],
"@tanstack/react-query": ["@tanstack/react-query@5.66.7", "", { "dependencies": { "@tanstack/query-core": "5.66.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-qd3q/tUpF2K1xItfPZddk1k/8pSXnovg41XyCqJgPoyYEirMBtB0sVEVVQ/CsAOngzgWtBPXimVf4q4kM9uO6A=="],
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.66.7", "", { "dependencies": { "@tanstack/query-devtools": "5.65.0" }, "peerDependencies": { "@tanstack/react-query": "^5.66.7", "react": "^18 || ^19" } }, "sha512-40z4PPkz06tYIF0vwLZZIZfZxKUH4OAaBOR14blCFyYm6hlU6qc+M82mkZ+D00HcEMhV7P4XeJiEuDhFq0q9Qw=="],
"@tanstack/react-table": ["@tanstack/react-table@8.21.2", "", { "dependencies": { "@tanstack/table-core": "8.21.2" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg=="],
"@tanstack/table-core": ["@tanstack/table-core@8.21.2", "", {}, "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA=="],
"@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
"@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
"@types/d3-path": ["@types/d3-path@3.1.0", "", {}, "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="],
"@types/d3-scale": ["@types/d3-scale@4.0.8", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ=="],
"@types/d3-shape": ["@types/d3-shape@3.1.6", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA=="],
"@types/d3-time": ["@types/d3-time@3.0.3", "", {}, "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw=="],
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/geojson": ["@types/geojson@7946.0.14", "", {}, "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg=="],
"@types/luxon": ["@types/luxon@3.4.2", "", {}, "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="],
"@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="],
"@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="],
"@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="],
"@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.8.0", "", { "dependencies": { "@swc/core": "^1.10.15" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw=="],
"aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"country-flag-icons": ["country-flag-icons@1.5.18", "", {}, "sha512-z+Uzesi8u8IdkViqqbzzbkf3+a7WJpcET5B7sPwTg7GXqPYpVEgNlZ/FC3l8KO4mEf+mNkmzKLppKTN4PlCJEQ=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
"d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
"d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
"dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="],
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"diacritics": ["diacritics@1.3.0", "", {}, "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA=="],
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
"fast-equals": ["fast-equals@5.0.1", "", {}, "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"framer-motion": ["framer-motion@12.23.26", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
"i18n-iso-countries": ["i18n-iso-countries@7.14.0", "", { "dependencies": { "diacritics": "1.3.0" } }, "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg=="],
"i18next": ["i18next@24.2.2", "", { "dependencies": { "@babel/runtime": "^7.23.2" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ=="],
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lucide-react": ["lucide-react@0.460.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg=="],
"luxon": ["luxon@3.5.0", "", {}, "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="],
"motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
"react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
"react-i18next": ["react-i18next@15.4.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw=="],
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
"react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="],
"react-router-dom": ["react-router-dom@7.13.0", "", { "dependencies": { "react-router": "7.13.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g=="],
"react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
"recharts": ["recharts@2.15.1", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q=="],
"recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="],
"regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
"scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="],
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
"sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="],
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tslib": ["tslib@2.8.0", "", {}, "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="],
"typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
"@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-dialog/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
"@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
"@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dialog/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
"@radix-ui/react-dismissable-layer/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
"@radix-ui/react-focus-scope/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
"@radix-ui/react-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
"@radix-ui/react-menu/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
"@radix-ui/react-menu/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA=="],
"@radix-ui/react-menu/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
"@radix-ui/react-popover/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
"@radix-ui/react-popover/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
"@radix-ui/react-popover/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA=="],
"@radix-ui/react-popover/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
"@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
"@radix-ui/react-select/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
"@radix-ui/react-select/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
"@radix-ui/react-select/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA=="],
"@radix-ui/react-select/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
"@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
"@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
"@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
"@radix-ui/react-use-escape-keydown/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.0", "", { "bundled": true }, "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="],
"cmdk/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw=="],
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"@radix-ui/react-dialog/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
"@radix-ui/react-dialog/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
"@radix-ui/react-dialog/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-menu/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="],
"@radix-ui/react-popover/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="],
"@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-select/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="],
"@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
"@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="],
}
}
BIN
View File
Binary file not shown.
-28
View File
@@ -1,28 +0,0 @@
import js from "@eslint/js"
import reactHooks from "eslint-plugin-react-hooks"
import reactRefresh from "eslint-plugin-react-refresh"
import globals from "globals"
import tseslint from "typescript-eslint"
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": "off",
"react-hooks/exhaustive-deps": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-object-type": "off",
},
},
)
+5 -5
View File
@@ -10,7 +10,7 @@
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
} }
document.documentElement.classList.add(theme) document.documentElement.classList.add(theme)
} catch (e) { } catch (_e) {
document.documentElement.classList.add("light") document.documentElement.classList.add("light")
} }
</script> </script>
@@ -36,15 +36,15 @@
} }
html { html {
background-color: var(--bg) !important; background-color: var(--bg);
} }
body { body {
background-color: var(--bg) !important; background-color: var(--bg);
} }
#root { #root {
background-color: var(--bg) !important; background-color: var(--bg);
visibility: hidden; visibility: hidden;
} }
@@ -63,7 +63,7 @@
} }
</style> </style>
<script> <script>
;(function () { ;(() => {
const storageKey = "vite-ui-theme" const storageKey = "vite-ui-theme"
const theme = localStorage.getItem(storageKey) || "system" const theme = localStorage.getItem(storageKey) || "system"
const root = document.documentElement const root = document.documentElement
+16 -20
View File
@@ -1,14 +1,16 @@
{ {
"name": "nazha-dash-v1", "name": "nazha-dash-v2",
"private": true, "private": true,
"version": "1.0.0", "version": "2.0.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "biome lint",
"lint:fix": "eslint --fix .", "lint:fix": "biome lint --fix",
"format": "prettier --write .", "format": "biome format --write .",
"check": "biome check",
"check:fix": "biome check --fix --unsafe",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
@@ -17,7 +19,7 @@
"@number-flow/react": "0.5.5", "@number-flow/react": "0.5.5",
"@radix-ui/react-accordion": "1.2.3", "@radix-ui/react-accordion": "1.2.3",
"@radix-ui/react-checkbox": "1.1.4", "@radix-ui/react-checkbox": "1.1.4",
"@radix-ui/react-dialog": "1.1.6", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "2.1.6", "@radix-ui/react-dropdown-menu": "2.1.6",
"@radix-ui/react-label": "2.1.2", "@radix-ui/react-label": "2.1.2",
"@radix-ui/react-popover": "1.1.6", "@radix-ui/react-popover": "1.1.6",
@@ -30,7 +32,6 @@
"@tanstack/react-query": "5.66.7", "@tanstack/react-query": "5.66.7",
"@tanstack/react-query-devtools": "5.66.7", "@tanstack/react-query-devtools": "5.66.7",
"@tanstack/react-table": "8.21.2", "@tanstack/react-table": "8.21.2",
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/d3-geo": "3.1.0", "@types/d3-geo": "3.1.0",
"@types/luxon": "3.4.2", "@types/luxon": "3.4.2",
"class-variance-authority": "0.7.1", "class-variance-authority": "0.7.1",
@@ -39,36 +40,31 @@
"country-flag-icons": "1.5.18", "country-flag-icons": "1.5.18",
"d3-geo": "3.1.1", "d3-geo": "3.1.1",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"framer-motion": "11.18.2", "framer-motion": "12.23.26",
"i18n-iso-countries": "7.14.0", "i18n-iso-countries": "7.14.0",
"i18next": "24.2.2", "i18next": "24.2.2",
"lucide-react": "0.460.0", "lucide-react": "0.460.0",
"luxon": "3.5.0", "luxon": "3.5.0",
"prettier-plugin-tailwindcss": "0.6.11",
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-i18next": "15.4.1", "react-i18next": "15.4.1",
"react-router-dom": "7.2.0", "react-router-dom": "^7.13.2",
"recharts": "2.15.1", "recharts": "2.15.1",
"sonner": "1.7.4", "sonner": "1.7.4",
"tailwind-merge": "2.6.0", "tailwind-merge": "2.6.0",
"tailwindcss-animate": "1.0.7" "tailwindcss-animate": "^1.0.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.20.0", "@biomejs/biome": "2.3.10",
"@tailwindcss/postcss": "^4.2.2",
"@types/node": "22.13.4", "@types/node": "22.13.4",
"@types/react": "19.0.10", "@types/react": "19.0.10",
"@types/react-dom": "19.0.4", "@types/react-dom": "19.0.4",
"@vitejs/plugin-react-swc": "3.8.0", "@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "10.4.20",
"eslint": "9.20.1",
"eslint-plugin-react-hooks": "5.1.0",
"eslint-plugin-react-refresh": "0.4.19",
"globals": "15.15.0", "globals": "15.15.0",
"postcss": "8.5.3", "postcss": "8.5.3",
"tailwindcss": "3.4.17", "tailwindcss": "^4.2.2",
"typescript": "~5.6.3", "typescript": "~5.6.3",
"typescript-eslint": "8.24.1", "vite": "8.0.3"
"vite": "6.1.1"
} }
} }
+911 -2355
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -1,6 +1,5 @@
export default { export default {
plugins: { plugins: {
tailwindcss: {}, "@tailwindcss/postcss": {},
autoprefixer: {},
}, },
} };
+3 -3
View File
@@ -1,7 +1,7 @@
const { execSync } = require("child_process") const { execSync } = require("node:child_process");
// Get the short version of the git hash // Get the short version of the git hash
const gitHash = execSync("git rev-parse --short HEAD").toString().trim() const gitHash = execSync("git rev-parse --short HEAD").toString().trim();
// Write it to stdout // Write it to stdout
console.log(gitHash) console.log(gitHash);
+67 -43
View File
@@ -1,85 +1,101 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query";
import React, { useEffect, useState } from "react" import type React from "react";
import { useTranslation } from "react-i18next" import { useEffect, useState } from "react";
import { Route, BrowserRouter as Router, Routes } from "react-router-dom" import { useTranslation } from "react-i18next";
import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import { DashCommand } from "./components/DashCommand" import { DashCommand } from "./components/DashCommand";
import ErrorBoundary from "./components/ErrorBoundary" import ErrorBoundary from "./components/ErrorBoundary";
import Footer from "./components/Footer" import Footer from "./components/Footer";
import Header, { RefreshToast } from "./components/Header" import Header, { RefreshToast } from "./components/Header";
import { useBackground } from "./hooks/use-background" import { useBackground } from "./hooks/use-background";
import { useTheme } from "./hooks/use-theme" import { useTheme } from "./hooks/use-theme";
import { InjectContext } from "./lib/inject" import { InjectContext } from "./lib/inject";
import { fetchSetting } from "./lib/nezha-api" import { fetchSetting } from "./lib/nezha-api";
import { cn } from "./lib/utils" import { cn } from "./lib/utils";
import ErrorPage from "./pages/ErrorPage" import ErrorPage from "./pages/ErrorPage";
import NotFound from "./pages/NotFound" import NotFound from "./pages/NotFound";
import Server from "./pages/Server" import Server from "./pages/Server";
import ServerDetail from "./pages/ServerDetail" import ServerDetail from "./pages/ServerDetail";
const App: React.FC = () => { // Route checker component
const RouteChecker: React.FC = () => {
return <MainApp />;
};
const MainApp: React.FC = () => {
const { data: settingData, error } = useQuery({ const { data: settingData, error } = useQuery({
queryKey: ["setting"], queryKey: ["setting"],
queryFn: () => fetchSetting(), queryFn: () => fetchSetting(),
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
}) });
const { i18n } = useTranslation() const { i18n } = useTranslation();
const { setTheme } = useTheme() const { setTheme } = useTheme();
const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false) const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false);
const { backgroundImage: customBackgroundImage } = useBackground() const { backgroundImage: customBackgroundImage } = useBackground();
useEffect(() => { useEffect(() => {
if (settingData?.data?.config?.custom_code) { if (settingData?.data?.config?.custom_code) {
InjectContext(settingData?.data?.config?.custom_code) InjectContext(settingData?.data?.config?.custom_code);
setIsCustomCodeInjected(true) setIsCustomCodeInjected(true);
} }
}, [settingData?.data?.config?.custom_code]) }, [settingData?.data?.config?.custom_code]);
// 检测是否强制指定了主题颜色 // 检测是否强制指定了主题颜色
const forceTheme = const forceTheme =
// @ts-expect-error ForceTheme is a global variable // @ts-expect-error ForceTheme is a global variable
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined (window.ForceTheme as string) !== "" ? window.ForceTheme : undefined;
useEffect(() => { useEffect(() => {
if (forceTheme === "dark" || forceTheme === "light") { if (forceTheme === "dark" || forceTheme === "light") {
setTheme(forceTheme) setTheme(forceTheme);
} }
}, [forceTheme]) }, [forceTheme, setTheme]);
if (error) { if (error) {
return <ErrorPage code={500} message={error.message} /> return <ErrorPage code={500} message={error.message} />;
} }
if (!settingData) { if (!settingData) {
return null return null;
} }
if (settingData?.data?.config?.custom_code && !isCustomCodeInjected) { if (settingData?.data?.config?.custom_code && !isCustomCodeInjected) {
return null return null;
} }
if (settingData?.data?.config?.language && !localStorage.getItem("language")) { if (
i18n.changeLanguage(settingData?.data?.config?.language) settingData?.data?.config?.language &&
!localStorage.getItem("language")
) {
i18n.changeLanguage(settingData?.data?.config?.language);
} }
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined const customMobileBackgroundImage =
window.CustomMobileBackgroundImage !== ""
? window.CustomMobileBackgroundImage
: undefined;
return ( return (
<Router basename={import.meta.env.BASE_URL}>
<ErrorBoundary> <ErrorBoundary>
{/* 固定定位的背景层 */} {/* 固定定位的背景层 */}
{customBackgroundImage && ( {customBackgroundImage && (
<div <div
className={cn("fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center dark:brightness-75", { className={cn(
"fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center dark:brightness-75",
{
"hidden sm:block": customMobileBackgroundImage, "hidden sm:block": customMobileBackgroundImage,
})} },
)}
style={{ backgroundImage: `url(${customBackgroundImage})` }} style={{ backgroundImage: `url(${customBackgroundImage})` }}
/> />
)} )}
{customMobileBackgroundImage && ( {customMobileBackgroundImage && (
<div <div
className={cn("fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center sm:hidden dark:brightness-75")} className={cn(
"fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center sm:hidden dark:brightness-75",
)}
style={{ backgroundImage: `url(${customMobileBackgroundImage})` }} style={{ backgroundImage: `url(${customMobileBackgroundImage})` }}
/> />
)} )}
@@ -102,8 +118,16 @@ const App: React.FC = () => {
</main> </main>
</div> </div>
</ErrorBoundary> </ErrorBoundary>
</Router> );
) };
}
export default App // Main App wrapper with router
const App: React.FC = () => {
return (
<Router basename={import.meta.env.BASE_URL}>
<RouteChecker />
</Router>
);
};
export default App;
+109
View File
@@ -0,0 +1,109 @@
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
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, previousCount]);
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>
);
}
+26 -20
View File
@@ -1,38 +1,43 @@
import { CycleTransferStats, NezhaServer } from "@/types/nezha-api" import type React from "react";
import React from "react" import type { CycleTransferStats, NezhaServer } from "@/types/nezha-api";
import { CycleTransferStatsClient } from "./CycleTransferStatsClient" import { CycleTransferStatsClient } from "./CycleTransferStatsClient";
interface CycleTransferStatsProps { interface CycleTransferStatsProps {
serverList: NezhaServer[] serverList: NezhaServer[];
cycleStats: CycleTransferStats cycleStats: CycleTransferStats;
className?: string className?: string;
} }
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({ serverList, cycleStats, className }) => { export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({
serverList,
cycleStats,
className,
}) => {
if (serverList.length === 0) { if (serverList.length === 0) {
return null return null;
} }
const serverIdList = serverList.map((server) => server.id.toString()) const serverIdList = serverList.map((server) => server.id.toString());
return ( return (
<section className="grid grid-cols-1 md:grid-cols-3 gap-3"> <section className="grid grid-cols-1 md:grid-cols-3 gap-3">
{Object.entries(cycleStats).map(([cycleId, cycleData]) => { {Object.entries(cycleStats).map(([cycleId, cycleData]) => {
if (!cycleData.server_name) { if (!cycleData.server_name) {
return null return null;
} }
return Object.entries(cycleData.server_name).map(([serverId, serverName]) => { return Object.entries(cycleData.server_name).map(
const transfer = cycleData.transfer?.[serverId] || 0 ([serverId, serverName]) => {
const nextUpdate = cycleData.next_update?.[serverId] const transfer = cycleData.transfer?.[serverId] || 0;
const nextUpdate = cycleData.next_update?.[serverId];
if (!serverIdList.includes(serverId)) { if (!serverIdList.includes(serverId)) {
return null return null;
} }
if (!transfer && !nextUpdate) { if (!transfer && !nextUpdate) {
return null return null;
} }
return ( return (
@@ -52,11 +57,12 @@ export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({ serv
]} ]}
className={className} className={className}
/> />
) );
}) },
);
})} })}
</section> </section>
) );
} };
export default CycleTransferStatsCard export default CycleTransferStatsCard;
+47 -30
View File
@@ -1,29 +1,34 @@
import { formatBytes } from "@/lib/format" import type React from "react";
import { cn } from "@/lib/utils" import { useTranslation } from "react-i18next";
import React from "react" import { formatBytes } from "@/lib/format";
import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils";
interface CycleTransferStatsClientProps { interface CycleTransferStatsClientProps {
name: string name: string;
from: string from: string;
to: string to: string;
max: number max: number;
serverStats: Array<{ serverStats: Array<{
serverId: string serverId: string;
serverName: string serverName: string;
transfer: number transfer: number;
nextUpdate: string nextUpdate: string;
}> }>;
className?: string className?: string;
} }
export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> = ({ name, from, to, max, serverStats, className }) => { export const CycleTransferStatsClient: React.FC<
const { t } = useTranslation() CycleTransferStatsClientProps
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined > = ({ name, from, to, max, serverStats, className }) => {
const { t } = useTranslation();
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
return ( return (
<div <div
className={cn( className={cn(
"w-full bg-white px-4 py-3.5 rounded-lg border bg-card text-card-foreground hover:shadow-sm transition-all duration-200 dark:shadow-none", "w-full bg-white px-4 py-3.5 rounded-lg border bg-card text-card-foreground hover:shadow-xs transition-all duration-200 dark:shadow-none",
className, className,
{ {
"bg-card/70": customBackgroundImage, "bg-card/70": customBackgroundImage,
@@ -31,24 +36,34 @@ export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> =
)} )}
> >
{serverStats.map(({ serverId, serverName, transfer, nextUpdate }) => { {serverStats.map(({ serverId, serverName, transfer, nextUpdate }) => {
const progress = (transfer / max) * 100 const progress = (transfer / max) * 100;
return ( return (
<div key={serverId} className="space-y-3"> <div key={serverId} className="space-y-3">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{serverName}</span> <span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
<div className="bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-2 py-0.5 rounded text-xs font-medium">{name}</div> {serverName}
</span>
<div className="bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-2 py-0.5 rounded text-xs font-medium">
{name}
</div>
</div> </div>
{/* Progress Section */} {/* Progress Section */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-baseline gap-1"> <div className="flex items-baseline gap-1">
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{formatBytes(transfer)}</span> <span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
<span className="text-xs text-neutral-500 dark:text-neutral-400">/ {formatBytes(max)}</span> {formatBytes(transfer)}
</span>
<span className="text-xs text-neutral-500 dark:text-neutral-400">
/ {formatBytes(max)}
</span>
</div> </div>
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">{progress.toFixed(1)}%</span> <span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">
{progress.toFixed(1)}%
</span>
</div> </div>
<div className="relative h-1.5"> <div className="relative h-1.5">
@@ -63,17 +78,19 @@ export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> =
{/* Footer */} {/* Footer */}
<div className="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400"> <div className="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400">
<span> <span>
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()} {new Date(from).toLocaleDateString()} -{" "}
{new Date(to).toLocaleDateString()}
</span> </span>
<span> <span>
{t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()} {t("cycleTransfer.nextUpdate")}:{" "}
{new Date(nextUpdate).toLocaleString()}
</span> </span>
</div> </div>
</div> </div>
) );
})} })}
</div> </div>
) );
} };
export default CycleTransferStatsClient export default CycleTransferStatsClient;
+47 -36
View File
@@ -1,39 +1,50 @@
"use client" "use client";
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command" import { Home, Moon, Sun, SunMoon } from "lucide-react";
import { useTheme } from "@/hooks/use-theme" import { useEffect, useState } from "react";
import { useWebSocketContext } from "@/hooks/use-websocket-context" import { useTranslation } from "react-i18next";
import { formatNezhaInfo } from "@/lib/utils" import { useNavigate } from "react-router-dom";
import { NezhaWebsocketResponse } from "@/types/nezha-api" import {
import { Home, Moon, Sun, SunMoon } from "lucide-react" CommandDialog,
import { useEffect, useState } from "react" CommandEmpty,
import { useTranslation } from "react-i18next" CommandGroup,
import { useNavigate } from "react-router-dom" CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import { useCommand } from "@/hooks/use-command";
import { useTheme } from "@/hooks/use-theme";
import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { formatNezhaInfo } from "@/lib/utils";
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
export function DashCommand() { export function DashCommand() {
const [open, setOpen] = useState(false) const { isOpen, closeCommand, toggleCommand } = useCommand();
const [search, setSearch] = useState("") const [search, setSearch] = useState("");
const navigate = useNavigate() const navigate = useNavigate();
const { t } = useTranslation() const { t } = useTranslation();
const { setTheme } = useTheme() const { setTheme } = useTheme();
const { lastMessage, connected } = useWebSocketContext() const { lastMessage, connected } = useWebSocketContext();
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null const nezhaWsData = lastMessage
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
: null;
useEffect(() => { useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) { if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault() e.preventDefault();
setOpen((open) => !open) toggleCommand();
}
} }
};
document.addEventListener("keydown", down) document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down) return () => document.removeEventListener("keydown", down);
}, []) }, [toggleCommand]);
if (!connected || !nezhaWsData) return null if (!connected || !nezhaWsData) return null;
const shortcuts = [ const shortcuts = [
{ {
@@ -63,24 +74,26 @@ export function DashCommand() {
].map((item) => ({ ].map((item) => ({
...item, ...item,
value: `${item.keywords.join(" ")} ${item.label}`, value: `${item.keywords.join(" ")} ${item.label}`,
})) }));
return ( return (
<> <CommandDialog open={isOpen} onOpenChange={closeCommand}>
<CommandDialog open={open} onOpenChange={setOpen}> <CommandInput
<CommandInput placeholder={t("TypeCommand")} value={search} onValueChange={setSearch} /> placeholder={t("TypeCommand")}
value={search}
onValueChange={setSearch}
/>
<CommandList className="border-t"> <CommandList className="border-t">
<CommandEmpty>{t("NoResults")}</CommandEmpty> <CommandEmpty>{t("NoResults")}</CommandEmpty>
{nezhaWsData.servers && nezhaWsData.servers.length > 0 && ( {nezhaWsData.servers && nezhaWsData.servers.length > 0 && (
<>
<CommandGroup heading={t("Servers")}> <CommandGroup heading={t("Servers")}>
{nezhaWsData.servers.map((server) => ( {nezhaWsData.servers.map((server) => (
<CommandItem <CommandItem
key={server.id} key={server.id}
value={server.name} value={server.name}
onSelect={() => { onSelect={() => {
navigate(`/server/${server.id}`) navigate(`/server/${server.id}`);
setOpen(false) closeCommand();
}} }}
> >
{formatNezhaInfo(nezhaWsData.now, server).online ? ( {formatNezhaInfo(nezhaWsData.now, server).online ? (
@@ -92,7 +105,6 @@ export function DashCommand() {
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
</>
)} )}
<CommandSeparator /> <CommandSeparator />
@@ -102,8 +114,8 @@ export function DashCommand() {
key={item.label} key={item.label}
value={item.value} value={item.value}
onSelect={() => { onSelect={() => {
item.action() item.action();
setOpen(false) closeCommand();
}} }}
> >
{item.icon} {item.icon}
@@ -113,6 +125,5 @@ export function DashCommand() {
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</CommandDialog> </CommandDialog>
</> );
)
} }
+16 -11
View File
@@ -1,36 +1,41 @@
import React from "react" import React from "react";
import ErrorPage from "../pages/ErrorPage" import ErrorPage from "../pages/ErrorPage";
interface Props { interface Props {
children: React.ReactNode children: React.ReactNode;
} }
interface State { interface State {
hasError: boolean hasError: boolean;
error?: Error error?: Error;
} }
class ErrorBoundary extends React.Component<Props, State> { class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props) super(props);
this.state = { hasError: false } this.state = { hasError: false };
} }
static getDerivedStateFromError(error: Error): State { static getDerivedStateFromError(error: Error): State {
return { return {
hasError: true, hasError: true,
error, error,
} };
} }
render() { render() {
if (this.state.hasError) { if (this.state.hasError) {
return <ErrorPage code={500} message={this.state.error?.message || "应用程序发生错误"} /> return (
<ErrorPage
code={500}
message={this.state.error?.message || "应用程序发生错误"}
/>
);
} }
return this.props.children return this.props.children;
} }
} }
export default ErrorBoundary export default ErrorBoundary;
+31 -17
View File
@@ -1,20 +1,18 @@
// src/components/Footer.tsx (已添加您的署名) import { useQuery } from "@tanstack/react-query";
import type React from "react";
import { fetchSetting } from "@/lib/nezha-api" import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query" import { fetchSetting } from "@/lib/nezha-api";
import React from "react"
import { useTranslation } from "react-i18next"
const Footer: React.FC = () => { const Footer: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation();
const isMac = /macintosh|mac os x/i.test(navigator.userAgent) const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
const { data: settingData } = useQuery({ const { data: settingData } = useQuery({
queryKey: ["setting"], queryKey: ["setting"],
queryFn: () => fetchSetting(), queryFn: () => fetchSetting(),
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
}) });
return ( return (
<footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4 server-footer"> <footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4 server-footer">
@@ -22,7 +20,11 @@ const Footer: React.FC = () => {
<section className="mt-1 flex items-center sm:flex-row flex-col justify-between gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50 server-footer-name"> <section className="mt-1 flex items-center sm:flex-row flex-col justify-between gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50 server-footer-name">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
&copy;2020-{new Date().getFullYear()}{" "} &copy;2020-{new Date().getFullYear()}{" "}
<a href={"https://github.com/naiba/nezha"} target="_blank" className="hover:underline"> <a
href={"https://github.com/naiba/nezha"}
target="_blank"
rel="noopener"
>
Nezha Nezha
</a> </a>
<p>{settingData?.data?.version || ""}</p> <p>{settingData?.data?.version || ""}</p>
@@ -34,19 +36,31 @@ const Footer: React.FC = () => {
</kbd> </kbd>
</p> </p>
<section> <section>
{t("footer.themeBy")}{" "} {t("footer.themeBy")}
<a href={"https://github.com/hamster1963/nezha-dash"} target="_blank" className="hover:underline"> <a
href={"https://github.com/hamster1963/nezha-dash"}
target="_blank"
rel="noopener"
>
nezha-dash nezha-dash
</a> </a>
{import.meta.env.VITE_GIT_HASH && ( {import.meta.env.VITE_GIT_HASH && (
<a href={"https://github.com/hamster1963/nezha-dash-v1/commit/" + import.meta.env.VITE_GIT_HASH} className="ml-1 hover:underline"> <a
href={`https://github.com/hamster1963/nezha-dash-v1/commit/${import.meta.env.VITE_GIT_HASH}`}
className="ml-1"
>
({import.meta.env.VITE_GIT_HASH}) ({import.meta.env.VITE_GIT_HASH})
</a> </a>
)} )}
</section> </section>
<section className="mt-1"> <section className="mt-1">
{"Modified by "} {"Modified by "}
<a href={"https://github.com/buriburizaem0n"} target="_blank" className="hover:underline font-medium"> <a
href={"https://github.com/buriburizaem0n"}
target="_blank"
rel="noopener"
className="hover:underline font-medium"
>
buriburizaem0n buriburizaem0n
</a> </a>
</section> </section>
@@ -54,7 +68,7 @@ const Footer: React.FC = () => {
</section> </section>
</section> </section>
</footer> </footer>
) );
} };
export default Footer export default Footer;
+110 -61
View File
@@ -1,35 +1,47 @@
import useTooltip from "@/hooks/use-tooltip" import { geoEquirectangular, geoPath } from "d3-geo";
import { geoJsonString } from "@/lib/geo-json-string" import { useTranslation } from "react-i18next";
import { countryCoordinates } from "@/lib/geo-limit" import useTooltip from "@/hooks/use-tooltip";
import { cn, formatNezhaInfo } from "@/lib/utils" import { geoJsonString } from "@/lib/geo-json-string";
import { NezhaServer } from "@/types/nezha-api" import { countryCoordinates } from "@/lib/geo-limit";
import { geoEquirectangular, geoPath } from "d3-geo" import { cn, formatNezhaInfo } from "@/lib/utils";
import { useTranslation } from "react-i18next" import type { NezhaServer } from "@/types/nezha-api";
import MapTooltip from "./MapTooltip" import MapTooltip from "./MapTooltip";
export default function GlobalMap({ serverList, now }: { serverList: NezhaServer[]; now: number }) { export default function GlobalMap({
const { t } = useTranslation() serverList,
const countryList: string[] = [] now,
const serverCounts: { [key: string]: number } = {} }: {
serverList: NezhaServer[];
now: number;
}) {
const { t } = useTranslation();
const countryList: string[] = [];
const serverCounts: { [key: string]: number } = {};
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
serverList.forEach((server) => { serverList.forEach((server) => {
if (server.country_code) { if (server.country_code) {
const countryCode = server.country_code.toUpperCase() const countryCode = server.country_code.toUpperCase();
if (!countryList.includes(countryCode)) { if (!countryList.includes(countryCode)) {
countryList.push(countryCode) countryList.push(countryCode);
} }
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1 serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1;
} }
}) });
const width = 900 const width = 900;
const height = 500 const height = 500;
const geoJson = JSON.parse(geoJsonString) const geoJson = JSON.parse(geoJsonString);
const filteredFeatures = geoJson.features.filter((feature: { properties: { iso_a3_eh: string } }) => feature.properties.iso_a3_eh !== "") const filteredFeatures = geoJson.features.filter(
(feature: { properties: { iso_a3_eh: string } }) =>
feature.properties.iso_a3_eh !== "",
);
return ( return (
<section <section
@@ -52,39 +64,56 @@ export default function GlobalMap({ serverList, now }: { serverList: NezhaServer
/> />
</div> </div>
</section> </section>
) );
} }
interface InteractiveMapProps { interface InteractiveMapProps {
countries: string[] countries: string[];
serverCounts: { [key: string]: number } serverCounts: { [key: string]: number };
width: number width: number;
height: number height: number;
filteredFeatures: { filteredFeatures: {
type: "Feature" type: "Feature";
properties: { properties: {
iso_a2_eh: string iso_a2_eh: string;
[key: string]: string [key: string]: string;
} };
geometry: never geometry: never;
}[] }[];
nezhaServerList: NezhaServer[] nezhaServerList: NezhaServer[];
now: number now: number;
} }
export function InteractiveMap({ countries, serverCounts, width, height, filteredFeatures, nezhaServerList, now }: InteractiveMapProps) { export function InteractiveMap({
const { setTooltipData } = useTooltip() countries,
serverCounts,
width,
height,
filteredFeatures,
nezhaServerList,
now,
}: InteractiveMapProps) {
const { setTooltipData } = useTooltip();
const projection = geoEquirectangular() const projection = geoEquirectangular()
.scale(140) .scale(140)
.translate([width / 2, height / 2]) .translate([width / 2, height / 2])
.rotate([-12, 0, 0]) .rotate([-12, 0, 0]);
const path = geoPath().projection(projection) const path = geoPath().projection(projection);
return ( return (
<div className="relative w-full aspect-[2/1]" onMouseLeave={() => setTooltipData(null)}> <div
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} xmlns="http://www.w3.org/2000/svg" className="w-full h-auto"> className="relative w-full aspect-2/1"
onMouseLeave={() => setTooltipData(null)}
>
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto"
>
<defs> <defs>
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse"> <pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.5" fill="currentColor" /> <circle cx="1" cy="1" r="0.5" fill="currentColor" />
@@ -92,11 +121,20 @@ export function InteractiveMap({ countries, serverCounts, width, height, filtere
</defs> </defs>
<g> <g>
{/* Background rect to handle mouse events in empty areas */} {/* Background rect to handle mouse events in empty areas */}
<rect x="0" y="0" width={width} height={height} fill="transparent" onMouseEnter={() => setTooltipData(null)} /> <rect
x="0"
y="0"
width={width}
height={height}
fill="transparent"
onMouseEnter={() => setTooltipData(null)}
/>
{filteredFeatures.map((feature, index) => { {filteredFeatures.map((feature, index) => {
const isHighlighted = countries.includes(feature.properties.iso_a2_eh) const isHighlighted = countries.includes(
feature.properties.iso_a2_eh,
);
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0 const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0;
return ( return (
<path <path
@@ -109,61 +147,72 @@ export function InteractiveMap({ countries, serverCounts, width, height, filtere
} }
onMouseEnter={() => { onMouseEnter={() => {
if (!isHighlighted) { if (!isHighlighted) {
setTooltipData(null) setTooltipData(null);
return return;
} }
if (path.centroid(feature)) { if (path.centroid(feature)) {
const countryCode = feature.properties.iso_a2_eh const countryCode = feature.properties.iso_a2_eh;
const countryServers = nezhaServerList const countryServers = nezhaServerList
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode) .filter(
(server: NezhaServer) =>
server.country_code?.toUpperCase() === countryCode,
)
.map((server: NezhaServer) => ({ .map((server: NezhaServer) => ({
id: server.id,
name: server.name, name: server.name,
status: formatNezhaInfo(now, server).online, status: formatNezhaInfo(now, server).online,
})) }));
setTooltipData({ setTooltipData({
centroid: path.centroid(feature), centroid: path.centroid(feature),
country: feature.properties.name, country: feature.properties.name,
count: serverCount, count: serverCount,
servers: countryServers, servers: countryServers,
}) });
} }
}} }}
/> />
) );
})} })}
{/* 渲染不在 filteredFeatures 中的国家标记点 */} {/* 渲染不在 filteredFeatures 中的国家标记点 */}
{countries.map((countryCode) => { {countries.map((countryCode) => {
// 检查该国家是否已经在 filteredFeatures 中 // 检查该国家是否已经在 filteredFeatures 中
const isInFilteredFeatures = filteredFeatures.some((feature) => feature.properties.iso_a2_eh === countryCode) const isInFilteredFeatures = filteredFeatures.some(
(feature) => feature.properties.iso_a2_eh === countryCode,
);
// 如果已经在 filteredFeatures 中,跳过 // 如果已经在 filteredFeatures 中,跳过
if (isInFilteredFeatures) return null if (isInFilteredFeatures) return null;
// 获取国家的经纬度 // 获取国家的经纬度
const coords = countryCoordinates[countryCode] const coords = countryCoordinates[countryCode];
if (!coords) return null if (!coords) return null;
// 使用投影函数将经纬度转换为 SVG 坐标 // 使用投影函数将经纬度转换为 SVG 坐标
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0] const [x, y] = projection([coords.lng, coords.lat]) || [0, 0];
const serverCount = serverCounts[countryCode] || 0 const serverCount = serverCounts[countryCode] || 0;
return ( return (
<g <g
key={countryCode} key={countryCode}
onMouseEnter={() => { onMouseEnter={() => {
const countryServers = nezhaServerList const countryServers = nezhaServerList
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode.toUpperCase()) .filter(
(server: NezhaServer) =>
server.country_code?.toUpperCase() ===
countryCode.toUpperCase(),
)
.map((server: NezhaServer) => ({ .map((server: NezhaServer) => ({
id: server.id,
name: server.name, name: server.name,
status: formatNezhaInfo(now, server).online, status: formatNezhaInfo(now, server).online,
})) }));
setTooltipData({ setTooltipData({
centroid: [x, y], centroid: [x, y],
country: coords.name, country: coords.name,
count: serverCount, count: serverCount,
servers: countryServers, servers: countryServers,
}) });
}} }}
className="cursor-pointer" className="cursor-pointer"
> >
@@ -174,11 +223,11 @@ export function InteractiveMap({ countries, serverCounts, width, height, filtere
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all" className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
/> />
</g> </g>
) );
})} })}
</g> </g>
</svg> </svg>
<MapTooltip /> <MapTooltip />
</div> </div>
) );
} }
+52 -33
View File
@@ -1,65 +1,82 @@
import { cn } from "@/lib/utils" import { m } from "framer-motion";
import { m } from "framer-motion" import { createRef, useEffect, useRef } from "react";
import { createRef, useEffect, useRef } from "react" import { cn } from "@/lib/utils";
export default function GroupSwitch({ export default function GroupSwitch({
tabs, tabs,
currentTab, currentTab,
setCurrentTab, setCurrentTab,
}: { }: {
tabs: string[] tabs: string[];
currentTab: string currentTab: string;
setCurrentTab: (tab: string) => void setCurrentTab: (tab: string) => void;
}) { }) {
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const scrollRef = useRef<HTMLDivElement>(null) const scrollRef = useRef<HTMLDivElement>(null);
const tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>())) const tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>()));
useEffect(() => { useEffect(() => {
const container = scrollRef.current const container = scrollRef.current;
if (!container) return if (!container) return;
const isOverflowing = container.scrollWidth > container.clientWidth const isOverflowing = container.scrollWidth > container.clientWidth;
if (!isOverflowing) return if (!isOverflowing) return;
const onWheel = (e: WheelEvent) => { const onWheel = (e: WheelEvent) => {
e.preventDefault() e.preventDefault();
container.scrollLeft += e.deltaY container.scrollLeft += e.deltaY;
} };
container.addEventListener("wheel", onWheel, { passive: false }) container.addEventListener("wheel", onWheel, { passive: false });
return () => { return () => {
container.removeEventListener("wheel", onWheel) container.removeEventListener("wheel", onWheel);
} };
}, []) }, []);
useEffect(() => { useEffect(() => {
const savedGroup = sessionStorage.getItem("selectedGroup") if (tabs.length === 1 && tabs[0] === "All") {
setCurrentTab("All");
return;
}
const savedGroup = sessionStorage.getItem("selectedGroup");
if (savedGroup && tabs.includes(savedGroup)) { if (savedGroup && tabs.includes(savedGroup)) {
setCurrentTab(savedGroup) setCurrentTab(savedGroup);
} }
}, [tabs, setCurrentTab]) }, [tabs, setCurrentTab]);
useEffect(() => { useEffect(() => {
const currentTagRef = tagRefs.current[tabs.indexOf(currentTab)] const currentTagRef = tagRefs.current[tabs.indexOf(currentTab)];
if (currentTagRef && currentTagRef.current) { if (currentTagRef?.current) {
currentTagRef.current.scrollIntoView({ currentTagRef.current.scrollIntoView({
behavior: "smooth", behavior: "smooth",
block: "nearest", block: "nearest",
inline: "center", inline: "center",
}) });
}
}, [currentTab, tabs.indexOf]);
if (tabs.length === 1 && tabs[0] === "All") {
return null;
} }
}, [currentTab])
return ( return (
<div ref={scrollRef} className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]">
<div <div
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", { ref={scrollRef}
className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]"
>
<div
className={cn(
"flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800",
{
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage, "bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
})} },
)}
> >
{tabs.map((tab: string, index: number) => ( {tabs.map((tab: string, index: number) => (
<div <div
@@ -67,8 +84,10 @@ export default function GroupSwitch({
ref={tagRefs.current[index]} ref={tagRefs.current[index]}
onClick={() => setCurrentTab(tab)} onClick={() => setCurrentTab(tab)}
className={cn( className={cn(
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500", "relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold transition-all duration-500",
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500", currentTab === tab
? "text-black dark:text-white"
: "text-stone-400 dark:text-stone-500",
)} )}
> >
{currentTab === tab && ( {currentTab === tab && (
@@ -88,5 +107,5 @@ export default function GroupSwitch({
))} ))}
</div> </div>
</div> </div>
) );
} }
+167 -106
View File
@@ -1,87 +1,129 @@
import { ModeToggle } from "@/components/ThemeSwitcher" import { useQuery } from "@tanstack/react-query";
import { Separator } from "@/components/ui/separator" import { AnimatePresence, m } from "framer-motion";
import { Skeleton } from "@/components/ui/skeleton" import { ImageMinus } from "lucide-react";
import { useBackground } from "@/hooks/use-background" import { DateTime } from "luxon";
import { useWebSocketContext } from "@/hooks/use-websocket-context" import { useEffect, useRef, useState } from "react";
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api" import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils" import { useNavigate } from "react-router-dom";
import NumberFlow, { NumberFlowGroup } from "@number-flow/react" import { ModeToggle } from "@/components/ThemeSwitcher";
import { useQuery } from "@tanstack/react-query" import { Separator } from "@/components/ui/separator";
import { AnimatePresence, m } from "framer-motion" import { Skeleton } from "@/components/ui/skeleton";
import { ImageMinus } from "lucide-react" import { useBackground } from "@/hooks/use-background";
import { DateTime } from "luxon" import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { useEffect, useRef, useState } from "react" import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api";
import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils";
import { useNavigate } from "react-router-dom"
import { LanguageSwitcher } from "./LanguageSwitcher" import AnimateCountClient from "./AnimatedCount";
import { Loader, LoadingSpinner } from "./loading/Loader" import { LanguageSwitcher } from "./LanguageSwitcher";
import { Button } from "./ui/button" import { Loader, LoadingSpinner } from "./loading/Loader";
import { SearchButton } from "./SearchButton";
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() { function Header() {
const { t } = useTranslation() const { t } = useTranslation();
const navigate = useNavigate() const navigate = useNavigate();
const { backgroundImage, updateBackground } = useBackground() const { backgroundImage, updateBackground } = useBackground();
const { data: settingData, isLoading } = useQuery({ const { data: settingData, isLoading } = useQuery({
queryKey: ["setting"], queryKey: ["setting"],
queryFn: () => fetchSetting(), queryFn: () => fetchSetting(),
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
}) });
const { lastMessage, connected } = useWebSocketContext() const { lastMessage, connected } = useWebSocketContext();
const onlineCount = connected ? (lastMessage ? JSON.parse(lastMessage.data).online || 0 : 0) : "..." const onlineCount = connected
? lastMessage
? JSON.parse(lastMessage.data).online || 0
: 0
: "...";
const siteName = settingData?.data?.config?.site_name const siteName = settingData?.data?.config?.site_name;
// @ts-expect-error CustomLogo is a global variable // @ts-expect-error CustomLogo is a global variable
const customLogo = window.CustomLogo || "/apple-touch-icon.png" const customLogo = window.CustomLogo || "/apple-touch-icon.png";
// @ts-expect-error CustomDesc is a global variable // @ts-expect-error CustomDesc is a global variable
const customDesc = window.CustomDesc || t("nezha") const customDesc = window.CustomDesc || t("nezha");
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined const customMobileBackgroundImage =
window.CustomMobileBackgroundImage !== ""
? window.CustomMobileBackgroundImage
: undefined;
useEffect(() => { useEffect(() => {
const link = document.querySelector("link[rel*='icon']") || document.createElement("link") const link =
document.querySelector("link[rel*='icon']") ||
document.createElement("link");
// @ts-expect-error set link.type // @ts-expect-error set link.type
link.type = "image/x-icon" link.type = "image/x-icon";
// @ts-expect-error set link.rel // @ts-expect-error set link.rel
link.rel = "shortcut icon" link.rel = "shortcut icon";
// @ts-expect-error set link.href // @ts-expect-error set link.href
link.href = customLogo link.href = customLogo;
document.getElementsByTagName("head")[0].appendChild(link) document.getElementsByTagName("head")[0].appendChild(link);
}, [customLogo]) }, [customLogo]);
useEffect(() => { useEffect(() => {
document.title = siteName || "哪吒监控 Nezha Monitoring" document.title = siteName || "哪吒监控 Nezha Monitoring";
}, [siteName]) }, [siteName]);
const handleBackgroundToggle = () => { const handleBackgroundToggle = () => {
if (window.CustomBackgroundImage) { if (window.CustomBackgroundImage) {
// Store the current background image before removing it // Store the current background image before removing it
sessionStorage.setItem("savedBackgroundImage", window.CustomBackgroundImage) sessionStorage.setItem(
updateBackground(undefined) "savedBackgroundImage",
window.CustomBackgroundImage,
);
updateBackground(undefined);
} else { } else {
// Restore the saved background image // Restore the saved background image
const savedImage = sessionStorage.getItem("savedBackgroundImage") const savedImage = sessionStorage.getItem("savedBackgroundImage");
if (savedImage) { if (savedImage) {
updateBackground(savedImage) updateBackground(savedImage);
}
} }
} }
};
const customBackgroundImage = backgroundImage const customBackgroundImage = backgroundImage;
return ( return (
<div className="mx-auto w-full max-w-5xl"> <div className="mx-auto w-full max-w-5xl">
<section className="flex items-center justify-between header-top"> <section className="flex items-center justify-between header-top">
<section <section
onClick={() => { onClick={() => {
sessionStorage.removeItem("selectedGroup") sessionStorage.removeItem("selectedGroup");
navigate("/") navigate("/");
}} }}
className="cursor-pointer flex items-center sm:text-base text-sm font-medium" className="cursor-pointer flex items-center sm:text-base text-sm font-medium"
> >
@@ -94,18 +136,29 @@ function Header() {
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!" className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!"
/> />
</div> </div>
{isLoading ? <Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" /> : siteName || "NEZHA"} {isLoading ? (
<Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" /> <Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" />
<p className="hidden text-sm font-medium opacity-40 md:block">{customDesc}</p> ) : (
siteName || "NEZHA"
)}
<Separator
orientation="vertical"
className="mx-2 hidden h-4 w-px md:block"
/>
<p className="hidden text-sm font-medium opacity-40 md:block">
{customDesc}
</p>
</section> </section>
<section className="flex items-center gap-2 header-handles"> <section className="flex items-center gap-2 header-handles">
<div className="hidden sm:flex items-center gap-2"> <div className="hidden sm:flex items-center gap-2">
<Links /> <Links />
<DashboardLink /> <DashboardLink />
</div> </div>
<SearchButton />
<LanguageSwitcher /> <LanguageSwitcher />
<ModeToggle /> <ModeToggle />
{(customBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) && ( {(customBackgroundImage ||
sessionStorage.getItem("savedBackgroundImage")) && (
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -121,12 +174,17 @@ function Header() {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className={cn("hover:bg-white dark:hover:bg-black cursor-default rounded-full flex items-center px-[9px] bg-white dark:bg-black", { className={cn(
"hover:bg-white dark:hover:bg-black cursor-default rounded-full flex items-center px-[9px] bg-white dark:bg-black",
{
"bg-white/70 dark:bg-black/70": customBackgroundImage, "bg-white/70 dark:bg-black/70": customBackgroundImage,
})} },
)}
> >
{connected ? onlineCount : <Loader visible={true} />} {connected ? onlineCount : <Loader visible={true} />}
<p className="text-muted-foreground">{connected ? t("online") : t("offline")}</p> <p className="text-muted-foreground">
{connected ? t("online") : t("offline")}
</p>
<span <span
className={cn("h-2 w-2 rounded-full bg-green-500", { className={cn("h-2 w-2 rounded-full bg-green-500", {
"bg-red-500": !connected, "bg-red-500": !connected,
@@ -141,21 +199,21 @@ function Header() {
</div> </div>
<Overview /> <Overview />
</div> </div>
) );
} }
type links = { type links = {
link: string link: string;
name: string name: string;
} };
function Links() { function Links() {
// @ts-expect-error CustomLinks is a global variable // @ts-expect-error CustomLinks is a global variable
const customLinks = window.CustomLinks as string const customLinks = window.CustomLinks as string;
const links: links[] | null = customLinks ? JSON.parse(customLinks) : null const links: links[] | null = customLinks ? JSON.parse(customLinks) : null;
if (!links) return null if (!links) return null;
return ( return (
<div className="flex items-center gap-2 w-fit"> <div className="flex items-center gap-2 w-fit">
@@ -170,27 +228,27 @@ function Links() {
> >
{link.name} {link.name}
</a> </a>
) );
})} })}
</div> </div>
) );
} }
export function RefreshToast() { export function RefreshToast() {
const { t } = useTranslation() const { t } = useTranslation();
const navigate = useNavigate() const navigate = useNavigate();
const { needReconnect } = useWebSocketContext() const { needReconnect } = useWebSocketContext();
if (!needReconnect) { if (!needReconnect) {
return null return null;
} }
if (needReconnect) { if (needReconnect) {
sessionStorage.removeItem("needRefresh") sessionStorage.removeItem("needRefresh");
setTimeout(() => { setTimeout(() => {
navigate(0) navigate(0);
}, 1000) }, 1000);
} }
return ( return (
@@ -200,7 +258,7 @@ export function RefreshToast() {
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }} animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
exit={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }} exit={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
transition={{ type: "spring", duration: 0.8 }} transition={{ type: "spring", duration: 0.8 }}
className="fixed left-1/2 -translate-x-1/2 top-8 z-[999] flex items-center justify-between gap-4 rounded-[50px] border-[1px] border-solid bg-white px-2 py-1.5 shadow-xl shadow-black/5 dark:border-stone-700 dark:bg-stone-800 dark:shadow-none" className="fixed left-1/2 -translate-x-1/2 top-8 z-999 flex items-center justify-between gap-4 rounded-[50px] border border-solid bg-white px-2 py-1.5 shadow-xl shadow-black/5 dark:border-stone-700 dark:bg-stone-800 dark:shadow-none"
> >
<section className="flex items-center gap-1.5"> <section className="flex items-center gap-1.5">
<LoadingSpinner /> <LoadingSpinner />
@@ -208,13 +266,13 @@ export function RefreshToast() {
</section> </section>
</m.div> </m.div>
</AnimatePresence> </AnimatePresence>
) );
} }
function DashboardLink() { function DashboardLink() {
const { t } = useTranslation() const { t } = useTranslation();
const { setNeedReconnect } = useWebSocketContext() const { setNeedReconnect } = useWebSocketContext();
const previousLoginState = useRef<boolean | null>(null) const previousLoginState = useRef<boolean | null>(null);
const { const {
data: userData, data: userData,
isFetched, isFetched,
@@ -229,27 +287,34 @@ function DashboardLink() {
refetchIntervalInBackground: true, refetchIntervalInBackground: true,
refetchInterval: 1000 * 30, refetchInterval: 1000 * 30,
retry: 0, retry: 0,
}) });
const isLogin = isError ? false : userData ? !!userData?.data?.id && !!document.cookie : false const isLogin = isError
? false
: userData
? !!userData?.data?.id && !!document.cookie
: false;
if (isLoadingError) { if (isLoadingError) {
previousLoginState.current = isLogin previousLoginState.current = isLogin;
} }
useEffect(() => { useEffect(() => {
refetch() refetch();
}, [document.cookie]) }, [refetch]);
useEffect(() => { useEffect(() => {
if (isFetched || isError) { if (isFetched || isError) {
// 只有当登录状态发生变化时才设置needReconnect // 只有当登录状态发生变化时才设置needReconnect
if (previousLoginState.current !== null && previousLoginState.current !== isLogin) { if (
setNeedReconnect(true) previousLoginState.current !== null &&
previousLoginState.current !== isLogin
) {
setNeedReconnect(true);
} }
previousLoginState.current = isLogin previousLoginState.current = isLogin;
} }
}, [isLogin]) }, [isLogin, isError, isFetched, setNeedReconnect]);
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -262,42 +327,38 @@ function DashboardLink() {
{isLogin && t("dashboard")} {isLogin && t("dashboard")}
</a> </a>
</div> </div>
) );
} }
function Overview() { function Overview() {
const { t } = useTranslation() const { t } = useTranslation();
const [time, setTime] = useState({ const time = useCurrentTime();
hh: DateTime.now().setLocale("en-US").hour, const [mounted, setMounted] = useState(false);
mm: DateTime.now().setLocale("en-US").minute,
ss: DateTime.now().setLocale("en-US").second,
})
useEffect(() => { useEffect(() => {
const timer = setInterval(() => { setMounted(true);
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)
}, [])
return ( return (
<section className={"mt-10 flex flex-col md:mt-16 header-timer"}> <section className={"mt-10 flex flex-col md:mt-16 header-timer"}>
<p className="text-base font-semibold">👋 {t("overview")}</p> <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> <p className="text-sm font-medium opacity-50">{t("whereTheTimeIs")}</p>
<NumberFlowGroup> {mounted ? (
<div style={{ fontVariantNumeric: "tabular-nums" }} className="flex text-sm font-medium mt-0.5"> <div className="flex items-center font-medium text-sm">
<NumberFlow trend={1} value={time.hh} format={{ minimumIntegerDigits: 2 }} /> <AnimateCountClient count={time.hh} minDigits={2} />
<NumberFlow prefix=":" trend={1} value={time.mm} digits={{ 1: { max: 5 } }} format={{ minimumIntegerDigits: 2 }} /> <span className="mb-px font-medium text-sm opacity-50">:</span>
<p className="mt-[0.5px]">:{time.ss.toString().padStart(2, "0")}</p> <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> </div>
</NumberFlowGroup> ) : (
<Skeleton className="h-[21px] w-16 animate-none rounded-[5px] bg-muted-foreground/10" />
)}
</div> </div>
</section> </section>
) );
} }
export default Header export default Header;
+2 -2
View File
@@ -3,7 +3,7 @@ export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
<svg viewBox="0 0 496 512" fill="white" {...props}> <svg viewBox="0 0 496 512" fill="white" {...props}>
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" /> <path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
</svg> </svg>
) );
} }
export function BackIcon() { export function BackIcon() {
@@ -28,5 +28,5 @@ export function BackIcon() {
height="20" height="20"
/> />
</> </>
) );
} }
+41 -17
View File
@@ -1,22 +1,30 @@
"use client" "use client";
import { Button } from "@/components/ui/button" import { CheckCircleIcon, LanguageIcon } from "@heroicons/react/20/solid";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button";
import { CheckCircleIcon, LanguageIcon } from "@heroicons/react/20/solid" import {
import { useTranslation } from "react-i18next" DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
export function LanguageSwitcher() { export function LanguageSwitcher() {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation();
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const locale = i18n.languages[0] const locale = i18n.languages[0];
const handleSelect = (e: Event, newLocale: string) => { const handleSelect = (e: Event, newLocale: string) => {
e.preventDefault() // 阻止默认的关闭行为 e.preventDefault(); // 阻止默认的关闭行为
i18n.changeLanguage(newLocale) i18n.changeLanguage(newLocale);
} };
const localeItems = [ const localeItems = [
{ name: t("language.zh-CN"), code: "zh-CN" }, { name: t("language.zh-CN"), code: "zh-CN" },
@@ -26,7 +34,7 @@ export function LanguageSwitcher() {
{ name: t("language.es-ES"), code: "es-ES" }, { name: t("language.es-ES"), code: "es-ES" },
{ name: t("language.de-DE"), code: "de-DE" }, { name: t("language.de-DE"), code: "de-DE" },
{ name: t("language.ta-IN"), code: "ta-IN" }, { name: t("language.ta-IN"), code: "ta-IN" },
] ];
return ( return (
<DropdownMenu> <DropdownMenu>
@@ -43,12 +51,28 @@ export function LanguageSwitcher() {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="flex flex-col gap-0.5" align="end"> <DropdownMenuContent className="flex flex-col gap-0.5" align="end">
{localeItems.map((item) => ( {localeItems.map((item, index) => (
<DropdownMenuItem key={item.code} onSelect={(e) => handleSelect(e, item.code)} className={locale === item.code ? "bg-muted gap-3" : ""}> <DropdownMenuItem
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />} key={item.code}
onSelect={(e) => handleSelect(e, item.code)}
className={cn(
"text-xs",
{
"gap-3 bg-muted font-semibold": locale === item.code,
},
{
"rounded-t-[5px]": index === localeItems.length - 1,
"rounded-[5px]":
index !== 0 && index !== localeItems.length - 1,
"rounded-b-[5px]": index === 0,
},
)}
>
{item.name}{" "}
{locale === item.code && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) );
} }
+33 -17
View File
@@ -1,13 +1,15 @@
import useTooltip from "@/hooks/use-tooltip" import { AnimatePresence, m } from "framer-motion";
import { AnimatePresence, m } from "framer-motion" import { memo } from "react";
import { memo } from "react" import { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom";
import useTooltip from "@/hooks/use-tooltip";
const MapTooltip = memo(function MapTooltip() { const MapTooltip = memo(function MapTooltip() {
const { t } = useTranslation() const { t } = useTranslation();
const { tooltipData } = useTooltip() const navigate = useNavigate();
const { tooltipData } = useTooltip();
if (!tooltipData) return null if (!tooltipData) return null;
return ( return (
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
@@ -23,12 +25,16 @@ const MapTooltip = memo(function MapTooltip() {
transform: "translate(20%, -50%)", transform: "translate(20%, -50%)",
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.stopPropagation() e.stopPropagation();
}} }}
> >
<div> <div>
<p className="font-medium">{tooltipData.country === "China" ? "Mainland China" : tooltipData.country}</p> <p className="font-medium">
<p className="text-neutral-600 dark:text-neutral-400 mb-1"> {tooltipData.country === "China"
? "Mainland China"
: tooltipData.country}
</p>
<p className="text-neutral-600 dark:text-neutral-400 text-xs font-light mb-1">
{tooltipData.count} {t("map.Servers")} {tooltipData.count} {t("map.Servers")}
</p> </p>
</div> </div>
@@ -39,16 +45,26 @@ const MapTooltip = memo(function MapTooltip() {
overflowY: "auto", overflowY: "auto",
}} }}
> >
{tooltipData.servers.map((server, index: number) => ( {tooltipData.servers.map((server) => (
<div key={index} className="flex items-center gap-1.5 py-0.5"> <button
<span className={`w-1.5 h-1.5 shrink-0 rounded-full ${server.status ? "bg-green-500" : "bg-red-500"}`}></span> key={server.id}
type="button"
className="flex items-center gap-1.5 py-0.5 text-neutral-500 transition-colors hover:text-black dark:text-neutral-400 dark:hover:text-white"
onClick={() => {
sessionStorage.setItem("fromMainPage", "true");
navigate(`/server/${server.id}`);
}}
>
<span
className={`h-1.5 w-1.5 shrink-0 rounded-full ${server.status ? "bg-green-500" : "bg-red-500"}`}
/>
<span className="text-xs">{server.name}</span> <span className="text-xs">{server.name}</span>
</div> </button>
))} ))}
</div> </div>
</m.div> </m.div>
</AnimatePresence> </AnimatePresence>
) );
}) });
export default MapTooltip export default MapTooltip;
+583 -153
View File
@@ -1,56 +1,200 @@
"use client" "use client";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { useQuery } from "@tanstack/react-query";
import { ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart" import { m } from "framer-motion";
import { fetchMonitor } from "@/lib/nezha-api" import * as React from "react";
import { cn, formatTime } from "@/lib/utils" import { useCallback, useMemo } from "react";
import { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api" import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query" import {
import * as React from "react" Area,
import { useCallback, useMemo } from "react" CartesianGrid,
import { useTranslation } from "react-i18next" ComposedChart,
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts" Line,
XAxis,
YAxis,
} from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
fetchLoginUser,
fetchMonitor,
type MonitorPeriod,
} from "@/lib/nezha-api";
import { cn, formatTime } from "@/lib/utils";
import type { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api";
import NetworkChartLoading from "./NetworkChartLoading" import NetworkChartLoading from "./NetworkChartLoading";
import { Label } from "./ui/label" import { Label } from "./ui/label";
import { Switch } from "./ui/switch" import { Switch } from "./ui/switch";
interface ResultItem { interface ResultItem {
created_at: number created_at: number;
[key: string]: number [key: string]: number;
} }
export function NetworkChart({ server_id, show }: { server_id: number; show: boolean }) { /**
const { t } = useTranslation() * Helper method to calculate packet loss from delay data
*/
const calculatePacketLoss = (delays: number[]): number[] => {
if (!delays || delays.length === 0) return [];
const packetLossRates: number[] = [];
const windowSize = Math.min(10, Math.max(3, Math.floor(delays.length / 10)));
const timeoutThreshold = 3000;
const extremeDelayThreshold = 10000;
for (let i = 0; i < delays.length; i++) {
const currentDelay = delays[i];
let lossRate = 0;
if (
currentDelay === 0 ||
currentDelay === null ||
currentDelay === undefined
) {
lossRate = 100;
} else if (currentDelay >= extremeDelayThreshold) {
lossRate = Math.min(
95,
60 + (currentDelay - extremeDelayThreshold) / 1000,
);
} else if (currentDelay >= timeoutThreshold) {
lossRate = Math.min(50, (currentDelay - timeoutThreshold) / 200);
} else {
const start = Math.max(0, i - Math.floor(windowSize / 2));
const end = Math.min(delays.length, i + Math.ceil(windowSize / 2));
const windowDelays = delays.slice(start, end).filter((d) => d > 0);
if (windowDelays.length > 2) {
const mean =
windowDelays.reduce((sum, d) => sum + d, 0) / windowDelays.length;
const variance =
windowDelays.reduce((sum, d) => sum + (d - mean) ** 2, 0) /
windowDelays.length;
const standardDeviation = Math.sqrt(variance);
const coefficientOfVariation = standardDeviation / mean;
if (coefficientOfVariation > 0.8) {
lossRate = Math.min(25, coefficientOfVariation * 15);
} else if (coefficientOfVariation > 0.5) {
lossRate = Math.min(10, coefficientOfVariation * 8);
} else if (coefficientOfVariation > 0.3) {
lossRate = Math.min(5, coefficientOfVariation * 5);
}
if (currentDelay > mean * 2.5) {
lossRate += Math.min(15, (currentDelay / mean - 2.5) * 10);
}
}
}
if (i > 0) {
const alpha = 0.3;
lossRate = alpha * lossRate + (1 - alpha) * packetLossRates[i - 1];
}
packetLossRates.push(Math.max(0, Math.min(100, lossRate)));
}
return packetLossRates.map((rate) => Number(rate.toFixed(2)));
};
export function NetworkChart({
server_id,
show,
}: {
server_id: number;
show: boolean;
}) {
const { t } = useTranslation();
const [period, setPeriod] = React.useState<MonitorPeriod>("1d");
const { data: userData, isError: isLoginError } = useQuery({
queryKey: ["login-user"],
queryFn: () => fetchLoginUser(),
refetchOnMount: false,
refetchOnWindowFocus: true,
refetchIntervalInBackground: true,
refetchInterval: 1000 * 30,
retry: 0,
});
const isLogin = isLoginError
? false
: userData
? !!userData?.data?.id && !!document.cookie
: false;
React.useEffect(() => {
if (!isLogin && period !== "1d") {
setPeriod("1d");
}
}, [isLogin, period]);
const { data: monitorData } = useQuery({ const { data: monitorData } = useQuery({
queryKey: ["monitor", server_id], queryKey: ["monitor", server_id, period],
queryFn: () => fetchMonitor(server_id), queryFn: () => fetchMonitor(server_id, period),
enabled: show, enabled: show,
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
refetchInterval: 10000, refetchInterval: 10000,
}) });
if (!monitorData) return <NetworkChartLoading /> if (!monitorData) return <NetworkChartLoading />;
if (monitorData?.success && !monitorData.data) { if (monitorData?.success && !monitorData.data) {
return ( return (
<> <>
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40"></p> <p className="text-sm font-medium opacity-40"></p>
<p className="text-sm font-medium opacity-40 mb-4">{t("monitor.noData")}</p> <p className="text-sm font-medium opacity-40 mb-4">
{t("monitor.noData")}
</p>
</div> </div>
<NetworkChartLoading /> <NetworkChartLoading />
</> </>
) );
} }
const transformedData = transformData(monitorData.data) const transformedData = transformData(monitorData.data);
const formattedData = formatData(monitorData.data) const formattedData = formatData(monitorData.data);
const chartDataKey = Object.keys(transformedData) const monitorInfoByName = new Map(
monitorData.data.map((item) => [
item.monitor_name,
{ id: item.monitor_id, displayIndex: item.display_index },
]),
);
const chartDataKey = Object.keys(transformedData).sort((a, b) => {
const aInfo = monitorInfoByName.get(a);
const bInfo = monitorInfoByName.get(b);
if (!aInfo && !bInfo) return a.localeCompare(b);
if (!aInfo) return 1;
if (!bInfo) return -1;
const indexDiff = (bInfo.displayIndex ?? 0) - (aInfo.displayIndex ?? 0);
if (indexDiff !== 0) return indexDiff;
return aInfo.id - bInfo.id;
});
const initChartConfig = { const initChartConfig = {
avg_delay: { avg_delay: {
@@ -59,10 +203,10 @@ export function NetworkChart({ server_id, show }: { server_id: number; show: boo
...chartDataKey.reduce((acc, key) => { ...chartDataKey.reduce((acc, key) => {
acc[key] = { acc[key] = {
label: key, label: key,
} };
return acc return acc;
}, {} as ChartConfig), }, {} as ChartConfig),
} satisfies ChartConfig } satisfies ChartConfig;
return ( return (
<NetworkChartClient <NetworkChartClient
@@ -71,8 +215,11 @@ export function NetworkChart({ server_id, show }: { server_id: number; show: boo
chartData={transformedData} chartData={transformedData}
serverName={monitorData.data[0].server_name} serverName={monitorData.data[0].server_name}
formattedData={formattedData} formattedData={formattedData}
period={period}
onPeriodChange={setPeriod}
isLogin={isLogin}
/> />
) );
} }
export const NetworkChartClient = React.memo(function NetworkChart({ export const NetworkChartClient = React.memo(function NetworkChart({
@@ -81,83 +228,185 @@ export const NetworkChartClient = React.memo(function NetworkChart({
chartData, chartData,
serverName, serverName,
formattedData, formattedData,
period,
onPeriodChange,
isLogin,
}: { }: {
chartDataKey: string[] chartDataKey: string[];
chartConfig: ChartConfig chartConfig: ChartConfig;
chartData: ServerMonitorChart chartData: ServerMonitorChart;
serverName: string serverName: string;
formattedData: ResultItem[] formattedData: ResultItem[];
period: MonitorPeriod;
onPeriodChange: (period: MonitorPeriod) => void;
isLogin: boolean;
}) { }) {
const { t } = useTranslation() const { t } = useTranslation();
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const TIME_RANGE_OPTIONS: { value: MonitorPeriod; label: string }[] = [
{ value: "1d", label: t("monitor.period1d") },
{ value: "7d", label: t("monitor.period7d") },
{ value: "30d", label: t("monitor.period30d") },
];
const forcePeakCutEnabled = (window.ForcePeakCutEnabled as boolean) ?? false const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const forcePeakCutEnabled = (window.ForcePeakCutEnabled as boolean) ?? false;
// Change from string to string array for multi-selection // Change from string to string array for multi-selection
const [activeCharts, setActiveCharts] = React.useState<string[]>([]) const [activeCharts, setActiveCharts] = React.useState<string[]>([]);
const [isPeakEnabled, setIsPeakEnabled] = React.useState(forcePeakCutEnabled) const [isPeakEnabled, setIsPeakEnabled] = React.useState(forcePeakCutEnabled);
// Function to clear all selected charts // Function to clear all selected charts
const clearAllSelections = useCallback(() => { const clearAllSelections = useCallback(() => {
setActiveCharts([]) setActiveCharts([]);
}, []) }, []);
// Updated to handle multiple selections // Updated to handle multiple selections
const handleButtonClick = useCallback((chart: string) => { const handleButtonClick = useCallback((chart: string) => {
setActiveCharts((prev) => { setActiveCharts((prev) => {
// If chart is already selected, remove it // If chart is already selected, remove it
if (prev.includes(chart)) { if (prev.includes(chart)) {
return prev.filter((c) => c !== chart) return prev.filter((c) => c !== chart);
} }
// Otherwise, add it to selected charts // Otherwise, add it to selected charts
return [...prev, chart] return [...prev, chart];
}) });
}, []) }, []);
const getColorByIndex = useCallback( const getColorByIndex = useCallback(
(chart: string) => { (chart: string) => {
const index = chartDataKey.indexOf(chart) const index = chartDataKey.indexOf(chart);
return `hsl(var(--chart-${(index % 10) + 1}))` return `hsl(var(--chart-${(index % 10) + 1}))`;
}, },
[chartDataKey], [chartDataKey],
) );
const chartStats = useMemo(() => {
const stats: { [key: string]: { minDelay: number; maxDelay: number } } = {};
for (const key of chartDataKey) {
const data = chartData[key] || [];
if (data.length > 0) {
const delays = data.map((item) => item.avg_delay);
const minDelay = Math.min(...delays);
const maxDelay = Math.max(...delays);
stats[key] = { minDelay, maxDelay };
} else {
stats[key] = { minDelay: 0, maxDelay: 0 };
}
}
return stats;
}, [chartDataKey, chartData]);
const chartButtons = useMemo( const chartButtons = useMemo(
() => () =>
chartDataKey.map((key) => ( chartDataKey.map((key) => {
const monitorData = chartData[key];
const lastDelay = monitorData[monitorData.length - 1].avg_delay;
const stats = chartStats[key];
// Calculate average packet loss if available
const packetLossData = monitorData.reduce<number[]>((acc, item) => {
if (item.packet_loss !== undefined) {
acc.push(item.packet_loss);
}
return acc;
}, []);
const avgPacketLoss =
packetLossData.length > 0
? packetLossData.reduce((sum, loss) => sum + loss, 0) /
packetLossData.length
: null;
return (
<button <button
key={key} key={key}
data-active={activeCharts.includes(key)} data-active={activeCharts.includes(key)}
className={`relative z-30 flex cursor-pointer grow basis-0 flex-col justify-center gap-1 border-b border-neutral-200 dark:border-neutral-800 px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6`} className={`relative z-30 flex cursor-pointer grow basis-0 flex-col justify-center gap-1 border-b border-neutral-200 dark:border-neutral-800 px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6`}
onClick={() => handleButtonClick(key)} onClick={() => handleButtonClick(key)}
> >
<span className="whitespace-nowrap text-xs text-muted-foreground">{key}</span> <span className="whitespace-nowrap text-xs text-muted-foreground">
<span className="text-md font-bold leading-none sm:text-lg">{chartData[key][chartData[key].length - 1].avg_delay.toFixed(2)}ms</span> {key}
</span>
<div className="flex flex-col gap-0.5">
<span className="text-md font-semibold leading-none sm:text-xl">
{lastDelay.toFixed(2)}ms
</span>
<div className="flex items-center gap-2 text-[12px]">
<span className="text-green-600 dark:text-green-400">
{stats.minDelay.toFixed(0)}
</span>
<span className="text-red-600 dark:text-red-500">
{stats.maxDelay.toFixed(0)}
</span>
{avgPacketLoss !== null && (
<span className="text-muted-foreground flex items-center gap-1">
{avgPacketLoss.toFixed(2)}%
</span>
)}
</div>
</div>
</button> </button>
)), );
[chartDataKey, activeCharts, chartData, handleButtonClick], }),
) [chartDataKey, activeCharts, chartData, chartStats, handleButtonClick],
);
const chartLines = useMemo(() => { const chartElements = useMemo(() => {
// If we have active charts selected, render only those const elements = [];
if (activeCharts.length > 0) {
return activeCharts.map((chart) => ( // If exactly one chart is selected, show delay line and packet loss area
if (activeCharts.length === 1) {
const chart = activeCharts[0];
elements.push(
<Area
key="packet-loss-area"
isAnimationActive={false}
dataKey="packet_loss"
stroke="none"
fill="hsl(45, 100%, 60%)"
fillOpacity={0.3}
yAxisId="packet-loss"
/>,
<Line
key="delay-line"
isAnimationActive={false}
strokeWidth={1}
type="linear"
dot={false}
dataKey="avg_delay"
stroke={getColorByIndex(chart)}
yAxisId="delay"
connectNulls={true}
/>,
);
} else if (activeCharts.length > 1) {
// Multiple charts selected - show only delay lines for selected monitors
elements.push(
...activeCharts.map((chart) => (
<Line <Line
key={chart} key={chart}
isAnimationActive={false} isAnimationActive={false}
strokeWidth={1} strokeWidth={1}
type="linear" type="linear"
dot={false} dot={false}
dataKey={chart} // Change from "avg_delay" to the actual chart key name dataKey={chart}
stroke={getColorByIndex(chart)} stroke={getColorByIndex(chart)}
name={chart} name={chart}
connectNulls={true} connectNulls={true}
yAxisId="delay"
/> />
)) )),
} );
// Otherwise show all charts (default view) } else {
return chartDataKey.map((key) => ( // No selection - show all charts (default view)
elements.push(
...chartDataKey.map((key) => (
<Line <Line
key={key} key={key}
isAnimationActive={false} isAnimationActive={false}
@@ -167,106 +416,206 @@ export const NetworkChartClient = React.memo(function NetworkChart({
dataKey={key} dataKey={key}
stroke={getColorByIndex(key)} stroke={getColorByIndex(key)}
connectNulls={true} connectNulls={true}
yAxisId="delay"
/> />
)) )),
}, [activeCharts, chartDataKey, getColorByIndex]) );
const processedData = useMemo(() => {
if (!isPeakEnabled) {
// Always use formattedData when multiple charts are selected or none selected
return formattedData
} }
// For peak cutting, always use the formatted data which contains all series return elements;
const data = formattedData }, [activeCharts, chartDataKey, getColorByIndex]);
const windowSize = 11 // 增加窗口大小以获取更好的统计效果 const processedData = useMemo(() => {
const alpha = 0.3 // EWMA平滑因子 // Special handling for single chart selection
let baseData = formattedData;
if (activeCharts.length === 1) {
const selectedChart = activeCharts[0];
baseData = chartData[selectedChart].map((item) => ({
created_at: item.created_at,
avg_delay: item.avg_delay,
packet_loss: item.packet_loss ?? 0,
}));
}
if (!isPeakEnabled) {
return baseData;
}
// For peak cutting, use the base data
const data = baseData;
const windowSize = 11; // 增加窗口大小以获取更好的统计效果
const alpha = 0.3; // EWMA平滑因子
// 辅助函数:计算中位数 // 辅助函数:计算中位数
const getMedian = (arr: number[]) => { const getMedian = (arr: number[]) => {
const sorted = [...arr].sort((a, b) => a - b) const sorted = [...arr].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2) const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2 return sorted.length % 2
} ? sorted[mid]
: (sorted[mid - 1] + sorted[mid]) / 2;
};
// 辅助函数:异常值处理 // 辅助函数:异常值处理
const processValues = (values: number[]) => { const processValues = (values: number[]) => {
if (values.length === 0) return null if (values.length === 0) return null;
const median = getMedian(values) const median = getMedian(values);
const deviations = values.map((v) => Math.abs(v - median)) const deviations = values.map((v) => Math.abs(v - median));
const medianDeviation = getMedian(deviations) * 1.4826 // MAD估计器 const medianDeviation = getMedian(deviations) * 1.4826; // MAD估计器
// 使用中位数绝对偏差(MAD)进行异常值检测 // 使用中位数绝对偏差(MAD)进行异常值检测
const validValues = values.filter( const validValues = values.filter(
(v) => (v) =>
Math.abs(v - median) <= 3 * medianDeviation && // 更严格的异常值判定 Math.abs(v - median) <= 3 * medianDeviation && // 更严格的异常值判定
v <= median * 3, // 限制最大值不超过中位数的3倍 v <= median * 3, // 限制最大值不超过中位数的3倍
) );
if (validValues.length === 0) return median // 如果没有有效值,返回中位数 if (validValues.length === 0) return median; // 如果没有有效值,返回中位数
// 计算EWMA // 计算EWMA
let ewma = validValues[0] let ewma = validValues[0];
for (let i = 1; i < validValues.length; i++) { for (let i = 1; i < validValues.length; i++) {
ewma = alpha * validValues[i] + (1 - alpha) * ewma ewma = alpha * validValues[i] + (1 - alpha) * ewma;
} }
return ewma return ewma;
} };
// 初始化EWMA历史值 // 初始化EWMA历史值
const ewmaHistory: { [key: string]: number } = {} const ewmaHistory: { [key: string]: number } = {};
return data.map((point, index) => { return data.map((point, index) => {
if (index < windowSize - 1) return point if (index < windowSize - 1) return point;
const window = data.slice(index - windowSize + 1, index + 1) const window = data.slice(index - windowSize + 1, index + 1);
const smoothed = { ...point } as ResultItem const smoothed = { ...point } as ResultItem;
// Process all chart keys or just the selected ones // Special handling for single chart selection
const keysToProcess = activeCharts.length > 0 ? activeCharts : chartDataKey if (activeCharts.length === 1) {
// Process avg_delay for single chart
keysToProcess.forEach((key) => { const values = window
const values = window.map((w) => w[key]).filter((v) => v !== undefined && v !== null) as number[] .map((w) => w.avg_delay as number)
.filter((v) => v !== undefined && v !== null);
if (values.length > 0) { if (values.length > 0) {
const processed = processValues(values) const processed = processValues(values);
if (processed !== null) {
if (ewmaHistory.avg_delay === undefined) {
ewmaHistory.avg_delay = processed;
} else {
ewmaHistory.avg_delay =
alpha * processed + (1 - alpha) * ewmaHistory.avg_delay;
}
smoothed.avg_delay = ewmaHistory.avg_delay;
}
}
} else {
// Process all chart keys or just the selected ones
const keysToProcess =
activeCharts.length > 0 ? activeCharts : chartDataKey;
keysToProcess.forEach((key) => {
const values = window
.map((w) => w[key])
.filter((v) => v !== undefined && v !== null) as number[];
if (values.length > 0) {
const processed = processValues(values);
if (processed !== null) { if (processed !== null) {
// Apply EWMA smoothing // Apply EWMA smoothing
if (ewmaHistory[key] === undefined) { if (ewmaHistory[key] === undefined) {
ewmaHistory[key] = processed ewmaHistory[key] = processed;
} else { } else {
ewmaHistory[key] = alpha * processed + (1 - alpha) * ewmaHistory[key] ewmaHistory[key] =
alpha * processed + (1 - alpha) * ewmaHistory[key];
} }
smoothed[key] = ewmaHistory[key] smoothed[key] = ewmaHistory[key];
} }
} }
}) });
}
return smoothed return smoothed;
}) });
}, [isPeakEnabled, activeCharts, formattedData, chartDataKey]) }, [isPeakEnabled, activeCharts, formattedData, chartData, chartDataKey]);
return ( return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3 sm:-mt-5 -mt-3 flex-wrap">
<TooltipProvider delayDuration={120}>
<div className="flex items-center gap-1 rounded-full bg-muted dark:bg-muted/40 p-0.5 border border-border/60 dark:border-border">
{TIME_RANGE_OPTIONS.map((option) => {
const isLocked = !isLogin && option.value !== "1d";
const optionItem = (
<div
onClick={() => {
if (!isLocked) {
onPeriodChange(option.value);
}
}}
className={cn(
"relative cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors duration-300",
period === option.value
? "text-foreground"
: "text-muted-foreground hover:text-foreground",
isLocked && "cursor-not-allowed opacity-40 grayscale",
)}
>
{period === option.value && (
<m.div
layoutId="network-period-selector-active"
className="absolute inset-0 z-10 h-full w-full bg-white dark:bg-background rounded-full ring-1 ring-border/60 dark:ring-border/40"
transition={{
type: "spring",
stiffness: 400,
damping: 30,
}}
/>
)}
<span className="relative z-20">{option.label}</span>
</div>
);
if (isLocked) {
return (
<Tooltip key={option.value}>
<TooltipTrigger asChild>{optionItem}</TooltipTrigger>
<TooltipContent>
{t("monitor.loginRequired", "Please login to view")}
</TooltipContent>
</Tooltip>
);
}
return <div key={option.value}>{optionItem}</div>;
})}
</div>
</TooltipProvider>
<div className="flex items-center space-x-2">
<Switch
id="Peak"
checked={isPeakEnabled}
onCheckedChange={setIsPeakEnabled}
/>
<Label className="text-xs" htmlFor="Peak">
{t("monitor.peakCut")}
</Label>
</div>
</div>
<Card <Card
className={cn({ className={cn({
"bg-card/70": customBackgroundImage, "bg-card/70": customBackgroundImage,
})} })}
> >
<CardHeader className="flex flex-col items-stretch space-y-0 p-0 sm:flex-row"> <CardHeader className="flex flex-col items-stretch space-y-0 overflow-hidden rounded-t-lg p-0 sm:flex-row">
<div className="flex flex-none flex-col justify-center gap-1 border-b px-6 py-4"> <div className="flex flex-none flex-col justify-center gap-1 border-b px-6 py-4">
<CardTitle className="flex flex-none items-center gap-0.5 text-md">{serverName}</CardTitle> <CardTitle className="flex flex-none items-center gap-0.5 text-md">
{serverName}
</CardTitle>
<CardDescription className="text-xs"> <CardDescription className="text-xs">
{chartDataKey.length} {t("monitor.monitorCount")} {chartDataKey.length} {t("monitor.monitorCount")}
</CardDescription> </CardDescription>
<div className="flex items-center mt-0.5 space-x-2">
<Switch id="Peak" checked={isPeakEnabled} onCheckedChange={setIsPeakEnabled} />
<Label className="text-xs" htmlFor="Peak">
Peak cut
</Label>
</div>
</div> </div>
<div className="flex flex-wrap w-full">{chartButtons}</div> <div className="flex flex-wrap w-full">{chartButtons}</div>
</CardHeader> </CardHeader>
@@ -274,14 +623,21 @@ export const NetworkChartClient = React.memo(function NetworkChart({
<div className="relative"> <div className="relative">
{activeCharts.length > 0 && ( {activeCharts.length > 0 && (
<button <button
className="absolute -top-2 right-1 z-10 text-xs px-2 py-1 bg-stone-100/80 dark:bg-stone-800/80 backdrop-blur-sm rounded-[5px] text-muted-foreground hover:text-foreground transition-colors" className="absolute -top-2 right-1 z-10 text-xs px-2 py-1 bg-stone-100/80 dark:bg-stone-800/80 backdrop-blur-xs rounded-[5px] text-muted-foreground hover:text-foreground transition-colors"
onClick={clearAllSelections} onClick={clearAllSelections}
> >
{t("monitor.clearSelections", "Clear")} ({activeCharts.length}) {t("monitor.clearSelections", "Clear")} ({activeCharts.length})
</button> </button>
)} )}
<ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full"> <ChartContainer
<LineChart accessibilityLayer data={processedData} margin={{ left: 12, right: 12 }}> config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<ComposedChart
accessibilityLayer
data={processedData}
margin={{ left: 12, right: 12 }}
>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<XAxis <XAxis
dataKey="created_at" dataKey="created_at"
@@ -293,30 +649,58 @@ export const NetworkChartClient = React.memo(function NetworkChart({
ticks={processedData ticks={processedData
.filter((item, index, array) => { .filter((item, index, array) => {
if (array.length < 6) { if (array.length < 6) {
return index === 0 || index === array.length - 1 return index === 0 || index === array.length - 1;
} }
// 计算数据的总时间跨度(毫秒) // 计算数据的总时间跨度(毫秒)
const timeSpan = array[array.length - 1].created_at - array[0].created_at const timeSpan =
const hours = timeSpan / (1000 * 60 * 60) array[array.length - 1].created_at -
array[0].created_at;
const hours = timeSpan / (1000 * 60 * 60);
// 根据时间跨度调整显示间隔 // 根据时间跨度调整显示间隔
if (hours <= 12) { if (hours <= 12) {
// 12小时内,每60分钟显示一个刻度 // 12小时内,每60分钟显示一个刻度
return index === 0 || index === array.length - 1 || new Date(item.created_at).getMinutes() % 60 === 0 return (
index === 0 ||
index === array.length - 1 ||
new Date(item.created_at).getMinutes() % 60 === 0
);
} }
// 超过12小时,每2小时显示一个刻度 // 超过12小时,每2小时显示一个刻度
const date = new Date(item.created_at) const date = new Date(item.created_at);
return date.getMinutes() === 0 && date.getHours() % 2 === 0 return (
date.getMinutes() === 0 && date.getHours() % 2 === 0
);
}) })
.map((item) => item.created_at)} .map((item) => item.created_at)}
tickFormatter={(value) => { tickFormatter={(value) => {
const date = new Date(value) const date = new Date(value);
const minutes = date.getMinutes() const minutes = date.getMinutes();
return minutes === 0 ? `${date.getHours()}:00` : `${date.getHours()}:${minutes}` return minutes === 0
? `${date.getHours()}:00`
: `${date.getHours()}:${minutes}`;
}} }}
/> />
<YAxis tickLine={false} axisLine={false} tickMargin={15} minTickGap={20} tickFormatter={(value) => `${value}ms`} /> <YAxis
yAxisId="delay"
tickLine={false}
axisLine={false}
tickMargin={15}
minTickGap={20}
tickFormatter={(value) => `${value}ms`}
/>
{activeCharts.length === 1 && (
<YAxis
yAxisId="packet-loss"
orientation="right"
tickLine={false}
axisLine={false}
tickMargin={15}
minTickGap={20}
tickFormatter={(value) => `${value}%`}
/>
)}
<ChartTooltip <ChartTooltip
isAnimationActive={false} isAnimationActive={false}
content={ content={
@@ -324,65 +708,111 @@ export const NetworkChartClient = React.memo(function NetworkChart({
indicator={"line"} indicator={"line"}
labelKey="created_at" labelKey="created_at"
labelFormatter={(_, payload) => { labelFormatter={(_, payload) => {
return formatTime(payload[0].payload.created_at) return formatTime(payload[0].payload.created_at);
}}
formatter={(value, name) => {
let formattedValue: string;
let label: string;
if (name === "packet_loss") {
formattedValue = `${Number(value).toFixed(2)}%`;
label = t("monitor.packetLoss", "Packet Loss");
} else if (name === "avg_delay") {
formattedValue = `${Number(value).toFixed(2)}ms`;
label = t("monitor.avgDelay", "Avg Delay");
} else {
// For monitor names (in multi-chart view) - delay data
formattedValue = `${Number(value).toFixed(2)}ms`;
label = name as string;
}
return (
<div className="flex flex-1 items-center justify-between leading-none">
<span className="text-muted-foreground">
{label}
</span>
<span className="ml-2 font-medium text-foreground tabular-nums">
{formattedValue}
</span>
</div>
);
}} }}
/> />
} }
/> />
{activeCharts.length !== 1 && (
<ChartLegend content={<ChartLegendContent />} /> <ChartLegend content={<ChartLegendContent />} />
{chartLines} )}
</LineChart> {chartElements}
</ComposedChart>
</ChartContainer> </ChartContainer>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
) </div>
}) );
});
const transformData = (data: NezhaMonitor[]) => { const transformData = (data: NezhaMonitor[]) => {
const monitorData: ServerMonitorChart = {} const monitorData: ServerMonitorChart = {};
data.forEach((item) => { data.forEach((item) => {
const monitorName = item.monitor_name const monitorName = item.monitor_name;
if (!monitorData[monitorName]) { if (!monitorData[monitorName]) {
monitorData[monitorName] = [] monitorData[monitorName] = [];
} }
// Calculate packet loss from delay data if not provided
const packetLoss = item.packet_loss || calculatePacketLoss(item.avg_delay);
for (let i = 0; i < item.created_at.length; i++) { for (let i = 0; i < item.created_at.length; i++) {
monitorData[monitorName].push({ monitorData[monitorName].push({
created_at: item.created_at[i], created_at: item.created_at[i],
avg_delay: item.avg_delay[i], avg_delay: item.avg_delay[i],
}) packet_loss: packetLoss[i],
});
} }
}) });
return monitorData return monitorData;
} };
const formatData = (rawData: NezhaMonitor[]) => { const formatData = (rawData: NezhaMonitor[]) => {
const result: { [time: number]: ResultItem } = {} const result: { [time: number]: ResultItem } = {};
const allTimes = new Set<number>() const allTimes = new Set<number>();
rawData.forEach((item) => { rawData.forEach((item) => {
item.created_at.forEach((time) => allTimes.add(time)) item.created_at.forEach((time) => {
}) allTimes.add(time);
});
});
const allTimeArray = Array.from(allTimes).sort((a, b) => a - b) const allTimeArray = Array.from(allTimes).sort((a, b) => a - b);
rawData.forEach((item) => { rawData.forEach((item) => {
const { monitor_name, created_at, avg_delay } = item const { monitor_name, created_at, avg_delay } = item;
// Calculate packet loss if not provided
const packetLoss = item.packet_loss || calculatePacketLoss(avg_delay);
allTimeArray.forEach((time) => { allTimeArray.forEach((time) => {
if (!result[time]) { if (!result[time]) {
result[time] = { created_at: time } result[time] = { created_at: time };
} }
const timeIndex = created_at.indexOf(time) const timeIndex = created_at.indexOf(time);
// @ts-expect-error - avg_delay is an array // @ts-expect-error - avg_delay is an array
result[time][monitor_name] = timeIndex !== -1 ? avg_delay[timeIndex] : null result[time][monitor_name] =
}) timeIndex !== -1 ? avg_delay[timeIndex] : null;
}) // Add packet loss data if available
if (packetLoss) {
// @ts-expect-error - packet_loss is calculated
result[time][`${monitor_name}_packet_loss`] =
timeIndex !== -1 ? packetLoss[timeIndex] : null;
}
});
});
return Object.values(result).sort((a, b) => a.created_at - b.created_at) return Object.values(result).sort((a, b) => a.created_at - b.created_at);
} };
+3 -3
View File
@@ -1,5 +1,5 @@
import { Loader } from "@/components/loading/Loader" import { Loader } from "@/components/loading/Loader";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function NetworkChartLoading() { export default function NetworkChartLoading() {
return ( return (
@@ -19,5 +19,5 @@ export default function NetworkChartLoading() {
<div className="aspect-auto h-[250px] w-full"></div> <div className="aspect-auto h-[250px] w-full"></div>
</CardContent> </CardContent>
</Card> </Card>
) );
} }
+41 -14
View File
@@ -1,8 +1,12 @@
import { PublicNoteData, cn } from "@/lib/utils" import { cn, type PublicNoteData } from "@/lib/utils";
export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData }) { export default function PlanInfo({
parsedData,
}: {
parsedData: PublicNoteData;
}) {
if (!parsedData || !parsedData.planDataMod) { if (!parsedData || !parsedData.planDataMod) {
return null return null;
} }
const extraList = const extraList =
@@ -10,36 +14,57 @@ export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData })
? parsedData.planDataMod.extra.split(",") ? parsedData.planDataMod.extra.split(",")
: parsedData.planDataMod.extra.split(",")[0] === "" : parsedData.planDataMod.extra.split(",")[0] === ""
? [] ? []
: [parsedData.planDataMod.extra] : [parsedData.planDataMod.extra];
const networkRoutes = parsedData.planDataMod.networkRoute
? parsedData.planDataMod.networkRoute.split(",")
: [];
return ( return (
<section className="flex gap-1 items-center flex-wrap mt-0.5"> <section className="flex gap-1 items-center flex-wrap mt-0.5">
{parsedData.planDataMod.bandwidth !== "" && ( {parsedData.planDataMod.bandwidth !== "" && (
<p className={cn("text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}> <p
className={cn(
"text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.bandwidth} {parsedData.planDataMod.bandwidth}
</p> </p>
)} )}
{parsedData.planDataMod.trafficVol !== "" && ( {parsedData.planDataMod.trafficVol !== "" && (
<p className={cn("text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}> <p
className={cn(
"text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{parsedData.planDataMod.trafficVol} {parsedData.planDataMod.trafficVol}
</p> </p>
)} )}
{parsedData.planDataMod.IPv4 === "1" && ( {parsedData.planDataMod.IPv4 === "1" && (
<p <p
className={cn("text-[9px] bg-purple-600 text-purple-200 dark:bg-purple-800 dark:text-purple-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")} className={cn(
"text-[9px] bg-purple-600 text-purple-200 dark:bg-purple-800 dark:text-purple-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
> >
IPv4 IPv4
</p> </p>
)} )}
{parsedData.planDataMod.IPv6 === "1" && ( {parsedData.planDataMod.IPv6 === "1" && (
<p className={cn("text-[9px] bg-pink-600 text-pink-200 dark:bg-pink-800 dark:text-pink-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}> <p
className={cn(
"text-[9px] bg-pink-600 text-pink-200 dark:bg-pink-800 dark:text-pink-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
IPv6 IPv6
</p> </p>
)} )}
{parsedData.planDataMod.networkRoute && ( {parsedData.planDataMod.networkRoute && (
<p className={cn("text-[9px] bg-blue-600 text-blue-200 dark:bg-blue-800 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}> <p
{parsedData.planDataMod.networkRoute.split(",").map((route, index) => { className={cn(
return route + (index === parsedData.planDataMod!.networkRoute.split(",").length - 1 ? "" : "") "text-[9px] bg-blue-600 text-blue-200 dark:bg-blue-800 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{networkRoutes.map((route, index) => {
return route + (index === networkRoutes.length - 1 ? "" : "");
})} })}
</p> </p>
)} )}
@@ -47,12 +72,14 @@ export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData })
return ( return (
<p <p
key={index} key={index}
className={cn("text-[9px] bg-stone-600 text-stone-200 dark:bg-stone-800 dark:text-stone-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")} className={cn(
"text-[9px] bg-stone-600 text-stone-200 dark:bg-stone-800 dark:text-stone-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
> >
{extra} {extra}
</p> </p>
) );
})} })}
</section> </section>
) );
} }
+17 -5
View File
@@ -1,15 +1,27 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Progress } from "./ui/progress" import { Progress } from "./ui/progress";
export default function RemainPercentBar({ value, className }: { value: number; className?: string }) { export default function RemainPercentBar({
value,
className,
}: {
value: number;
className?: string;
}) {
return ( return (
<Progress <Progress
aria-label={"Server Usage Bar"} aria-label={"Server Usage Bar"}
aria-labelledby={"Server Usage Bar"} aria-labelledby={"Server Usage Bar"}
value={value} value={value}
indicatorClassName={value < 30 ? "bg-red-500" : value < 70 ? "bg-orange-400" : "bg-green-500"} indicatorClassName={
value < 30
? "bg-red-500"
: value < 70
? "bg-orange-400"
: "bg-green-500"
}
className={cn("h-[3px] rounded-sm w-[70px]", className)} className={cn("h-[3px] rounded-sm w-[70px]", className)}
/> />
) );
} }
+31
View File
@@ -0,0 +1,31 @@
"use client";
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
import { useCommand } from "@/hooks/use-command";
import { cn } from "@/lib/utils";
import { Button } from "./ui/button";
export function SearchButton() {
const { openCommand } = useCommand();
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
return (
<Button
variant="outline"
size="sm"
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
"bg-white/70 dark:bg-black/70": customBackgroundImage,
})}
onClick={openCommand}
title="Search"
>
<MagnifyingGlassIcon className="size-4" />
<span className="sr-only">Search</span>
</Button>
);
}
+130 -46
View File
@@ -1,41 +1,63 @@
import ServerFlag from "@/components/ServerFlag" import { useTranslation } from "react-i18next";
import ServerUsageBar from "@/components/ServerUsageBar" import { useNavigate } from "react-router-dom";
import { formatBytes } from "@/lib/format" import ServerFlag from "@/components/ServerFlag";
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class" import ServerUsageBar from "@/components/ServerUsageBar";
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils" import { formatBytes } from "@/lib/format";
import { NezhaServer } from "@/types/nezha-api" import {
import { useTranslation } from "react-i18next" GetFontLogoClass,
import { useNavigate } from "react-router-dom" GetOsName,
MageMicrosoftWindows,
} from "@/lib/logo-class";
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
import type { NezhaServer } from "@/types/nezha-api";
import BillingInfo from "./billingInfo";
import PlanInfo from "./PlanInfo";
import { Badge } from "./ui/badge";
import { Card } from "./ui/card";
import PlanInfo from "./PlanInfo" export default function ServerCard({
import BillingInfo from "./billingInfo"
import { Badge } from "./ui/badge"
import { Card } from "./ui/card"
export default function ServerCard({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) {
const { t } = useTranslation()
const navigate = useNavigate()
const { name, country_code, online, cpu, up, down, mem, stg, net_in_transfer, net_out_transfer, public_note, platform } = formatNezhaInfo(
now, now,
serverInfo, serverInfo,
) }: {
now: number;
serverInfo: NezhaServer;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const {
name,
country_code,
online,
cpu,
up,
down,
mem,
stg,
net_in_transfer,
net_out_transfer,
public_note,
platform,
} = formatNezhaInfo(now, serverInfo);
const cardClick = () => { const cardClick = () => {
sessionStorage.setItem("fromMainPage", "true") sessionStorage.setItem("fromMainPage", "true");
navigate(`/server/${serverInfo.id}`) navigate(`/server/${serverInfo.id}`);
} };
const showFlag = true const showFlag = true;
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
// @ts-expect-error ShowNetTransfer is a global variable // @ts-expect-error ShowNetTransfer is a global variable
const showNetTransfer = window.ShowNetTransfer as boolean const showNetTransfer = window.ShowNetTransfer as boolean;
// @ts-expect-error FixedTopServerName is a global variable // @ts-expect-error FixedTopServerName is a global variable
const fixedTopServerName = window.FixedTopServerName as boolean const fixedTopServerName = window.FixedTopServerName as boolean;
const parsedData = parsePublicNote(public_note) const parsedData = parsePublicNote(public_note);
return online ? ( return online ? (
<Card <Card
@@ -58,17 +80,31 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
style={{ gridTemplateColumns: "auto auto 1fr" }} style={{ gridTemplateColumns: "auto auto 1fr" }}
> >
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span> <span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}> <div
className={cn(
"flex items-center justify-center",
showFlag ? "min-w-[17px]" : "min-w-0",
)}
>
{showFlag ? <ServerFlag country_code={country_code} /> : null} {showFlag ? <ServerFlag country_code={country_code} /> : null}
</div> </div>
<div className="relative flex flex-col"> <div className="relative flex flex-col">
<p className={cn("break-normal font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}>{name}</p> <p
className={cn(
"break-normal font-bold tracking-tight",
showFlag ? "text-xs " : "text-sm",
)}
>
{name}
</p>
<div <div
className={cn("hidden lg:block", { className={cn("hidden lg:block", {
"lg:hidden": fixedTopServerName, "lg:hidden": fixedTopServerName,
})} })}
> >
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />} {parsedData?.billingDataMod && (
<BillingInfo parsedData={parsedData} />
)}
</div> </div>
</div> </div>
</section> </section>
@@ -86,7 +122,11 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
})} })}
> >
{fixedTopServerName && ( {fixedTopServerName && (
<div className={"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"}> <div
className={
"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"
}
>
<div className="text-xs font-semibold"> <div className="text-xs font-semibold">
{platform.includes("Windows") ? ( {platform.includes("Windows") ? (
<MageMicrosoftWindows className="size-[10px]" /> <MageMicrosoftWindows className="size-[10px]" />
@@ -95,36 +135,64 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
)} )}
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.system")}</p> <p className="text-xs text-muted-foreground">
<div className="flex items-center text-[10.5px] font-semibold">{platform.includes("Windows") ? "Windows" : GetOsName(platform)}</div> {t("serverCard.system")}
</p>
<div className="flex items-center text-[10.5px] font-semibold">
{platform.includes("Windows")
? "Windows"
: GetOsName(platform)}
</div>
</div> </div>
</div> </div>
)} )}
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"CPU"}</p> <p className="text-xs text-muted-foreground">{"CPU"}</p>
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div> <div className="flex items-center text-xs font-semibold">
{cpu.toFixed(2)}%
</div>
<ServerUsageBar value={cpu} /> <ServerUsageBar value={cpu} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p> <p className="text-xs text-muted-foreground">
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div> {t("serverCard.mem")}
</p>
<div className="flex items-center text-xs font-semibold">
{mem.toFixed(2)}%
</div>
<ServerUsageBar value={mem} /> <ServerUsageBar value={mem} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p> <p className="text-xs text-muted-foreground">
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div> {t("serverCard.stg")}
</p>
<div className="flex items-center text-xs font-semibold">
{stg.toFixed(2)}%
</div>
<ServerUsageBar value={stg} /> <ServerUsageBar value={stg} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p> <p className="text-xs text-muted-foreground">
{t("serverCard.upload")}
</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : up >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`} {up >= 1024
? `${(up / 1024).toFixed(2)}G/s`
: up >= 1
? `${up.toFixed(2)}M/s`
: `${(up * 1024).toFixed(2)}K/s`}
</div> </div>
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p> <p className="text-xs text-muted-foreground">
{t("serverCard.download")}
</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : down >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`} {down >= 1024
? `${(down / 1024).toFixed(2)}G/s`
: down >= 1
? `${down.toFixed(2)}M/s`
: `${(down * 1024).toFixed(2)}K/s`}
</div> </div>
</div> </div>
</section> </section>
@@ -151,7 +219,9 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
<Card <Card
className={cn( className={cn(
"flex flex-col items-center justify-start gap-3 sm:gap-0 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors", "flex flex-col items-center justify-start gap-3 sm:gap-0 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
showNetTransfer ? "lg:min-h-[91px] min-h-[123px]" : "lg:min-h-[61px] min-h-[93px]", showNetTransfer
? "lg:min-h-[91px] min-h-[123px]"
: "lg:min-h-[61px] min-h-[93px]",
{ {
"flex-col": fixedTopServerName, "flex-col": fixedTopServerName,
"lg:flex-row": !fixedTopServerName, "lg:flex-row": !fixedTopServerName,
@@ -169,17 +239,31 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
style={{ gridTemplateColumns: "auto auto 1fr" }} style={{ gridTemplateColumns: "auto auto 1fr" }}
> >
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span> <span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}> <div
className={cn(
"flex items-center justify-center",
showFlag ? "min-w-[17px]" : "min-w-0",
)}
>
{showFlag ? <ServerFlag country_code={country_code} /> : null} {showFlag ? <ServerFlag country_code={country_code} /> : null}
</div> </div>
<div className="relative flex flex-col"> <div className="relative flex flex-col">
<p className={cn("break-normal font-bold tracking-tight max-w-[108px]", showFlag ? "text-xs" : "text-sm")}>{name}</p> <p
className={cn(
"break-normal font-bold tracking-tight max-w-[108px]",
showFlag ? "text-xs" : "text-sm",
)}
>
{name}
</p>
<div <div
className={cn("hidden lg:block", { className={cn("hidden lg:block", {
"lg:hidden": fixedTopServerName, "lg:hidden": fixedTopServerName,
})} })}
> >
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />} {parsedData?.billingDataMod && (
<BillingInfo parsedData={parsedData} />
)}
</div> </div>
</div> </div>
</section> </section>
@@ -192,5 +276,5 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
</div> </div>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />} {parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</Card> </Card>
) );
} }
+147 -50
View File
@@ -1,35 +1,58 @@
import ServerFlag from "@/components/ServerFlag" import { useTranslation } from "react-i18next";
import ServerUsageBar from "@/components/ServerUsageBar" import { useNavigate } from "react-router-dom";
import { formatBytes } from "@/lib/format" import ServerFlag from "@/components/ServerFlag";
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class" import ServerUsageBar from "@/components/ServerUsageBar";
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils" import { formatBytes } from "@/lib/format";
import { NezhaServer } from "@/types/nezha-api" import {
import { useTranslation } from "react-i18next" GetFontLogoClass,
import { useNavigate } from "react-router-dom" GetOsName,
MageMicrosoftWindows,
} from "@/lib/logo-class";
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
import type { NezhaServer } from "@/types/nezha-api";
import BillingInfo from "./billingInfo";
import PlanInfo from "./PlanInfo";
import { Card } from "./ui/card";
import { Separator } from "./ui/separator";
import PlanInfo from "./PlanInfo" export default function ServerCardInline({
import BillingInfo from "./billingInfo"
import { Card } from "./ui/card"
import { Separator } from "./ui/separator"
export default function ServerCardInline({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) {
const { t } = useTranslation()
const navigate = useNavigate()
const { name, country_code, online, cpu, up, down, mem, stg, platform, uptime, net_in_transfer, net_out_transfer, public_note } = formatNezhaInfo(
now, now,
serverInfo, serverInfo,
) }: {
now: number;
serverInfo: NezhaServer;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const {
name,
country_code,
online,
cpu,
up,
down,
mem,
stg,
platform,
uptime,
net_in_transfer,
net_out_transfer,
public_note,
} = formatNezhaInfo(now, serverInfo);
const cardClick = () => { const cardClick = () => {
sessionStorage.setItem("fromMainPage", "true") sessionStorage.setItem("fromMainPage", "true");
navigate(`/server/${serverInfo.id}`) navigate(`/server/${serverInfo.id}`);
} };
const showFlag = true const showFlag = true;
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const parsedData = parsePublicNote(public_note) const parsedData = parsePublicNote(public_note);
return online ? ( return online ? (
<section> <section>
@@ -42,20 +65,39 @@ export default function ServerCardInline({ now, serverInfo }: { now: number; ser
)} )}
onClick={cardClick} onClick={cardClick}
> >
<section className={cn("grid items-center gap-2 lg:w-36")} style={{ gridTemplateColumns: "auto auto 1fr" }}> <section
className={cn("grid items-center gap-2 lg:w-36")}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span> <span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}> <div
className={cn(
"flex items-center justify-center",
showFlag ? "min-w-[17px]" : "min-w-0",
)}
>
{showFlag ? <ServerFlag country_code={country_code} /> : null} {showFlag ? <ServerFlag country_code={country_code} /> : null}
</div> </div>
<div className="relative w-28 flex flex-col"> <div className="relative w-28 flex flex-col">
<p className={cn("break-normal font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}>{name}</p> <p
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />} className={cn(
"break-normal font-bold tracking-tight",
showFlag ? "text-xs " : "text-sm",
)}
>
{name}
</p>
{parsedData?.billingDataMod && (
<BillingInfo parsedData={parsedData} />
)}
</div> </div>
</section> </section>
<Separator orientation="vertical" className="h-8 mx-0 ml-2" /> <Separator orientation="vertical" className="h-8 mx-0 ml-2" />
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}> <section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
<div className={"items-center flex flex-row gap-2 whitespace-nowrap"}> <div
className={"items-center flex flex-row gap-2 whitespace-nowrap"}
>
<div className="text-xs font-semibold"> <div className="text-xs font-semibold">
{platform.includes("Windows") ? ( {platform.includes("Windows") ? (
<MageMicrosoftWindows className="size-[10px]" /> <MageMicrosoftWindows className="size-[10px]" />
@@ -64,12 +106,20 @@ export default function ServerCardInline({ now, serverInfo }: { now: number; ser
)} )}
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.system")}</p> <p className="text-xs text-muted-foreground">
<div className="flex items-center text-[10.5px] font-semibold">{platform.includes("Windows") ? "Windows" : GetOsName(platform)}</div> {t("serverCard.system")}
</p>
<div className="flex items-center text-[10.5px] font-semibold">
{platform.includes("Windows")
? "Windows"
: GetOsName(platform)}
</div>
</div> </div>
</div> </div>
<div className={"flex w-20 flex-col"}> <div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.uptime")}</p> <p className="text-xs text-muted-foreground">
{t("serverCard.uptime")}
</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{uptime / 86400 >= 1 {uptime / 86400 >= 1
? `${(uptime / 86400).toFixed(0)} ${t("serverCard.days")}` ? `${(uptime / 86400).toFixed(0)} ${t("serverCard.days")}`
@@ -78,38 +128,68 @@ export default function ServerCardInline({ now, serverInfo }: { now: number; ser
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"CPU"}</p> <p className="text-xs text-muted-foreground">{"CPU"}</p>
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div> <div className="flex items-center text-xs font-semibold">
{cpu.toFixed(2)}%
</div>
<ServerUsageBar value={cpu} /> <ServerUsageBar value={cpu} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p> <p className="text-xs text-muted-foreground">
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div> {t("serverCard.mem")}
</p>
<div className="flex items-center text-xs font-semibold">
{mem.toFixed(2)}%
</div>
<ServerUsageBar value={mem} /> <ServerUsageBar value={mem} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p> <p className="text-xs text-muted-foreground">
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div> {t("serverCard.stg")}
</p>
<div className="flex items-center text-xs font-semibold">
{stg.toFixed(2)}%
</div>
<ServerUsageBar value={stg} /> <ServerUsageBar value={stg} />
</div> </div>
<div className={"flex w-16 flex-col"}> <div className={"flex w-16 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p> <p className="text-xs text-muted-foreground">
{t("serverCard.upload")}
</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : up >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`} {up >= 1024
? `${(up / 1024).toFixed(2)}G/s`
: up >= 1
? `${up.toFixed(2)}M/s`
: `${(up * 1024).toFixed(2)}K/s`}
</div> </div>
</div> </div>
<div className={"flex w-16 flex-col"}> <div className={"flex w-16 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p> <p className="text-xs text-muted-foreground">
{t("serverCard.download")}
</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : down >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`} {down >= 1024
? `${(down / 1024).toFixed(2)}G/s`
: down >= 1
? `${down.toFixed(2)}M/s`
: `${(down * 1024).toFixed(2)}K/s`}
</div> </div>
</div> </div>
<div className={"flex w-20 flex-col"}> <div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.totalUpload")}</p> <p className="text-xs text-muted-foreground">
<div className="flex items-center text-xs font-semibold">{formatBytes(net_out_transfer)}</div> {t("serverCard.totalUpload")}
</p>
<div className="flex items-center text-xs font-semibold">
{formatBytes(net_out_transfer)}
</div>
</div> </div>
<div className={"flex w-20 flex-col"}> <div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.totalDownload")}</p> <p className="text-xs text-muted-foreground">
<div className="flex items-center text-xs font-semibold">{formatBytes(net_in_transfer)}</div> {t("serverCard.totalDownload")}
</p>
<div className="flex items-center text-xs font-semibold">
{formatBytes(net_in_transfer)}
</div>
</div> </div>
</section> </section>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />} {parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
@@ -126,18 +206,35 @@ export default function ServerCardInline({ now, serverInfo }: { now: number; ser
)} )}
onClick={cardClick} onClick={cardClick}
> >
<section className={cn("grid items-center gap-2 w-40")} style={{ gridTemplateColumns: "auto auto 1fr" }}> <section
className={cn("grid items-center gap-2 w-40")}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span> <span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}> <div
className={cn(
"flex items-center justify-center",
showFlag ? "min-w-[17px]" : "min-w-0",
)}
>
{showFlag ? <ServerFlag country_code={country_code} /> : null} {showFlag ? <ServerFlag country_code={country_code} /> : null}
</div> </div>
<div className="relative flex flex-col"> <div className="relative flex flex-col">
<p className={cn("break-normal font-bold w-28 tracking-tight", showFlag ? "text-xs" : "text-sm")}>{name}</p> <p
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />} className={cn(
"break-normal font-bold w-28 tracking-tight",
showFlag ? "text-xs" : "text-sm",
)}
>
{name}
</p>
{parsedData?.billingDataMod && (
<BillingInfo parsedData={parsedData} />
)}
</div> </div>
</section> </section>
<Separator orientation="vertical" className="h-8 ml-3 lg:ml-1 mr-3" /> <Separator orientation="vertical" className="h-8 ml-3 lg:ml-1 mr-3" />
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />} {parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</Card> </Card>
) );
} }
File diff suppressed because it is too large Load Diff
+127 -60
View File
@@ -1,58 +1,74 @@
import { BackIcon } from "@/components/Icon" import countries from "i18n-iso-countries";
import ServerFlag from "@/components/ServerFlag" import enLocale from "i18n-iso-countries/langs/en.json";
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading" import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge" import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card" import { useNavigate } from "react-router-dom";
import { useWebSocketContext } from "@/hooks/use-websocket-context" import { BackIcon } from "@/components/Icon";
import { formatBytes } from "@/lib/format" import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading";
import { cn, formatNezhaInfo } from "@/lib/utils" import ServerFlag from "@/components/ServerFlag";
import { NezhaWebsocketResponse } from "@/types/nezha-api" import { Badge } from "@/components/ui/badge";
import countries from "i18n-iso-countries" import { Card, CardContent } from "@/components/ui/card";
import enLocale from "i18n-iso-countries/langs/en.json" import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { useEffect, useState } from "react" import { formatBytes } from "@/lib/format";
import { useTranslation } from "react-i18next" import { cn, formatNezhaInfo } from "@/lib/utils";
import { useNavigate } from "react-router-dom" import type { NezhaWebsocketResponse } from "@/types/nezha-api";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion" import {
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip" Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "./ui/accordion";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./ui/tooltip";
export default function ServerDetailOverview({ server_id }: { server_id: string }) { export default function ServerDetailOverview({
const { t } = useTranslation() server_id,
const navigate = useNavigate() }: {
server_id: string;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const [hasHistory, setHasHistory] = useState(false) const [hasHistory, setHasHistory] = useState(false);
useEffect(() => { useEffect(() => {
const previousPath = sessionStorage.getItem("fromMainPage") const previousPath = sessionStorage.getItem("fromMainPage");
if (previousPath) { if (previousPath) {
setHasHistory(true) setHasHistory(true);
} }
}, []) }, []);
const { lastMessage, connected } = useWebSocketContext() const { lastMessage, connected } = useWebSocketContext();
if (!connected && !lastMessage) { if (!connected && !lastMessage) {
return <ServerDetailLoading /> return <ServerDetailLoading />;
} }
const linkClick = () => { const linkClick = () => {
if (hasHistory) { if (hasHistory) {
navigate(-1) navigate(-1);
} else { } else {
navigate("/") navigate("/");
}
} }
};
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null const nezhaWsData = lastMessage
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
: null;
if (!nezhaWsData) { if (!nezhaWsData) {
return <ServerDetailLoading /> return <ServerDetailLoading />;
} }
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id)) const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
if (!server) { if (!server) {
return <ServerDetailLoading /> return <ServerDetailLoading />;
} }
const { const {
@@ -75,11 +91,14 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
net_in_transfer, net_in_transfer,
last_active_time_string, last_active_time_string,
boot_time_string, boot_time_string,
} = formatNezhaInfo(nezhaWsData.now, server) } = formatNezhaInfo(nezhaWsData.now, server);
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
countries.registerLocale(enLocale) countries.registerLocale(enLocale);
return ( return (
<div <div
@@ -98,12 +117,17 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.status")}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.status")}
</p>
<Badge <Badge
className={cn("text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white", { className={cn(
"text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white",
{
" bg-green-800": online, " bg-green-800": online,
" bg-red-600": !online, " bg-red-600": !online,
})} },
)}
> >
{online ? t("serverDetail.online") : t("serverDetail.offline")} {online ? t("serverDetail.online") : t("serverDetail.offline")}
</Badge> </Badge>
@@ -114,7 +138,9 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.uptime")}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.uptime")}
</p>
<div className="text-xs"> <div className="text-xs">
{" "} {" "}
{uptime / 86400 >= 1 {uptime / 86400 >= 1
@@ -129,7 +155,9 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.version")}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.version")}
</p>
<div className="text-xs">{version} </div> <div className="text-xs">{version} </div>
</section> </section>
</CardContent> </CardContent>
@@ -139,7 +167,9 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.arch")}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.arch")}
</p>
<div className="text-xs">{arch} </div> <div className="text-xs">{arch} </div>
</section> </section>
</CardContent> </CardContent>
@@ -150,7 +180,9 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.mem")}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.mem")}
</p>
<div className="text-xs">{formatBytes(mem_total)}</div> <div className="text-xs">{formatBytes(mem_total)}</div>
</section> </section>
</CardContent> </CardContent>
@@ -161,7 +193,9 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.disk")}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.disk")}
</p>
<div className="text-xs">{formatBytes(disk_total)}</div> <div className="text-xs">{formatBytes(disk_total)}</div>
</section> </section>
</CardContent> </CardContent>
@@ -175,10 +209,19 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.region")}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.region")}
</p>
<section className="flex items-start gap-1"> <section className="flex items-start gap-1">
<div className="text-xs text-start">{country_code?.toUpperCase()}</div> <div className="text-xs text-start">
{country_code && <ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />} {country_code?.toUpperCase()}
</div>
{country_code && (
<ServerFlag
className="text-[11px] -mt-px"
country_code={country_code}
/>
)}
</section> </section>
</section> </section>
</CardContent> </CardContent>
@@ -196,10 +239,12 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.system")}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.system")}
</p>
<div className="text-xs"> <div className="text-xs">
{" "} {" "}
{platform} {platform_version ? " - " + platform_version : ""} {platform} {platform_version ? ` - ${platform_version}` : ""}
</div> </div>
</section> </section>
</CardContent> </CardContent>
@@ -241,9 +286,14 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.upload")}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.upload")}
</p>
{net_out_transfer ? ( {net_out_transfer ? (
<div className="text-xs"> {formatBytes(net_out_transfer)} </div> <div className="text-xs">
{" "}
{formatBytes(net_out_transfer)}{" "}
</div>
) : ( ) : (
<div className="text-xs"> {t("serverDetail.unknown")}</div> <div className="text-xs"> {t("serverDetail.unknown")}</div>
)} )}
@@ -255,9 +305,14 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.download")}</p> <p className="text-xs text-muted-foreground">
{t("serverDetail.download")}
</p>
{net_in_transfer ? ( {net_in_transfer ? (
<div className="text-xs"> {formatBytes(net_in_transfer)} </div> <div className="text-xs">
{" "}
{formatBytes(net_in_transfer)}{" "}
</div>
) : ( ) : (
<div className="text-xs"> {t("serverDetail.unknown")}</div> <div className="text-xs"> {t("serverDetail.unknown")}</div>
)} )}
@@ -267,16 +322,20 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
) : null} ) : null}
</section> </section>
<section className="flex flex-wrap gap-2 mt-1"> <section className="flex flex-wrap gap-2 mt-1">
{server?.state.temperatures && server?.state.temperatures.length > 0 && ( {server?.state.temperatures &&
server?.state.temperatures.length > 0 && (
<section className="flex flex-wrap gap-2 ml-1.5"> <section className="flex flex-wrap gap-2 ml-1.5">
<Accordion type="single" collapsible className="w-fit"> <Accordion type="single" collapsible className="w-fit">
<AccordionItem value="item-1" className="border-none"> <AccordionItem value="item-1" className="border-none">
<AccordionTrigger className="text-xs py-0 text-muted-foreground font-normal">{t("serverDetail.temperature")}</AccordionTrigger> <AccordionTrigger className="text-xs py-0 text-muted-foreground font-normal">
{t("serverDetail.temperature")}
</AccordionTrigger>
<AccordionContent className="pb-0"> <AccordionContent className="pb-0">
<section className="flex items-start flex-wrap gap-2"> <section className="flex items-start flex-wrap gap-2">
{server?.state.temperatures.map((item, index) => ( {server?.state.temperatures.map((item, index) => (
<div className="text-xs flex items-center" key={index}> <div className="text-xs flex items-center" key={index}>
<p className="font-semibold">{item.Name}</p>: {item.Temperature.toFixed(2)} °C <p className="font-semibold">{item.Name}</p>:{" "}
{item.Temperature.toFixed(2)} °C
</div> </div>
))} ))}
</section> </section>
@@ -291,20 +350,28 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.bootTime")}</p> <p className="text-xs text-muted-foreground">
<div className="text-xs">{boot_time_string ? boot_time_string : "N/A"}</div> {t("serverDetail.bootTime")}
</p>
<div className="text-xs">
{boot_time_string ? boot_time_string : "N/A"}
</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.lastActive")}</p> <p className="text-xs text-muted-foreground">
<div className="text-xs">{last_active_time_string ? last_active_time_string : "N/A"}</div> {t("serverDetail.lastActive")}
</p>
<div className="text-xs">
{last_active_time_string ? last_active_time_string : "N/A"}
</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
</section> </section>
</div> </div>
) );
} }
+111
View File
@@ -0,0 +1,111 @@
"use client";
import { Progress } from "@/components/ui/progress";
import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { formatNezhaInfo } from "@/lib/utils";
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
export default function ServerDetailSummary({
server_id,
}: {
server_id: number;
}) {
const { lastMessage, connected } = useWebSocketContext();
if (!connected && !lastMessage) {
return null;
}
const nezhaWsData = lastMessage
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
: null;
if (!nezhaWsData) {
return null;
}
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
if (!server) {
return null;
}
const { cpu, mem, disk, up, down, tcp, udp, process } = formatNezhaInfo(
nezhaWsData.now,
server,
);
return (
<div className="mb-2 flex flex-wrap items-center gap-4 server-detail-summary">
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
<section className="flex items-center justify-between">
<span className="text-[10px] text-muted-foreground">CPU</span>
<span className="font-medium text-[10px]">{cpu.toFixed(2)}%</span>
</section>
<UsageBar value={cpu} />
</section>
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
<section className="flex items-center justify-between">
<span className="text-[10px] text-muted-foreground">Mem</span>
<span className="font-medium text-[10px]">{mem.toFixed(2)}%</span>
</section>
<UsageBar value={mem} />
</section>
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
<section className="flex items-center justify-between">
<span className="text-[10px] text-muted-foreground">Disk</span>
<span className="font-medium text-[10px]">{disk.toFixed(2)}%</span>
</section>
<UsageBar value={disk} />
</section>
<section className="flex min-w-[85px] flex-col justify-center px-1.5 py-1">
<section className="flex items-center justify-between gap-4">
<span className="text-[10px] text-muted-foreground">Process</span>
<span className="font-medium text-[10px]">{process}</span>
</section>
</section>
<section className="flex min-w-[70px] flex-col justify-center gap-0.5 px-1.5 py-1">
<section className="flex items-center justify-between gap-4">
<span className="text-[10px] text-muted-foreground">TCP</span>
<span className="font-medium text-[10px]">{tcp}</span>
</section>
<section className="flex items-center justify-between gap-4">
<span className="text-[10px] text-muted-foreground">UDP</span>
<span className="font-medium text-[10px]">{udp}</span>
</section>
</section>
<section className="flex min-w-[120px] flex-col justify-center gap-0.5 px-1.5 py-1">
<section className="flex items-center justify-between gap-4">
<span className="text-[10px] text-muted-foreground">Upload</span>
<span className="font-medium text-[10px]">{up.toFixed(2)}M/s</span>
</section>
<section className="flex items-center justify-between gap-4">
<span className="text-[10px] text-muted-foreground">Download</span>
<span className="font-medium text-[10px]">{down.toFixed(2)}M/s</span>
</section>
</section>
</div>
);
}
type UsageBarProps = {
value: number;
};
function UsageBar({ value }: UsageBarProps) {
return (
<Progress
aria-label={"Server Usage Bar"}
aria-labelledby={"Server Usage Bar"}
value={value}
indicatorClassName={
value > 90
? "bg-red-500"
: value > 70
? "bg-orange-400"
: "bg-green-500"
}
className={"h-[3px] rounded-sm bg-stone-200 dark:bg-stone-800"}
/>
);
}
+34 -24
View File
@@ -1,42 +1,52 @@
import { cn } from "@/lib/utils" import getUnicodeFlagIcon from "country-flag-icons/unicode";
import getUnicodeFlagIcon from "country-flag-icons/unicode" import { useEffect, useState } from "react";
import { useEffect, useState } from "react" import { cn } from "@/lib/utils";
export default function ServerFlag({ country_code, className }: { country_code: string; className?: string }) { export default function ServerFlag({
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false) country_code,
className,
}: {
country_code: string;
className?: string;
}) {
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false);
// @ts-expect-error ForceUseSvgFlag is a global variable // @ts-expect-error ForceUseSvgFlag is a global variable
const forceUseSvgFlag = window.ForceUseSvgFlag as boolean const forceUseSvgFlag = window.ForceUseSvgFlag as boolean;
useEffect(() => { useEffect(() => {
if (forceUseSvgFlag) { if (forceUseSvgFlag) {
// 如果环境变量要求直接使用 SVG,则无需检查 Emoji 支持 // 如果环境变量要求直接使用 SVG,则无需检查 Emoji 支持
setSupportsEmojiFlags(false) setSupportsEmojiFlags(false);
return return;
} }
const checkEmojiSupport = () => { const checkEmojiSupport = () => {
const canvas = document.createElement("canvas") const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d") const ctx = canvas.getContext("2d");
const emojiFlag = "🇺🇸" // 使用美国国旗作为测试 const emojiFlag = "🇺🇸"; // 使用美国国旗作为测试
if (!ctx) return if (!ctx) return;
ctx.fillStyle = "#000" ctx.fillStyle = "#000";
ctx.textBaseline = "top" ctx.textBaseline = "top";
ctx.font = "32px Arial" ctx.font = "32px Arial";
ctx.fillText(emojiFlag, 0, 0) ctx.fillText(emojiFlag, 0, 0);
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0 const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0;
setSupportsEmojiFlags(support) setSupportsEmojiFlags(support);
} };
checkEmojiSupport() checkEmojiSupport();
}, []) }, [forceUseSvgFlag]);
if (!country_code) return null if (!country_code) return null;
return ( return (
<span className={cn("text-[12px] text-muted-foreground", className)}> <span className={cn("text-[12px] text-muted-foreground", className)}>
{forceUseSvgFlag || !supportsEmojiFlags ? <span className={`fi fi-${country_code}`} /> : getUnicodeFlagIcon(country_code)} {forceUseSvgFlag || !supportsEmojiFlags ? (
<span className={`fi fi-${country_code}`} />
) : (
getUnicodeFlagIcon(country_code)
)}
</span> </span>
) );
} }
+108 -57
View File
@@ -1,26 +1,27 @@
// src/components/ServerOverview.tsx (最终完整版) import {
ArrowDownCircleIcon,
ArrowUpCircleIcon,
} from "@heroicons/react/20/solid";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { useStatus } from "@/hooks/use-status";
import { formatBytes } from "@/lib/format";
import { cn } from "@/lib/utils";
import { Globe } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card"
import { useStatus } from "@/hooks/use-status"
import { formatBytes} from "@/lib/format"
import { cn } from "@/lib/utils"
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
import { useTranslation } from "react-i18next"
import { Globe } from "lucide-react"
// 扩展 props 类型,以接收域名总数和新的交互逻辑
type ServerOverviewProps = { type ServerOverviewProps = {
online: number online: number;
offline: number offline: number;
total: number total: number;
up: number up: number;
down: number down: number;
upSpeed: number upSpeed: number;
downSpeed: number upSpeed: number;
totalDomains: number // 新增:接收域名总数 downSpeed: number;
onViewChange: (view: 'servers' | 'domains') => void // 新增:点击事件回调 totalDomains: number; // 新增:接收域名总数
activeView: 'servers' | 'domains' // 新增:当前激活的视图 onViewChange: (view: 'servers' | 'domains') => void; // 新增:点击事件回调
} activeView: 'servers' | 'domains'; // 新增:当前激活的视图
};
export default function ServerOverview({ export default function ServerOverview({
online, online,
@@ -34,33 +35,41 @@ export default function ServerOverview({
onViewChange, onViewChange,
activeView, activeView,
}: ServerOverviewProps) { }: ServerOverviewProps) {
const { t } = useTranslation() const { t } = useTranslation();
const { status, setStatus } = useStatus() const { status, setStatus } = useStatus();
// --- 所有原始变量和逻辑保持不变 --- // @ts-expect-error DisableAnimatedMan is a global variable
const disableAnimatedMan = (window as any).DisableAnimatedMan as boolean const disableAnimatedMan = window.DisableAnimatedMan as boolean;
const customIllustration = (window as any).CustomIllustration || "/animated-man.webp"
const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined // @ts-expect-error CustomIllustration is a global variable
const customIllustration = window.CustomIllustration || "/animated-man.webp";
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
// 新增:一个组合了两个动作的点击处理函数 // 新增:一个组合了两个动作的点击处理函数
const handleServerCardClick = (serverStatus: 'all' | 'online' | 'offline') => { const handleServerCardClick = (serverStatus: 'all' | 'online' | 'offline') => {
onViewChange('servers'); // 动作1: 确保视图切换回服务器 onViewChange('servers'); // 动作1: 确保视图切换回服务器
setStatus(serverStatus); // 动作2: 执行原有的状态筛选 setStatus(serverStatus); // 动作2: 执行原有的状态筛选
} };
return ( return (
<>
<section className="grid grid-cols-2 gap-4 lg:grid-cols-5 server-overview"> <section className="grid grid-cols-2 gap-4 lg:grid-cols-5 server-overview">
<Card <Card
onClick={() => handleServerCardClick("all")} onClick={() => {
className={cn( handleServerCardClick("all");
"hover:border-blue-500 cursor-pointer transition-all", }}
{ "bg-card/70": customBackgroundImage }, className={cn("hover:border-blue-500 cursor-pointer transition-all", {
)} "bg-card/70": customBackgroundImage,
})}
> >
<CardContent className="flex h-full items-center px-6 py-3"> <CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">{t("serverOverview.totalServers")}</p> <p className="text-sm font-medium md:text-base">
{t("serverOverview.totalServers")}
</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="relative flex h-2 w-2"> <span className="relative flex h-2 w-2">
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span> <span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
@@ -71,37 +80,54 @@ export default function ServerOverview({
</CardContent> </CardContent>
</Card> </Card>
<Card <Card
onClick={() => handleServerCardClick("online")} onClick={() => {
handleServerCardClick("online");
}}
className={cn( className={cn(
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all", "cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all",
{ "bg-card/70": customBackgroundImage }, {
{ "ring-green-500 ring-2 border-transparent": activeView === 'servers' && status === "online" } "bg-card/70": customBackgroundImage,
},
{
"ring-green-500 ring-2 border-transparent": activeView === "servers" && status === "online",
},
)} )}
> >
<CardContent className="flex h-full items-center px-6 py-3"> <CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">{t("serverOverview.onlineServers")}</p> <p className="text-sm font-medium md:text-base">
{t("serverOverview.onlineServers")}
</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="relative flex h-2 w-2"> <span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span> <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span> <span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
</span> </span>
<div className="text-lg font-semibold">{online}</div> <div className="text-lg font-semibold">{online}</div>
</div> </div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<Card <Card
onClick={() => handleServerCardClick("offline")} onClick={() => {
handleServerCardClick("offline");
}}
className={cn( className={cn(
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all", "cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all",
{ "bg-card/70": customBackgroundImage }, {
{ "ring-red-500 ring-2 border-transparent": activeView === 'servers' && status === "offline" } "bg-card/70": customBackgroundImage,
},
{
"ring-red-500 ring-2 border-transparent": activeView === "servers" && status === "offline",
},
)} )}
> >
<CardContent className="flex h-full items-center px-6 py-3"> <CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">{t("serverOverview.offlineServers")}</p> <p className="text-sm font-medium md:text-base">
{t("serverOverview.offlineServers")}
</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="relative flex h-2 w-2"> <span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span> <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span>
@@ -112,13 +138,16 @@ export default function ServerOverview({
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<Card <Card
onClick={() => onViewChange('domains')} onClick={() => onViewChange("domains")}
className={cn( className={cn(
"cursor-pointer hover:ring-indigo-500 ring-1 ring-transparent transition-all", "cursor-pointer hover:ring-indigo-500 ring-1 ring-transparent transition-all",
{ "bg-card/70": customBackgroundImage }, {
{ "ring-indigo-500 ring-2 border-transparent": activeView === 'domains' } "bg-card/70": customBackgroundImage,
},
{
"ring-indigo-500 ring-2 border-transparent": activeView === "domains",
},
)} )}
> >
<CardContent className="flex h-full items-center px-6 py-3"> <CardContent className="flex h-full items-center px-6 py-3">
@@ -131,28 +160,50 @@ export default function ServerOverview({
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<Card <Card
className={cn("hover:ring-purple-500 ring-1 ring-transparent transition-all", { "bg-card/70": customBackgroundImage })} className={cn(
"hover:ring-purple-500 ring-1 ring-transparent transition-all",
{
"bg-card/70": customBackgroundImage,
},
)}
> >
<CardContent className="flex h-full items-center relative px-6 py-3"> <CardContent className="flex h-full items-center relative px-6 py-3">
<section className="flex flex-col gap-1 w-full"> <section className="flex flex-col gap-1 w-full">
<div className="flex items-center w-full justify-between"><p className="text-sm font-medium md:text-base">{t("serverOverview.network")}</p></div> <div className="flex items-center w-full justify-between">
<p className="text-sm font-medium md:text-base">
{t("serverOverview.network")}
</p>
</div>
<section className="flex items-start flex-row z-10 pr-0 gap-1"> <section className="flex items-start flex-row z-10 pr-0 gap-1">
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">{formatBytes(up)}</p> <p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">
<p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">{formatBytes(down)}</p> {formatBytes(up)}
</p>
<p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">
{formatBytes(down)}
</p>
</section> </section>
<section className="flex flex-col sm:flex-row -mr-1 sm:items-center items-start gap-1"> <section className="flex flex-col sm:flex-row -mr-1 sm:items-center items-start gap-1">
<p className="text-[11px] flex items-center text-nowrap font-semibold"><ArrowUpCircleIcon className="size-3 mr-0.5 sm:mb-[1px]" />{formatBytes(upSpeed)}/s</p> <p className="text-[11px] flex items-center text-nowrap font-semibold">
<p className="text-[11px] flex items-center text-nowrap font-semibold"><ArrowDownCircleIcon className="size-3 mr-0.5" />{formatBytes(downSpeed)}/s</p> <ArrowUpCircleIcon className="size-3 mr-0.5 sm:mb-px" />
{formatBytes(upSpeed)}/s
</p>
<p className="text-[11px] flex items-center text-nowrap font-semibold">
<ArrowDownCircleIcon className="size-3 mr-0.5" />
{formatBytes(downSpeed)}/s
</p>
</section> </section>
</section> </section>
{!disableAnimatedMan && ( {!disableAnimatedMan && (
<img className="absolute right-[-30px] top-[-120px] z-10 w-40 scale-100 group-hover:opacity-50 md:scale-100 transition-all" alt={"animated-man"} src={customIllustration} loading="eager" /> <img
className="absolute right-3 top-[-85px] z-50 w-20 scale-90 group-hover:opacity-50 md:scale-100 transition-all"
alt={"animated-man"}
src={customIllustration}
loading="eager"
/>
)} )}
</CardContent> </CardContent>
</Card> </Card>
</section> </section>
</> );
)
} }
+11 -5
View File
@@ -1,8 +1,8 @@
import { Progress } from "@/components/ui/progress" import { Progress } from "@/components/ui/progress";
type ServerUsageBarProps = { type ServerUsageBarProps = {
value: number value: number;
} };
export default function ServerUsageBar({ value }: ServerUsageBarProps) { export default function ServerUsageBar({ value }: ServerUsageBarProps) {
return ( return (
@@ -10,8 +10,14 @@ export default function ServerUsageBar({ value }: ServerUsageBarProps) {
aria-label={"Server Usage Bar"} aria-label={"Server Usage Bar"}
aria-labelledby={"Server Usage Bar"} aria-labelledby={"Server Usage Bar"}
value={value} value={value}
indicatorClassName={value > 90 ? "bg-red-500" : value > 70 ? "bg-orange-400" : "bg-green-500"} indicatorClassName={
value > 90
? "bg-red-500"
: value > 70
? "bg-orange-400"
: "bg-green-500"
}
className={"h-[3px] rounded-sm"} className={"h-[3px] rounded-sm"}
/> />
) );
} }
+50 -29
View File
@@ -1,43 +1,49 @@
import { fetchService } from "@/lib/nezha-api" import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
import { NezhaServer, ServiceData } from "@/types/nezha-api" import { useQuery } from "@tanstack/react-query";
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid" import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query" import { fetchService } from "@/lib/nezha-api";
import { useTranslation } from "react-i18next" import type { NezhaServer, ServiceData } from "@/types/nezha-api";
import { CycleTransferStatsCard } from "./CycleTransferStats" import { CycleTransferStatsCard } from "./CycleTransferStats";
import ServiceTrackerClient from "./ServiceTrackerClient" import { Loader } from "./loading/Loader";
import { Loader } from "./loading/Loader" import ServiceTrackerClient from "./ServiceTrackerClient";
export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) { export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) {
const { t } = useTranslation() const { t } = useTranslation();
const { data: serviceData, isLoading } = useQuery({ const { data: serviceData, isLoading } = useQuery({
queryKey: ["service"], queryKey: ["service"],
queryFn: () => fetchService(), queryFn: () => fetchService(),
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
refetchInterval: 10000, refetchInterval: 10000,
}) });
const processServiceData = (serviceData: ServiceData) => { const processServiceData = (serviceData: ServiceData) => {
const days = serviceData.up.map((up, index) => { const days = serviceData.up.map((up, index) => {
const totalChecks = up + serviceData.down[index] const totalChecks = up + serviceData.down[index];
const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0 const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0;
return { return {
completed: up > serviceData.down[index], completed: up > serviceData.down[index],
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000), date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
uptime: dailyUptime, uptime: dailyUptime,
delay: serviceData.delay[index] || 0, delay: serviceData.delay[index] || 0,
} };
}) });
const totalUp = serviceData.up.reduce((a, b) => a + b, 0) const totalUp = serviceData.up.reduce((a, b) => a + b, 0);
const totalChecks = serviceData.up.reduce((a, b) => a + b, 0) + serviceData.down.reduce((a, b) => a + b, 0) const totalChecks =
const uptime = (totalUp / totalChecks) * 100 serviceData.up.reduce((a, b) => a + b, 0) +
serviceData.down.reduce((a, b) => a + b, 0);
const uptime = (totalUp / totalChecks) * 100;
const avgDelay = serviceData.delay.length > 0 ? serviceData.delay.reduce((a, b) => a + b, 0) / serviceData.delay.length : 0 const avgDelay =
serviceData.delay.length > 0
? serviceData.delay.reduce((a, b) => a + b, 0) /
serviceData.delay.length
: 0;
return { days, uptime, avgDelay } return { days, uptime, avgDelay };
} };
if (isLoading) { if (isLoading) {
return ( return (
@@ -45,35 +51,50 @@ export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) {
<Loader visible={true} /> <Loader visible={true} />
{t("serviceTracker.loading")} {t("serviceTracker.loading")}
</div> </div>
) );
} }
if (!serviceData?.data?.services && !serviceData?.data?.cycle_transfer_stats) { if (
!serviceData?.data?.services &&
!serviceData?.data?.cycle_transfer_stats
) {
return ( return (
<div className="mt-4 text-sm font-medium flex items-center gap-1"> <div className="mt-4 text-sm font-medium flex items-center gap-1">
<ExclamationTriangleIcon className="w-4 h-4" /> <ExclamationTriangleIcon className="w-4 h-4" />
{t("serviceTracker.noService")} {t("serviceTracker.noService")}
</div> </div>
) );
} }
return ( return (
<div className="mt-4 w-full mx-auto "> <div className="mt-4 w-full mx-auto ">
{serviceData.data.cycle_transfer_stats && ( {serviceData.data.cycle_transfer_stats && (
<div> <div>
<CycleTransferStatsCard serverList={serverList} cycleStats={serviceData.data.cycle_transfer_stats} /> <CycleTransferStatsCard
serverList={serverList}
cycleStats={serviceData.data.cycle_transfer_stats}
/>
</div> </div>
)} )}
{serviceData.data.services && Object.keys(serviceData.data.services).length > 0 && ( {serviceData.data.services &&
Object.keys(serviceData.data.services).length > 0 && (
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4"> <section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
{Object.entries(serviceData.data.services).map(([name, data]) => { {Object.entries(serviceData.data.services).map(([name, data]) => {
const { days, uptime, avgDelay } = processServiceData(data) const { days, uptime, avgDelay } = processServiceData(data);
return <ServiceTrackerClient key={name} days={days} title={data.service_name} uptime={uptime} avgDelay={avgDelay} /> return (
<ServiceTrackerClient
key={name}
days={days}
title={data.service_name}
uptime={uptime}
avgDelay={avgDelay}
/>
);
})} })}
</section> </section>
)} )}
</div> </div>
) );
} }
export default ServiceTracker export default ServiceTracker;
+93 -45
View File
@@ -1,44 +1,58 @@
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import type React from "react";
import { cn } from "@/lib/utils" import { useTranslation } from "react-i18next";
import React from "react" import {
import { useTranslation } from "react-i18next" Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { Separator } from "./ui/separator" import { Separator } from "./ui/separator";
interface ServiceTrackerProps { interface ServiceTrackerProps {
days: Array<{ days: Array<{
completed: boolean completed: boolean;
date?: Date date?: Date;
uptime: number uptime: number;
delay: number delay: number;
}> }>;
className?: string className?: string;
title?: string title?: string;
uptime?: number uptime?: number;
avgDelay?: number avgDelay?: number;
} }
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, className, title, uptime = 100, avgDelay = 0 }) => { export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
const { t } = useTranslation() days,
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined className,
title,
uptime = 100,
avgDelay = 0,
}) => {
const { t } = useTranslation();
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const getUptimeColor = (uptime: number) => { const getUptimeColor = (uptime: number) => {
if (uptime >= 99) return "text-emerald-500" if (uptime >= 99) return "text-emerald-500";
if (uptime >= 95) return "text-amber-500" if (uptime >= 95) return "text-amber-500";
return "text-rose-500" return "text-rose-500";
} };
const getDelayColor = (delay: number) => { const getDelayColor = (delay: number) => {
if (delay < 100) return "text-emerald-500" if (delay < 100) return "text-emerald-500";
if (delay < 300) return "text-amber-500" if (delay < 300) return "text-amber-500";
return "text-rose-500" return "text-rose-500";
} };
const getStatusColor = (uptime: number) => { const getStatusColor = (uptime: number) => {
if (uptime >= 99) return "bg-emerald-500" if (uptime >= 99) return "bg-emerald-500";
if (uptime >= 95) return "bg-amber-500" if (uptime >= 95) return "bg-amber-500";
return "bg-rose-500" return "bg-rose-500";
} };
return ( return (
<div <div
@@ -52,13 +66,30 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, clas
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className={cn("w-2.5 h-2.5 rounded-full transition-colors", getStatusColor(uptime))} /> <div
className={cn(
"w-2.5 h-2.5 rounded-full transition-colors",
getStatusColor(uptime),
)}
/>
<span className="font-medium text-sm">{title}</span> <span className="font-medium text-sm">{title}</span>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className={cn("font-medium text-sm transition-colors", getDelayColor(avgDelay))}>{avgDelay.toFixed(0)}ms</span> <span
className={cn(
"font-medium text-sm transition-colors",
getDelayColor(avgDelay),
)}
>
{avgDelay.toFixed(0)}ms
</span>
<Separator className="h-4" orientation="vertical" /> <Separator className="h-4" orientation="vertical" />
<span className={cn("font-medium text-sm transition-colors", getUptimeColor(uptime))}> <span
className={cn(
"font-medium text-sm transition-colors",
getUptimeColor(uptime),
)}
>
{uptime.toFixed(1)}% {t("serviceTracker.uptime")} {uptime.toFixed(1)}% {t("serviceTracker.uptime")}
</span> </span>
</div> </div>
@@ -73,27 +104,44 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, clas
className={cn( className={cn(
"relative flex-1 h-7 rounded-[4px] transition-all duration-200 cursor-help", "relative flex-1 h-7 rounded-[4px] transition-all duration-200 cursor-help",
"before:absolute before:inset-0 before:rounded-[4px] before:opacity-0 hover:before:opacity-100 before:bg-white/10 before:transition-opacity", "before:absolute before:inset-0 before:rounded-[4px] before:opacity-0 hover:before:opacity-100 before:bg-white/10 before:transition-opacity",
"after:absolute after:inset-0 after:rounded-[4px] after:shadow-[inset_0_1px_theme(colors.white/10%)]", "after:absolute after:inset-0 after:rounded-[4px] after:shadow-[inset_0_1px_--theme(--color-white/10%)]",
day.completed day.completed
? "bg-gradient-to-b from-green-500/90 to-green-600 shadow-[0_1px_2px_theme(colors.green.600/30%)]" ? "bg-linear-to-b from-green-500/90 to-green-600 shadow-[0_1px_2px_--theme(--color-green-600/30%)]"
: "bg-gradient-to-b from-red-500/80 to-red-600/90 shadow-[0_1px_2px_theme(colors.red.600/30%)]", : "bg-linear-to-b from-red-500/80 to-red-600/90 shadow-[0_1px_2px_--theme(--color-red-600/30%)]",
)} )}
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="p-0 overflow-hidden"> <TooltipContent className="p-0 overflow-hidden">
<div className="px-3 py-2 bg-popover"> <div className="px-3 py-2 bg-popover">
<p className="font-medium text-sm mb-2">{day.date?.toLocaleDateString()}</p> <p className="font-medium text-sm mb-2">
{day.date?.toLocaleDateString()}
</p>
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">{t("serviceTracker.uptime")}:</span> <span className="text-xs text-muted-foreground">
<span className={cn("text-xs font-medium", day.uptime > 95 ? "text-green-500" : "text-red-500")}>{day.uptime.toFixed(1)}%</span> {t("serviceTracker.uptime")}:
</div> </span>
<div className="flex items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">{t("serviceTracker.delay")}:</span>
<span <span
className={cn( className={cn(
"text-xs font-medium", "text-xs font-medium",
day.delay < 100 ? "text-green-500" : day.delay < 300 ? "text-yellow-500" : "text-red-500", day.uptime > 95 ? "text-green-500" : "text-red-500",
)}
>
{day.uptime.toFixed(1)}%
</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">
{t("serviceTracker.delay")}:
</span>
<span
className={cn(
"text-xs font-medium",
day.delay < 100
? "text-green-500"
: day.delay < 300
? "text-yellow-500"
: "text-red-500",
)} )}
> >
{day.delay.toFixed(0)}ms {day.delay.toFixed(0)}ms
@@ -112,7 +160,7 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, clas
<span>{t("serviceTracker.today")}</span> <span>{t("serviceTracker.today")}</span>
</div> </div>
</div> </div>
) );
} };
export default ServiceTrackerClient export default ServiceTrackerClient;
+28 -12
View File
@@ -1,24 +1,40 @@
import { cn } from "@/lib/utils" import { m } from "framer-motion";
import { m } from "framer-motion" import { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils";
export default function TabSwitch({ tabs, currentTab, setCurrentTab }: { tabs: string[]; currentTab: string; setCurrentTab: (tab: string) => void }) { export default function TabSwitch({
const { t } = useTranslation() tabs,
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined currentTab,
setCurrentTab,
}: {
tabs: string[];
currentTab: string;
setCurrentTab: (tab: string) => void;
}) {
const { t } = useTranslation();
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
return ( return (
<div className="z-50 flex flex-col items-start rounded-[50px] server-info-tab"> <div className="z-50 flex flex-col items-start rounded-[50px] server-info-tab">
<div <div
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", { className={cn(
"flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800",
{
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage, "bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
})} },
)}
> >
{tabs.map((tab: string) => ( {tabs.map((tab: string) => (
<div <div
key={tab} key={tab}
onClick={() => setCurrentTab(tab)} onClick={() => setCurrentTab(tab)}
className={cn( className={cn(
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500", "relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold transition-all duration-500",
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500", currentTab === tab
? "text-black dark:text-white"
: "text-stone-400 dark:text-stone-500",
)} )}
> >
{currentTab === tab && ( {currentTab === tab && (
@@ -32,11 +48,11 @@ export default function TabSwitch({ tabs, currentTab, setCurrentTab }: { tabs: s
/> />
)} )}
<div className="relative z-20 flex items-center gap-1"> <div className="relative z-20 flex items-center gap-1">
<p className="whitespace-nowrap">{t("tabSwitch." + tab)}</p> <p className="whitespace-nowrap">{t(`tabSwitch.${tab}`)}</p>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
) );
} }
+20 -18
View File
@@ -1,39 +1,41 @@
"use client" "use client";
import { useTheme } from "@/hooks/use-theme" import { useEffect } from "react";
import { useEffect } from "react" import { useTheme } from "@/hooks/use-theme";
export function ThemeColorManager() { export function ThemeColorManager() {
const { theme } = useTheme() const { theme } = useTheme();
useEffect(() => { useEffect(() => {
const updateThemeColor = () => { const updateThemeColor = () => {
const currentTheme = theme const currentTheme = theme;
const meta = document.querySelector('meta[name="theme-color"]') const meta = document.querySelector('meta[name="theme-color"]');
if (!meta) { if (!meta) {
const newMeta = document.createElement("meta") const newMeta = document.createElement("meta");
newMeta.name = "theme-color" newMeta.name = "theme-color";
document.head.appendChild(newMeta) document.head.appendChild(newMeta);
} }
const themeColor = const themeColor =
currentTheme === "dark" currentTheme === "dark"
? "hsl(30 15% 8%)" // 深色模式背景色 ? "hsl(30 15% 8%)" // 深色模式背景色
: "hsl(0 0% 98%)" // 浅色模式背景色 : "hsl(0 0% 98%)"; // 浅色模式背景色
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor) document
} .querySelector('meta[name="theme-color"]')
?.setAttribute("content", themeColor);
};
// Update on mount and theme change // Update on mount and theme change
updateThemeColor() updateThemeColor();
// Listen for system theme changes // Listen for system theme changes
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener("change", updateThemeColor) mediaQuery.addEventListener("change", updateThemeColor);
return () => mediaQuery.removeEventListener("change", updateThemeColor) return () => mediaQuery.removeEventListener("change", updateThemeColor);
}, [theme]) }, [theme]);
return null return null;
} }
+54 -29
View File
@@ -1,56 +1,81 @@
import { ReactNode, createContext, useEffect, useState } from "react" import { createContext, type ReactNode, useEffect, useState } from "react";
export type Theme = "dark" | "light" | "system" export type Theme = "dark" | "light" | "system";
type ThemeProviderProps = { type ThemeProviderProps = {
children: ReactNode children: ReactNode;
defaultTheme?: Theme defaultTheme?: Theme;
storageKey?: string storageKey?: string;
} };
type ThemeProviderState = { type ThemeProviderState = {
theme: Theme theme: Theme;
setTheme: (theme: Theme) => void setTheme: (theme: Theme) => void;
} };
const initialState: ThemeProviderState = { const initialState: ThemeProviderState = {
theme: "system", theme: "system",
setTheme: () => null, setTheme: () => null,
} };
const ThemeProviderContext = createContext<ThemeProviderState>(initialState) const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({ children, storageKey = "vite-ui-theme" }: ThemeProviderProps) { export function ThemeProvider({
const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || "system") children,
storageKey = "vite-ui-theme",
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || "system",
);
useEffect(() => { useEffect(() => {
const root = window.document.documentElement const root = window.document.documentElement;
root.classList.remove("light", "dark") root.classList.add("disable-transitions");
root.classList.remove("light", "dark");
if (theme === "system") { if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme) root.classList.add(systemTheme);
const themeColor = systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)" const themeColor =
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor) systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
return document
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", themeColor);
const timeoutId = window.setTimeout(() => {
root.classList.remove("disable-transitions");
}, 0);
return () => window.clearTimeout(timeoutId);
} }
root.classList.add(theme) root.classList.add(theme);
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)" const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor) document
}, [theme]) .querySelector('meta[name="theme-color"]')
?.setAttribute("content", themeColor);
const timeoutId = window.setTimeout(() => {
root.classList.remove("disable-transitions");
}, 0);
return () => window.clearTimeout(timeoutId);
}, [theme]);
const value = { const value = {
theme, theme,
setTheme: (theme: Theme) => { setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme) localStorage.setItem(storageKey, theme);
setTheme(theme) setTheme(theme);
}, },
} };
return <ThemeProviderContext.Provider value={value}>{children}</ThemeProviderContext.Provider> return (
<ThemeProviderContext.Provider value={value}>
{children}
</ThemeProviderContext.Provider>
);
} }
export { ThemeProviderContext } export { ThemeProviderContext };
+41 -18
View File
@@ -1,23 +1,31 @@
import { Theme } from "@/components/ThemeProvider" import { CheckCircleIcon } from "@heroicons/react/20/solid";
import { Button } from "@/components/ui/button" import { Moon, Sun } from "lucide-react";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils" import type { Theme } from "@/components/ThemeProvider";
import { CheckCircleIcon } from "@heroicons/react/20/solid" import { Button } from "@/components/ui/button";
import { Moon, Sun } from "lucide-react" import {
import { useTranslation } from "react-i18next" DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { useTheme } from "../hooks/use-theme" import { useTheme } from "../hooks/use-theme";
export function ModeToggle() { export function ModeToggle() {
const { t } = useTranslation() const { t } = useTranslation();
const { setTheme, theme } = useTheme() const { setTheme, theme } = useTheme();
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const handleSelect = (e: Event, newTheme: Theme) => { const handleSelect = (e: Event, newTheme: Theme) => {
e.preventDefault() e.preventDefault();
setTheme(newTheme) setTheme(newTheme);
} };
return ( return (
<DropdownMenu> <DropdownMenu>
@@ -35,19 +43,34 @@ export function ModeToggle() {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="flex flex-col gap-0.5" align="end"> <DropdownMenuContent className="flex flex-col gap-0.5" align="end">
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "light" })} onSelect={(e) => handleSelect(e, "light")}> <DropdownMenuItem
className={cn("rounded-b-[5px] text-xs", {
"gap-3 bg-muted font-semibold": theme === "light",
})}
onSelect={(e) => handleSelect(e, "light")}
>
{t("theme.light")} {t("theme.light")}
{theme === "light" && <CheckCircleIcon className="size-4" />} {theme === "light" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "dark" })} onSelect={(e) => handleSelect(e, "dark")}> <DropdownMenuItem
className={cn("rounded-[5px] text-xs", {
"gap-3 bg-muted font-semibold": theme === "dark",
})}
onSelect={(e) => handleSelect(e, "dark")}
>
{t("theme.dark")} {t("theme.dark")}
{theme === "dark" && <CheckCircleIcon className="size-4" />} {theme === "dark" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "system" })} onSelect={(e) => handleSelect(e, "system")}> <DropdownMenuItem
className={cn("rounded-t-[5px] text-xs", {
"gap-3 bg-muted font-semibold": theme === "system",
})}
onSelect={(e) => handleSelect(e, "system")}
>
{t("theme.system")} {t("theme.system")}
{theme === "system" && <CheckCircleIcon className="size-4" />} {theme === "system" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) );
} }
+62 -24
View File
@@ -1,68 +1,106 @@
import { PublicNoteData, cn, getDaysBetweenDatesWithAutoRenewal } from "@/lib/utils" import { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next" import {
cn,
getDaysBetweenDatesWithAutoRenewal,
type PublicNoteData,
} from "@/lib/utils";
import RemainPercentBar from "./RemainPercentBar" import RemainPercentBar from "./RemainPercentBar";
export default function BillingInfo({ parsedData }: { parsedData: PublicNoteData }) { export default function BillingInfo({
const { t } = useTranslation() parsedData,
}: {
parsedData: PublicNoteData;
}) {
const { t } = useTranslation();
if (!parsedData || !parsedData.billingDataMod) { if (!parsedData || !parsedData.billingDataMod) {
return null return null;
} }
let isNeverExpire = false let isNeverExpire = false;
let daysLeftObject = { let daysLeftObject = {
days: 0, days: 0,
cycleLabel: "", cycleLabel: "",
remainingPercentage: 0, remainingPercentage: 0,
} };
const hasBillingDates =
Boolean(parsedData.billingDataMod.startDate) ||
Boolean(parsedData.billingDataMod.endDate);
if (parsedData?.billingDataMod?.endDate) { if (parsedData?.billingDataMod?.endDate) {
if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) { if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) {
isNeverExpire = true isNeverExpire = true;
} else { } else {
try { try {
daysLeftObject = getDaysBetweenDatesWithAutoRenewal(parsedData.billingDataMod) daysLeftObject = getDaysBetweenDatesWithAutoRenewal(
parsedData.billingDataMod,
);
} catch (error) { } catch (error) {
console.error(error) console.error(error);
return ( return (
<div className={cn("text-[10px] text-muted-foreground text-red-600")}> <div className={cn("text-[10px] text-muted-foreground text-red-600")}>
{t("billingInfo.remaining")}: {t("billingInfo.error")} {t("billingInfo.remaining")}: {t("billingInfo.error")}
</div> </div>
) );
} }
} }
} }
return daysLeftObject.days >= 0 ? ( return daysLeftObject.days >= 0 ? (
<> <>
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? ( {parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}> <p className={cn("text-[10px] text-muted-foreground ")}>
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} {t("billingInfo.price")}: {parsedData.billingDataMod.amount}/
{parsedData.billingDataMod.cycle}
</p> </p>
) : parsedData.billingDataMod.amount === "0" ? ( ) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}>{t("billingInfo.free")}</p> <p className={cn("text-[10px] text-green-600 ")}>
{t("billingInfo.free")}
</p>
) : parsedData.billingDataMod.amount === "-1" ? ( ) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}>{t("billingInfo.usage-baseed")}</p> <p className={cn("text-[10px] text-pink-600 ")}>
{t("billingInfo.usage-baseed")}
</p>
) : null} ) : null}
{hasBillingDates && (
<div className={cn("text-[10px] text-muted-foreground")}> <div className={cn("text-[10px] text-muted-foreground")}>
{t("billingInfo.remaining")}: {isNeverExpire ? t("billingInfo.indefinite") : daysLeftObject.days + " " + t("billingInfo.days")} {t("billingInfo.remaining")}:{" "}
{isNeverExpire
? t("billingInfo.indefinite")
: `${daysLeftObject.days} ${t("billingInfo.days")}`}
</div> </div>
{!isNeverExpire && <RemainPercentBar className="mt-0.5" value={daysLeftObject.remainingPercentage * 100} />} )}
{hasBillingDates && !isNeverExpire && (
<RemainPercentBar
className="mt-0.5"
value={daysLeftObject.remainingPercentage * 100}
/>
)}
</> </>
) : ( ) : (
<> <>
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? ( {parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}> <p className={cn("text-[10px] text-muted-foreground ")}>
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} {t("billingInfo.price")}: {parsedData.billingDataMod.amount}/
{parsedData.billingDataMod.cycle}
</p> </p>
) : parsedData.billingDataMod.amount === "0" ? ( ) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}>{t("billingInfo.free")}</p> <p className={cn("text-[10px] text-green-600 ")}>
{t("billingInfo.free")}
</p>
) : parsedData.billingDataMod.amount === "-1" ? ( ) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}>{t("billingInfo.usage-baseed")}</p> <p className={cn("text-[10px] text-pink-600 ")}>
{t("billingInfo.usage-baseed")}
</p>
) : null} ) : null}
<p className={cn("text-[10px] text-muted-foreground text-red-600")}> <p className={cn("text-[10px] text-muted-foreground text-red-600")}>
{t("billingInfo.expired")}: {daysLeftObject.days * -1} {t("billingInfo.days")} {t("billingInfo.expired")}: {daysLeftObject.days * -1}{" "}
{t("billingInfo.days")}
</p> </p>
</> </>
) );
} }
+24
View File
@@ -0,0 +1,24 @@
export default function ChartSkeleton({
width,
height,
}: {
width?: number | string;
height?: number | string;
}) {
const resolvedWidth = typeof width === "number" ? `${width}px` : width;
const resolvedHeight = typeof height === "number" ? `${height}px` : height;
return (
<div
className="relative h-full w-full overflow-hidden"
style={{
width: resolvedWidth || "100%",
height: resolvedHeight || "100%",
}}
>
<div className="absolute inset-0 flex items-center justify-center">
<div className="size-4 rounded-full border-2 border-muted-foreground/20 border-t-muted-foreground/70 animate-spin" />
</div>
</div>
);
}
+5 -5
View File
@@ -1,4 +1,4 @@
const bars = Array(8).fill(0) const bars = Array(8).fill(0);
export const Loader = ({ visible }: { visible: boolean }) => { export const Loader = ({ visible }: { visible: boolean }) => {
return ( return (
@@ -9,8 +9,8 @@ export const Loader = ({ visible }: { visible: boolean }) => {
))} ))}
</div> </div>
</div> </div>
) );
} };
export const LoadingSpinner = () => { export const LoadingSpinner = () => {
return ( return (
@@ -28,5 +28,5 @@ export const LoadingSpinner = () => {
> >
<path d="M21 12a9 9 0 1 1-6.219-8.56" /> <path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg> </svg>
) );
} };
@@ -1,7 +1,7 @@
import { Skeleton } from "@/components/ui/skeleton" import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom" import { Skeleton } from "@/components/ui/skeleton";
import { BackIcon } from "../Icon" import { BackIcon } from "../Icon";
export function ServerDetailChartLoading() { export function ServerDetailChartLoading() {
return ( return (
@@ -15,17 +15,17 @@ export function ServerDetailChartLoading() {
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</section> </section>
</div> </div>
) );
} }
export function ServerDetailLoading() { export function ServerDetailLoading() {
const navigate = useNavigate() const navigate = useNavigate();
return ( return (
<div className="mx-auto w-full max-w-5xl px-0"> <div className="mx-auto w-full max-w-5xl px-0">
<div <div
onClick={() => { onClick={() => {
navigate("/") navigate("/");
}} }}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl" className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
> >
@@ -34,5 +34,5 @@ export function ServerDetailLoading() {
</div> </div>
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</div> </div>
) );
} }
+1 -1
View File
@@ -1 +1 @@
export { domMax as default } from "framer-motion" export { domMax as default } from "framer-motion";
+5 -4
View File
@@ -1,11 +1,12 @@
import { LazyMotion } from "framer-motion" import { LazyMotion } from "framer-motion";
const loadFeatures = () => import("./framer-lazy-feature").then((res) => res.default) const loadFeatures = () =>
import("./framer-lazy-feature").then((res) => res.default);
export const MotionProvider = ({ children }: { children: React.ReactNode }) => { export const MotionProvider = ({ children }: { children: React.ReactNode }) => {
return ( return (
<LazyMotion features={loadFeatures} strict key="framer"> <LazyMotion features={loadFeatures} strict key="framer">
{children} {children}
</LazyMotion> </LazyMotion>
) );
} };
+18 -12
View File
@@ -1,15 +1,21 @@
import { cn } from "@/lib/utils" import * as AccordionPrimitive from "@radix-ui/react-accordion";
import * as AccordionPrimitive from "@radix-ui/react-accordion" import { ChevronDown } from "lucide-react";
import { ChevronDown } from "lucide-react" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef< const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>, React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />) >(({ className, ...props }, ref) => (
AccordionItem.displayName = "AccordionItem" <AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef< const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>, React.ElementRef<typeof AccordionPrimitive.Trigger>,
@@ -28,8 +34,8 @@ const AccordionTrigger = React.forwardRef<
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" /> <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger> </AccordionPrimitive.Trigger>
</AccordionPrimitive.Header> </AccordionPrimitive.Header>
)) ));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef< const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>, React.ElementRef<typeof AccordionPrimitive.Content>,
@@ -42,8 +48,8 @@ const AccordionContent = React.forwardRef<
> >
<div className={cn("pb-4 pt-0", className)}>{children}</div> <div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content> </AccordionPrimitive.Content>
)) ));
AccordionContent.displayName = AccordionPrimitive.Content.displayName AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -1,17 +1,23 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
interface Props { interface Props {
max: number max: number;
value: number value: number;
min: number min: number;
className?: string className?: string;
primaryColor?: string primaryColor?: string;
} }
export default function AnimatedCircularProgressBar({ max = 100, min = 0, value = 0, primaryColor, className }: Props) { export default function AnimatedCircularProgressBar({
const circumference = 2 * Math.PI * 45 max = 100,
const percentPx = circumference / 100 min = 0,
const currentPercent = ((value - min) / (max - min)) * 100 value = 0,
primaryColor,
className,
}: Props) {
const circumference = 2 * Math.PI * 45;
const percentPx = circumference / 100;
const currentPercent = ((value - min) / (max - min)) * 100;
return ( return (
<div <div
@@ -31,7 +37,12 @@ export default function AnimatedCircularProgressBar({ max = 100, min = 0, value
} as React.CSSProperties } as React.CSSProperties
} }
> >
<svg fill="none" className="size-full" strokeWidth="2" viewBox="0 0 100 100"> <svg
fill="none"
className="size-full"
strokeWidth="2"
viewBox="0 0 100 100"
>
{currentPercent <= 90 && currentPercent >= 0 && ( {currentPercent <= 90 && currentPercent >= 0 && (
<circle <circle
cx="50" cx="50"
@@ -46,10 +57,13 @@ export default function AnimatedCircularProgressBar({ max = 100, min = 0, value
{ {
"--stroke-percent": 90 - currentPercent, "--stroke-percent": 90 - currentPercent,
"--offset-factor-secondary": "calc(1 - var(--offset-factor))", "--offset-factor-secondary": "calc(1 - var(--offset-factor))",
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)", strokeDasharray:
transform: "rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)", "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
transform:
"rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
transition: "all var(--transition-length) ease var(--delay)", transition: "all var(--transition-length) ease var(--delay)",
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)", transformOrigin:
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties } as React.CSSProperties
} }
/> />
@@ -63,17 +77,21 @@ export default function AnimatedCircularProgressBar({ max = 100, min = 0, value
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
className={cn("opacity-100 stroke-current", { className={cn("opacity-100 stroke-current", {
"stroke-[var(--stroke-primary-color)]": primaryColor, "stroke-(--stroke-primary-color)": primaryColor,
})} })}
style={ style={
{ {
"--stroke-primary-color": primaryColor, "--stroke-primary-color": primaryColor,
"--stroke-percent": currentPercent, "--stroke-percent": currentPercent,
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)", strokeDasharray:
transition: "var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)", "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
transition:
"var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
transitionProperty: "stroke-dasharray,transform", transitionProperty: "stroke-dasharray,transform",
transform: "rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))", transform:
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)", "rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))",
transformOrigin:
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties } as React.CSSProperties
} }
/> />
@@ -85,5 +103,5 @@ export default function AnimatedCircularProgressBar({ max = 100, min = 0, value
{currentPercent} {currentPercent}
</span> </span>
</div> </div>
) );
} }
+18 -11
View File
@@ -1,15 +1,18 @@
import { cn } from "@/lib/utils" import { cva, type VariantProps } from "class-variance-authority";
import { type VariantProps, cva } from "class-variance-authority" import type * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
{ {
variants: { variants: {
variant: { variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", default:
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground", outline: "text-foreground",
}, },
}, },
@@ -17,12 +20,16 @@ const badgeVariants = cva(
variant: "default", variant: "default",
}, },
}, },
) );
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {} export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} /> return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
} }
export { Badge, badgeVariants } export { Badge, badgeVariants };
+30 -17
View File
@@ -1,17 +1,20 @@
import { cn } from "@/lib/utils" import { Slot } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority";
import { type VariantProps, cva } from "class-variance-authority" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", destructive:
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", "bg-destructive text-destructive-foreground hover:bg-destructive/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
@@ -27,16 +30,26 @@ const buttonVariants = cva(
size: "default", size: "default",
}, },
}, },
) );
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { export interface ButtonProps
asChild?: boolean extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => { const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
const Comp = asChild ? Slot : "button" ({ className, variant, size, asChild = false, ...props }, ref) => {
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} /> const Comp = asChild ? Slot : "button";
}) return (
Button.displayName = "Button" <Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants } export { Button, buttonVariants };
+73 -26
View File
@@ -1,38 +1,85 @@
import { cn } from "@/lib/utils" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => ( const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn("rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none", className)} className={cn(
"rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
className,
)}
{...props} {...props}
/> />
)) ));
Card.displayName = "Card" Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => ( const CardHeader = React.forwardRef<
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} /> HTMLDivElement,
)) React.HTMLAttributes<HTMLDivElement>
CardHeader.displayName = "CardHeader" >(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => ( const CardTitle = React.forwardRef<
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} /> HTMLParagraphElement,
)) React.HTMLAttributes<HTMLHeadingElement>
CardTitle.displayName = "CardTitle" >(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(({ className, ...props }, ref) => ( const CardDescription = React.forwardRef<
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} /> HTMLParagraphElement,
)) React.HTMLAttributes<HTMLParagraphElement>
CardDescription.displayName = "CardDescription" >(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => ( const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
)) ));
CardContent.displayName = "CardContent" CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => ( const CardFooter = React.forwardRef<
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} /> HTMLDivElement,
)) React.HTMLAttributes<HTMLDivElement>
CardFooter.displayName = "CardFooter" >(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
+211 -91
View File
@@ -1,42 +1,49 @@
import { cn } from "@/lib/utils" "use client";
import * as React from "react"
import * as RechartsPrimitive from "recharts" import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR } // Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = { export type ChartConfig = {
[k in string]: { [k: string]: {
label?: React.ReactNode label?: React.ReactNode;
icon?: React.ComponentType icon?: React.ComponentType;
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> }) } & (
} | { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = { type ChartContextProps = {
config: ChartConfig config: ChartConfig;
} };
const ChartContext = React.createContext<ChartContextProps | null>(null) const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() { function useChart() {
const context = React.useContext(ChartContext) const context = React.useContext(ChartContext);
if (!context) { if (!context) {
throw new Error("useChart must be used within a <ChartContainer />") throw new Error("useChart must be used within a <ChartContainer />");
} }
return context return context;
} }
const ChartContainer = React.forwardRef< const ChartContainer = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
config: ChartConfig config: ChartConfig;
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"] children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"];
} }
>(({ id, className, children, config, ...props }, ref) => { >(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId() const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return ( return (
<ChartContext.Provider value={{ config }}> <ChartContext.Provider value={{ config }}>
@@ -44,24 +51,28 @@ const ChartContainer = React.forwardRef<
data-chart={chartId} data-chart={chartId}
ref={ref} ref={ref}
className={cn( className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none", "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden",
className, className,
)} )}
{...props} {...props}
> >
<ChartStyle id={chartId} config={config} /> <ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer> <RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div> </div>
</ChartContext.Provider> </ChartContext.Provider>
) );
}) });
ChartContainer.displayName = "Chart" ChartContainer.displayName = "Chart";
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color) const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color,
);
if (!colorConfig.length) { if (!colorConfig.length) {
return null return null;
} }
return ( return (
@@ -73,8 +84,10 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
${prefix} [data-chart=${id}] { ${prefix} [data-chart=${id}] {
${colorConfig ${colorConfig
.map(([key, itemConfig]) => { .map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color const color =
return color ? ` --color-${key}: ${color};` : null itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
}) })
.join("\n")} .join("\n")}
} }
@@ -83,20 +96,32 @@ ${colorConfig
.join("\n"), .join("\n"),
}} }}
/> />
) );
} };
const ChartTooltip = RechartsPrimitive.Tooltip const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef< const ChartTooltipContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
hideLabel?: boolean active?: boolean;
hideIndicator?: boolean payload?: any[];
indicator?: "line" | "dot" | "dashed" label?: any;
nameKey?: string hideLabel?: boolean;
labelKey?: string hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
labelFormatter?: (value: any, payload: any[]) => React.ReactNode;
formatter?: (
value: any,
name: any,
item: any,
index: number,
payload: any,
) => React.ReactNode;
color?: string;
labelClassName?: string;
} }
>( >(
( (
@@ -117,49 +142,77 @@ const ChartTooltipContent = React.forwardRef<
}, },
ref, ref,
) => { ) => {
const { config } = useChart() const { config } = useChart();
const tooltipLabel = React.useMemo(() => { const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) { if (hideLabel || !payload?.length) {
return null return null;
} }
const [item] = payload const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || "value"}` const key = `${labelKey || item.dataKey || item.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value = !labelKey && typeof label === "string" ? config[label as keyof typeof config]?.label || label : itemConfig?.label const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) { if (labelFormatter) {
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div> return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
} }
if (!value) { if (!value) {
return null return null;
} }
return <div className={cn("font-medium", labelClassName)}>{value}</div> return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]) }, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
if (!active || !payload?.length) { if (!active || !payload?.length) {
return null return null;
} }
const nestLabel = payload.length === 1 && indicator !== "dot" payload.sort((a, b) => {
return Number(b.value) - Number(a.value);
});
const nestLabel = payload.length === 1 && indicator !== "dot";
return ( return (
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl", "grid min-w-32 items-start gap-1.5 overflow-hidden rounded-sm border border-border/50 bg-stone-100 text-xs dark:bg-stone-900",
className, className,
)} )}
> >
{!nestLabel && (
<div className="mx-auto -mb-1 px-2.5 pt-1">
{!nestLabel ? tooltipLabel : null} {!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5"> </div>
)}
<div
className={cn("grid gap-1.5 bg-white px-2.5 py-1.5 dark:bg-black", {
"border-t": !nestLabel,
})}
>
{payload.map((item, index) => { {payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}` const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color const indicatorColor = color || item.payload.fill || item.color;
return ( return (
<div <div
@@ -178,12 +231,16 @@ const ChartTooltipContent = React.forwardRef<
) : ( ) : (
!hideIndicator && ( !hideIndicator && (
<div <div
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", { className={cn(
"shrink-0 rounded-[2px] border-border bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot", "h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line", "w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed", "w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed", "my-0.5": nestLabel && indicator === "dashed",
})} },
)}
style={ style={
{ {
"--color-bg": indicatorColor, "--color-bg": indicatorColor,
@@ -193,49 +250,84 @@ const ChartTooltipContent = React.forwardRef<
/> />
) )
)} )}
<div className={cn("flex flex-1 justify-between leading-none", nestLabel ? "items-end" : "items-center")}> <div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5"> <div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null} {nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span> <span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div> </div>
{item.value && <span className="font-mono font-medium tabular-nums text-foreground">{item.value.toLocaleString()}</span>} {item.value && (
<span
className={cn(
"ml-2 font-medium text-foreground tabular-nums",
payload.length === 1 && "-ml-9",
)}
>
{typeof item.value === "number"
? item.value.toFixed(2).toLocaleString()
: item.value}{" "}
</span>
)}
</div> </div>
</> </>
)} )}
</div> </div>
) );
})} })}
</div> </div>
</div> </div>
) );
}, },
) );
ChartTooltipContent.displayName = "ChartTooltip" ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef< const ChartLegendContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & React.ComponentProps<"div"> & {
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { payload?: any[];
hideIcon?: boolean verticalAlign?: "top" | "bottom" | "middle";
nameKey?: string hideIcon?: boolean;
nameKey?: string;
} }
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => { >(
const { config } = useChart() (
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref,
) => {
const { config } = useChart();
if (!payload?.length) { if (!payload?.length) {
return null return null;
} }
return ( return (
<div ref={ref} className={cn("flex flex-wrap items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}> <div
ref={ref}
className={cn(
"flex flex-wrap items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className,
)}
>
{payload.map((item) => { {payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}` const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key);
return ( return (
<div key={item.value} className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}> <div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
)}
>
{itemConfig?.icon && !hideIcon ? ( {itemConfig?.icon && !hideIcon ? (
<itemConfig.icon /> <itemConfig.icon />
) : ( ) : (
@@ -246,32 +338,60 @@ const ChartLegendContent = React.forwardRef<
}} }}
/> />
)} )}
{itemConfig?.label} {key}
</div> </div>
) );
})} })}
</div> </div>
) );
}) },
ChartLegendContent.displayName = "ChartLegend" );
ChartLegendContent.displayName = "ChartLegend";
// Helper to extract item config from a payload. // Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) { function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string,
) {
if (typeof payload !== "object" || payload === null) { if (typeof payload !== "object" || payload === null) {
return undefined return undefined;
} }
const payloadPayload = "payload" in payload && typeof payload.payload === "object" && payload.payload !== null ? payload.payload : undefined const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key let configLabelKey: string = key;
if (key in payload && typeof payload[key as keyof typeof payload] === "string") { if (
configLabelKey = payload[key as keyof typeof payload] as string key in payload &&
} else if (payloadPayload && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === "string") { typeof payload[key as keyof typeof payload] === "string"
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string ) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
} }
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config] return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
} }
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle } export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};
+15 -12
View File
@@ -1,24 +1,27 @@
import { cn } from "@/lib/utils" import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import { Check } from "lucide-react";
import { Check } from "lucide-react" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<React.ElementRef<typeof CheckboxPrimitive.Root>, React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>>( const Checkbox = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className, className,
)} )}
{...props} {...props}
> >
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}> <CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
), ));
) Checkbox.displayName = CheckboxPrimitive.Root.displayName;
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox } export { Checkbox };
+109 -57
View File
@@ -1,107 +1,159 @@
"use client" "use client";
import { Dialog, DialogContent } from "@/components/ui/dialog" import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog";
import { cn } from "@/lib/utils" import { Command as CommandPrimitive } from "cmdk";
import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog" import { Search } from "lucide-react";
import { Command as CommandPrimitive } from "cmdk" import * as React from "react";
import { Search } from "lucide-react" import { Dialog, DialogContent } from "@/components/ui/dialog";
import * as React from "react" import { cn } from "@/lib/utils";
const Command = React.forwardRef<React.ElementRef<typeof CommandPrimitive>, React.ComponentPropsWithoutRef<typeof CommandPrimitive>>( const Command = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive <CommandPrimitive
ref={ref} ref={ref}
className={cn("flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", className)} className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props} {...props}
/> />
), ));
) Command.displayName = CommandPrimitive.displayName;
Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, ...props }: DialogProps) => { const CommandDialog = ({ children, ...props }: DialogProps) => {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogTitle /> <DialogTitle />
<DialogContent className="overflow-hidden p-0 shadow-lg"> <DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4"> <Command className="**:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 **:[[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4">
{children} {children}
</Command> </Command>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} };
const CommandInput = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Input>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>>( const CommandInput = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof CommandPrimitive.Input>,
<div className="flex items-center bg-stone-100 dark:bg-stone-900 px-3" cmdk-input-wrapper=""> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div
className="flex items-center bg-stone-100 dark:bg-stone-900 px-3"
cmdk-input-wrapper=""
>
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input <CommandPrimitive.Input
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className, className,
)} )}
{...props} {...props}
/> />
</div> </div>
), ));
)
CommandInput.displayName = CommandPrimitive.Input.displayName CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<React.ElementRef<typeof CommandPrimitive.List>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>>( const CommandList = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof CommandPrimitive.List>,
<CommandPrimitive.List ref={ref} className={cn("max-h-[300px] mb-1 overflow-y-auto overflow-x-hidden", className)} {...props} /> React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
), >(({ className, ...props }, ref) => (
) <CommandPrimitive.List
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Empty>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>>(
(props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />,
)
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Group>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>>(
({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref} ref={ref}
className={cn( className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", "max-h-[300px] mb-1 overflow-y-auto overflow-x-hidden",
className, className,
)} )}
{...props} {...props}
/> />
), ));
)
CommandGroup.displayName = CommandPrimitive.Group.displayName CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef< const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>, React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => <CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />) >(({ className, ...props }, ref) => (
CommandSeparator.displayName = CommandPrimitive.Separator.displayName <CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Item>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>>( const CommandItem = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-[8px] px-2 py-1.5 text-xs outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-stone-100 dark:data-[selected='true']:bg-stone-900 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "relative flex cursor-default gap-2 select-none items-center rounded-[8px] px-2 py-1.5 text-xs outline-hidden data-[disabled=true]:pointer-events-none data-[selected='true']:bg-stone-100 dark:data-[selected='true']:bg-stone-900 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className, className,
)} )}
{...props} {...props}
/> />
), ));
)
CommandItem.displayName = CommandPrimitive.Item.displayName CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { const CommandShortcut = ({
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} /> className,
} ...props
CommandShortcut.displayName = "CommandShortcut" }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className,
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator } export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};
+74 -30
View File
@@ -1,15 +1,16 @@
import { cn } from "@/lib/utils" import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as DialogPrimitive from "@radix-ui/react-dialog" import { X } from "lucide-react";
import { X } from "lucide-react" import * as React from "react";
import * as React from "react"
const Dialog = DialogPrimitive.Root import { cn } from "@/lib/utils";
const DialogTrigger = DialogPrimitive.Trigger const Dialog = DialogPrimitive.Root;
const DialogPortal = DialogPrimitive.Portal const DialogTrigger = DialogPrimitive.Trigger;
const DialogClose = DialogPrimitive.Close const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -23,8 +24,8 @@ const DialogOverlay = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
@@ -35,7 +36,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className, className,
)} )}
{...props} {...props}
@@ -47,30 +48,73 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)) ));
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogHeader = ({
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} /> className,
) ...props
DialogHeader.displayName = "DialogHeader" }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogFooter = ({
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} /> className,
) ...props
DialogFooter.displayName = "DialogFooter" }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Title>, React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>>( const DialogTitle = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof DialogPrimitive.Title>,
<DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} /> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
), >(({ className, ...props }, ref) => (
) <DialogPrimitive.Title
DialogTitle.displayName = DialogPrimitive.Title.displayName ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => <DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />) >(({ className, ...props }, ref) => (
DialogDescription.displayName = DialogPrimitive.Description.displayName <DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription } export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
+66 -41
View File
@@ -1,30 +1,30 @@
import { cn } from "@/lib/utils" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import { Check, ChevronRight, Circle } from "lucide-react";
import { Check, ChevronRight, Circle } from "lucide-react" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef< const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, children, ...props }, ref) => ( >(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent", "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8", inset && "pl-8",
className, className,
)} )}
@@ -33,8 +33,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
{children} {children}
<ChevronRight className="ml-auto h-4 w-4" /> <ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)) ));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -43,13 +44,14 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className, className,
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -60,32 +62,32 @@ const DropdownMenuContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className, className,
)} )}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
)) ));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>, React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default justify-between select-none items-center gap-2 rounded-[10px] px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "relative flex cursor-default justify-between select-none items-center gap-2 rounded-[10px] px-2 py-1.5 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8", inset && "pl-8",
className, className,
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef< const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@@ -94,7 +96,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className, className,
)} )}
checked={checked} checked={checked}
@@ -107,8 +109,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
)) ));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -117,7 +120,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className, className,
)} )}
{...props} {...props}
@@ -129,29 +132,51 @@ const DropdownMenuRadioItem = React.forwardRef<
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
)) ));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef< const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>, React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)} {...props} /> <DropdownMenuPrimitive.Label
)) ref={ref}
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef< const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => <DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />) >(({ className, ...props }, ref) => (
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName <DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { const DropdownMenuShortcut = ({
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} /> className,
} ...props
DropdownMenuShortcut.displayName = "DropdownMenuShortcut" }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export { export {
DropdownMenu, DropdownMenu,
@@ -169,4 +194,4 @@ export {
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuRadioGroup, DropdownMenuRadioGroup,
} };
+12 -9
View File
@@ -1,21 +1,24 @@
import { cn } from "@/lib/utils" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {} export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => { const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return ( return (
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50", "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className, className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
}) },
Input.displayName = "Input" );
Input.displayName = "Input";
export { Input } export { Input };
+18 -9
View File
@@ -1,14 +1,23 @@
import { cn } from "@/lib/utils" import * as LabelPrimitive from "@radix-ui/react-label";
import * as LabelPrimitive from "@radix-ui/react-label" import { cva, type VariantProps } from "class-variance-authority";
import { type VariantProps, cva } from "class-variance-authority" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70") const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants> React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
>(({ className, ...props }, ref) => <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />) VariantProps<typeof labelVariants>
Label.displayName = LabelPrimitive.Root.displayName >(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label } export { Label };
+9 -9
View File
@@ -1,10 +1,10 @@
import { cn } from "@/lib/utils" import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as PopoverPrimitive from "@radix-ui/react-popover" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef< const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, React.ElementRef<typeof PopoverPrimitive.Content>,
@@ -16,13 +16,13 @@ const PopoverContent = React.forwardRef<
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-2xl outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-2xl outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className, className,
)} )}
{...props} {...props}
/> />
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
)) ));
PopoverContent.displayName = PopoverPrimitive.Content.displayName PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent } export { Popover, PopoverTrigger, PopoverContent };
+19 -9
View File
@@ -1,20 +1,30 @@
import { cn } from "@/lib/utils" import * as ProgressPrimitive from "@radix-ui/react-progress";
import * as ProgressPrimitive from "@radix-ui/react-progress" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Progress = React.forwardRef< const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>, React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & { React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string indicatorClassName?: string;
} }
>(({ className, value, indicatorClassName, ...props }, ref) => ( >(({ className, value, indicatorClassName, ...props }, ref) => (
<ProgressPrimitive.Root ref={ref} className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)} {...props}> <ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)} className={cn(
"h-full w-full flex-1 bg-primary transition-all",
indicatorClassName,
)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>
)) ));
Progress.displayName = ProgressPrimitive.Root.displayName Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress } export { Progress };
+66 -35
View File
@@ -1,13 +1,13 @@
import { cn } from "@/lib/utils" import * as SelectPrimitive from "@radix-ui/react-select";
import * as SelectPrimitive from "@radix-ui/react-select" import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { Check, ChevronDown, ChevronUp } from "lucide-react" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef< const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, React.ElementRef<typeof SelectPrimitive.Trigger>,
@@ -16,7 +16,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className, className,
)} )}
{...props} {...props}
@@ -26,28 +26,43 @@ const SelectTrigger = React.forwardRef<
<ChevronDown className="h-4 w-4 opacity-50" /> <ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
)) ));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef< const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}> <SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" /> <ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpButton>
)) ));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef< const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}> <SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
)) ));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef< const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, React.ElementRef<typeof SelectPrimitive.Content>,
@@ -57,7 +72,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content <SelectPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" && position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className, className,
@@ -67,29 +82,40 @@ const SelectContent = React.forwardRef<
> >
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
className={cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")} className={cn(
"p-1",
position === "popper" &&
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)",
)}
> >
{children} {children}
</SelectPrimitive.Viewport> </SelectPrimitive.Viewport>
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
)) ));
SelectContent.displayName = SelectPrimitive.Content.displayName SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>>( const SelectLabel = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof SelectPrimitive.Label>,
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} /> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
), >(({ className, ...props }, ref) => (
) <SelectPrimitive.Label
SelectLabel.displayName = SelectPrimitive.Label.displayName ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>>( const SelectItem = React.forwardRef<
({ className, children, ...props }, ref) => ( React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className, className,
)} )}
{...props} {...props}
@@ -102,15 +128,20 @@ const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
), ));
) SelectItem.displayName = SelectPrimitive.Item.displayName;
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef< const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>, React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => <SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />) >(({ className, ...props }, ref) => (
SelectSeparator.displayName = SelectPrimitive.Separator.displayName <SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export { export {
Select, Select,
@@ -123,4 +154,4 @@ export {
SelectSeparator, SelectSeparator,
SelectScrollUpButton, SelectScrollUpButton,
SelectScrollDownButton, SelectScrollDownButton,
} };
+19 -9
View File
@@ -1,18 +1,28 @@
import { cn } from "@/lib/utils" import * as SeparatorPrimitive from "@radix-ui/react-separator";
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Separator = React.forwardRef<React.ElementRef<typeof SeparatorPrimitive.Root>, React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>>( const Separator = React.forwardRef<
({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
ref={ref} ref={ref}
decorative={decorative} decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)} className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
className,
)}
{...props} {...props}
/> />
), ),
) );
Separator.displayName = SeparatorPrimitive.Root.displayName Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator } export { Separator };
+12 -4
View File
@@ -1,7 +1,15 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) { function Skeleton({
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} /> className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
} }
export { Skeleton } export { Skeleton };
+11 -10
View File
@@ -1,12 +1,14 @@
import { cn } from "@/lib/utils" import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as SwitchPrimitives from "@radix-ui/react-switch" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>>( const Switch = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root <SwitchPrimitives.Root
className={cn( className={cn(
"peer inline-flex h-3 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", "peer inline-flex h-3 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className, className,
)} )}
{...props} {...props}
@@ -18,8 +20,7 @@ const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>,
)} )}
/> />
</SwitchPrimitives.Root> </SwitchPrimitives.Root>
), ));
) Switch.displayName = SwitchPrimitives.Root.displayName;
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch } export { Switch };
+108 -42
View File
@@ -1,50 +1,116 @@
import { cn } from "@/lib/utils" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(({ className, ...props }, ref) => ( const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto"> <div className="relative w-full overflow-auto">
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} /> <table
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} />
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(({ className, ...props }, ref) => (
<tr ref={ref} className={cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", className)} {...props} />
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (
<th
ref={ref} ref={ref}
className={cn("h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className)} className={cn("w-full caption-bottom text-sm", className)}
{...props} {...props}
/> />
)) </div>
TableHead.displayName = "TableHead" ));
Table.displayName = "Table";
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => ( const TableHeader = React.forwardRef<
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} /> HTMLTableSectionElement,
)) React.HTMLAttributes<HTMLTableSectionElement>
TableCell.displayName = "TableCell" >(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(({ className, ...props }, ref) => ( const TableBody = React.forwardRef<
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} /> HTMLTableSectionElement,
)) React.HTMLAttributes<HTMLTableSectionElement>
TableCaption.displayName = "TableCaption" >(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium last:[&>tr]:border-b-0",
className,
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};
+9 -9
View File
@@ -1,12 +1,12 @@
import { cn } from "@/lib/utils" import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef< const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
@@ -21,7 +21,7 @@ const TooltipContent = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
TooltipContent.displayName = TooltipPrimitive.Content.displayName TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+12
View File
@@ -0,0 +1,12 @@
import { createContext } from "react";
export interface CommandContextType {
isOpen: boolean;
openCommand: () => void;
closeCommand: () => void;
toggleCommand: () => void;
}
export const CommandContext = createContext<CommandContextType | undefined>(
undefined,
);
+24
View File
@@ -0,0 +1,24 @@
import { type ReactNode, useCallback, useState } from "react";
import { CommandContext } from "./command-context";
export function CommandProvider({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const openCommand = useCallback(() => setIsOpen(true), []);
const closeCommand = useCallback(() => setIsOpen(false), []);
const toggleCommand = useCallback(() => setIsOpen((prev) => !prev), []);
return (
<CommandContext.Provider
value={{
isOpen,
openCommand,
closeCommand,
toggleCommand,
}}
>
{children}
</CommandContext.Provider>
);
}
+35 -10
View File
@@ -1,18 +1,43 @@
import { createContext } from "react" import { createContext } from "react";
export type SortType = "default" | "name" | "uptime" | "system" | "cpu" | "mem" | "disk" | "up" | "down" | "up total" | "down total" export type SortType =
| "default"
| "name"
| "uptime"
| "system"
| "cpu"
| "mem"
| "disk"
| "up"
| "down"
| "up total"
| "down total";
export const SORT_TYPES: SortType[] = ["default", "name", "uptime", "system", "cpu", "mem", "disk", "up", "down", "up total", "down total"] export const SORT_TYPES: SortType[] = [
"default",
"name",
"uptime",
"system",
"cpu",
"mem",
"disk",
"up",
"down",
"up total",
"down total",
];
export type SortOrder = "asc" | "desc" export type SortOrder = "asc" | "desc";
export const SORT_ORDERS: SortOrder[] = ["desc", "asc"] export const SORT_ORDERS: SortOrder[] = ["desc", "asc"];
export interface SortContextType { export interface SortContextType {
sortType: SortType sortType: SortType;
sortOrder: SortOrder sortOrder: SortOrder;
setSortType: (sortType: SortType) => void setSortType: (sortType: SortType) => void;
setSortOrder: (sortOrder: SortOrder) => void setSortOrder: (sortOrder: SortOrder) => void;
} }
export const SortContext = createContext<SortContextType | undefined>(undefined) export const SortContext = createContext<SortContextType | undefined>(
undefined,
);
+11 -5
View File
@@ -1,10 +1,16 @@
import { ReactNode, useState } from "react" import { type ReactNode, useState } from "react";
import { SortContext, SortOrder, SortType } from "./sort-context" import { SortContext, type SortOrder, type SortType } from "./sort-context";
export function SortProvider({ children }: { children: ReactNode }) { export function SortProvider({ children }: { children: ReactNode }) {
const [sortType, setSortType] = useState<SortType>("default") const [sortType, setSortType] = useState<SortType>("default");
const [sortOrder, setSortOrder] = useState<SortOrder>("desc") const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
return <SortContext.Provider value={{ sortType, setSortType, sortOrder, setSortOrder }}>{children}</SortContext.Provider> return (
<SortContext.Provider
value={{ sortType, setSortType, sortOrder, setSortOrder }}
>
{children}
</SortContext.Provider>
);
} }
+7 -5
View File
@@ -1,10 +1,12 @@
import { createContext } from "react" import { createContext } from "react";
export type Status = "all" | "online" | "offline" export type Status = "all" | "online" | "offline";
export interface StatusContextType { export interface StatusContextType {
status: Status status: Status;
setStatus: (status: Status) => void setStatus: (status: Status) => void;
} }
export const StatusContext = createContext<StatusContextType | undefined>(undefined) export const StatusContext = createContext<StatusContextType | undefined>(
undefined,
);
+8 -4
View File
@@ -1,9 +1,13 @@
import { ReactNode, useState } from "react" import { type ReactNode, useState } from "react";
import { Status, StatusContext } from "./status-context" import { type Status, StatusContext } from "./status-context";
export function StatusProvider({ children }: { children: ReactNode }) { export function StatusProvider({ children }: { children: ReactNode }) {
const [status, setStatus] = useState<Status>("all") const [status, setStatus] = useState<Status>("all");
return <StatusContext.Provider value={{ status, setStatus }}>{children}</StatusContext.Provider> return (
<StatusContext.Provider value={{ status, setStatus }}>
{children}
</StatusContext.Provider>
);
} }
+13 -10
View File
@@ -1,18 +1,21 @@
import { createContext } from "react" import { createContext } from "react";
export interface TooltipData { export interface TooltipData {
centroid: [number, number] centroid: [number, number];
country: string country: string;
count: number count: number;
servers: Array<{ servers: Array<{
name: string id: number;
status: boolean name: string;
}> status: boolean;
}>;
} }
interface TooltipContextType { interface TooltipContextType {
tooltipData: TooltipData | null tooltipData: TooltipData | null;
setTooltipData: (data: TooltipData | null) => void setTooltipData: (data: TooltipData | null) => void;
} }
export const TooltipContext = createContext<TooltipContextType | undefined>(undefined) export const TooltipContext = createContext<TooltipContextType | undefined>(
undefined,
);
+8 -4
View File
@@ -1,9 +1,13 @@
import { ReactNode, useState } from "react" import { type ReactNode, useState } from "react";
import { TooltipContext, TooltipData } from "./tooltip-context" import { TooltipContext, type TooltipData } from "./tooltip-context";
export function TooltipProvider({ children }: { children: ReactNode }) { export function TooltipProvider({ children }: { children: ReactNode }) {
const [tooltipData, setTooltipData] = useState<TooltipData | null>(null) const [tooltipData, setTooltipData] = useState<TooltipData | null>(null);
return <TooltipContext.Provider value={{ tooltipData, setTooltipData }}>{children}</TooltipContext.Provider> return (
<TooltipContext.Provider value={{ tooltipData, setTooltipData }}>
{children}
</TooltipContext.Provider>
);
} }
+8 -8
View File
@@ -1,12 +1,12 @@
import { createContext } from "react" import { createContext } from "react";
export interface WebSocketContextType { export interface WebSocketContextType {
lastMessage: { data: string } | null lastMessage: { data: string } | null;
connected: boolean connected: boolean;
messageHistory: { data: string }[] messageHistory: { data: string }[];
reconnect: () => void reconnect: () => void;
needReconnect: boolean needReconnect: boolean;
setNeedReconnect: (needReconnect: boolean) => void setNeedReconnect: (needReconnect: boolean) => void;
} }
export const WebSocketContext = createContext<WebSocketContextType>({ export const WebSocketContext = createContext<WebSocketContextType>({
@@ -16,4 +16,4 @@ export const WebSocketContext = createContext<WebSocketContextType>({
reconnect: () => {}, reconnect: () => {},
needReconnect: false, needReconnect: false,
setNeedReconnect: () => {}, setNeedReconnect: () => {},
}) });
+89 -75
View File
@@ -1,123 +1,133 @@
import React, { useEffect, useRef, useState } from "react" import type React from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { WebSocketContext, WebSocketContextType } from "./websocket-context" import {
WebSocketContext,
type WebSocketContextType,
} from "./websocket-context";
interface WebSocketProviderProps { interface WebSocketProviderProps {
url: string url: string;
children: React.ReactNode children: React.ReactNode;
} }
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, children }) => { export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null) url,
const [messageHistory, setMessageHistory] = useState<{ data: string }[]>([]) // 新增历史消息状态 children,
const [connected, setConnected] = useState(false) }) => {
const [needReconnect, setNeedReconnect] = useState(false) const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null);
const ws = useRef<WebSocket | null>(null) const [messageHistory, setMessageHistory] = useState<{ data: string }[]>([]); // 新增历史消息状态
const reconnectTimeout = useRef<NodeJS.Timeout>(null) const [connected, setConnected] = useState(false);
const maxReconnectAttempts = 30 const [needReconnect, setNeedReconnect] = useState(false);
const reconnectAttempts = useRef(0) const ws = useRef<WebSocket | null>(null);
const isConnecting = useRef(false) const reconnectTimeout = useRef<NodeJS.Timeout>(null);
const maxReconnectAttempts = 30;
const reconnectAttempts = useRef(0);
const isConnecting = useRef(false);
const cleanup = () => { const cleanup = useCallback(() => {
if (ws.current) { if (ws.current) {
// 移除所有事件监听器 // 移除所有事件监听器
ws.current.onopen = null ws.current.onopen = null;
ws.current.onclose = null ws.current.onclose = null;
ws.current.onmessage = null ws.current.onmessage = null;
ws.current.onerror = null ws.current.onerror = null;
if (ws.current.readyState === WebSocket.OPEN || ws.current.readyState === WebSocket.CONNECTING) { if (
ws.current.close() ws.current.readyState === WebSocket.OPEN ||
ws.current.readyState === WebSocket.CONNECTING
) {
ws.current.close();
} }
ws.current = null ws.current = null;
} }
if (reconnectTimeout.current) { if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current) clearTimeout(reconnectTimeout.current);
reconnectTimeout.current = null reconnectTimeout.current = null;
}
setConnected(false)
} }
setConnected(false);
}, []);
const connect = () => { const connect = useCallback(() => {
if (isConnecting.current) { if (isConnecting.current) {
console.log("Connection already in progress") console.log("Connection already in progress");
return return;
} }
cleanup() cleanup();
isConnecting.current = true isConnecting.current = true;
try { try {
const wsUrl = new URL(url, window.location.origin) const wsUrl = new URL(url, window.location.origin);
wsUrl.protocol = wsUrl.protocol.replace("http", "ws") wsUrl.protocol = wsUrl.protocol.replace("http", "ws");
ws.current = new WebSocket(wsUrl.toString()) ws.current = new WebSocket(wsUrl.toString());
ws.current.onopen = () => { ws.current.onopen = () => {
console.log("WebSocket connected") console.log("WebSocket connected");
setConnected(true) setConnected(true);
reconnectAttempts.current = 0 reconnectAttempts.current = 0;
isConnecting.current = false isConnecting.current = false;
} };
ws.current.onclose = () => { ws.current.onclose = () => {
console.log("WebSocket disconnected") console.log("WebSocket disconnected");
setConnected(false) setConnected(false);
ws.current = null ws.current = null;
isConnecting.current = false isConnecting.current = false;
if (reconnectAttempts.current < maxReconnectAttempts) { if (reconnectAttempts.current < maxReconnectAttempts) {
reconnectTimeout.current = setTimeout(() => { reconnectTimeout.current = setTimeout(() => {
reconnectAttempts.current++ reconnectAttempts.current++;
connect() connect();
}, 3000) }, 3000);
}
} }
};
ws.current.onmessage = (event) => { ws.current.onmessage = (event) => {
const newMessage = { data: event.data } const newMessage = { data: event.data };
setLastMessage(newMessage) setLastMessage(newMessage);
// 更新历史消息,保持最新的30条记录 // 更新历史消息,保持最新的30条记录
setMessageHistory((prev) => { setMessageHistory((prev) => {
const updated = [newMessage, ...prev] const updated = [newMessage, ...prev];
return updated.slice(0, 30) return updated.slice(0, 30);
}) });
} };
ws.current.onerror = (error) => { ws.current.onerror = (error) => {
console.error("WebSocket error:", error) console.error("WebSocket error:", error);
isConnecting.current = false isConnecting.current = false;
} };
} catch (error) { } catch (error) {
console.error("WebSocket connection error:", error) console.error("WebSocket connection error:", error);
isConnecting.current = false isConnecting.current = false;
}
} }
}, [cleanup, url]);
const reconnect = () => { const reconnect = () => {
reconnectAttempts.current = 0 reconnectAttempts.current = 0;
// 等待一个小延时确保清理完成 // 等待一个小延时确保清理完成
cleanup() cleanup();
setTimeout(() => { setTimeout(() => {
connect() connect();
}, 1000) }, 1000);
} };
useEffect(() => { useEffect(() => {
connect() connect();
// 添加页面卸载事件监听 // 添加页面卸载事件监听
const handleBeforeUnload = () => { const handleBeforeUnload = () => {
cleanup() cleanup();
} };
window.addEventListener("beforeunload", handleBeforeUnload) window.addEventListener("beforeunload", handleBeforeUnload);
return () => { return () => {
cleanup() cleanup();
window.removeEventListener("beforeunload", handleBeforeUnload) window.removeEventListener("beforeunload", handleBeforeUnload);
} };
}, [url]) }, [cleanup, connect]);
const contextValue: WebSocketContextType = { const contextValue: WebSocketContextType = {
lastMessage, lastMessage,
@@ -126,7 +136,11 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, child
reconnect, reconnect,
needReconnect, needReconnect,
setNeedReconnect, setNeedReconnect,
} };
return <WebSocketContext.Provider value={contextValue}>{children}</WebSocketContext.Provider> return (
} <WebSocketContext.Provider value={contextValue}>
{children}
</WebSocketContext.Provider>
);
};
+37 -29
View File
@@ -1,60 +1,68 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react";
declare global { declare global {
interface Window { interface Window {
CustomBackgroundImage: string CustomBackgroundImage: string;
CustomMobileBackgroundImage: string CustomMobileBackgroundImage: string;
ForceShowServices: boolean ForceShowServices: boolean;
ForceCardInline: boolean ForceCardInline: boolean;
ForceShowMap: boolean ForceShowMap: boolean;
ForcePeakCutEnabled: boolean ForcePeakCutEnabled: boolean;
} }
} }
const BACKGROUND_CHANGE_EVENT = "backgroundChange" const BACKGROUND_CHANGE_EVENT = "backgroundChange";
export function useBackground() { export function useBackground() {
const [backgroundImage, setBackgroundImage] = useState<string | undefined>(undefined) const [backgroundImage, setBackgroundImage] = useState<string | undefined>(
undefined,
);
useEffect(() => { useEffect(() => {
// 监听背景变化 // 监听背景变化
const handleBackgroundChange = () => { const handleBackgroundChange = () => {
setBackgroundImage(window.CustomBackgroundImage || undefined) setBackgroundImage(window.CustomBackgroundImage || undefined);
} };
// 初始化检查 // 初始化检查
const checkInitialBackground = () => { const checkInitialBackground = () => {
if (window.CustomBackgroundImage) { if (window.CustomBackgroundImage) {
setBackgroundImage(window.CustomBackgroundImage) setBackgroundImage(window.CustomBackgroundImage);
} else { } else {
const savedImage = sessionStorage.getItem("savedBackgroundImage") const savedImage = sessionStorage.getItem("savedBackgroundImage");
if (savedImage) { if (savedImage) {
window.CustomBackgroundImage = savedImage window.CustomBackgroundImage = savedImage;
setBackgroundImage(savedImage) setBackgroundImage(savedImage);
}
} }
} }
};
// 设置一个轮询来检查初始背景 // 设置一个轮询来检查初始背景
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
if (window.CustomBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) { if (
checkInitialBackground() window.CustomBackgroundImage ||
clearInterval(intervalId) sessionStorage.getItem("savedBackgroundImage")
) {
checkInitialBackground();
clearInterval(intervalId);
} }
}, 100) }, 100);
window.addEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange) window.addEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange);
return () => { return () => {
window.removeEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange) window.removeEventListener(
clearInterval(intervalId) BACKGROUND_CHANGE_EVENT,
} handleBackgroundChange,
}, []) );
clearInterval(intervalId);
};
}, []);
const updateBackground = (newBackground: string | undefined) => { const updateBackground = (newBackground: string | undefined) => {
window.CustomBackgroundImage = newBackground || "" window.CustomBackgroundImage = newBackground || "";
window.dispatchEvent(new Event(BACKGROUND_CHANGE_EVENT)) window.dispatchEvent(new Event(BACKGROUND_CHANGE_EVENT));
} };
return { backgroundImage, updateBackground } return { backgroundImage, updateBackground };
} }
+9 -9
View File
@@ -1,26 +1,26 @@
import { NezhaWebsocketResponse } from "@/types/nezha-api" import { useEffect, useState } from "react";
import { useEffect, useState } from "react" import type { NezhaWebsocketResponse } from "@/types/nezha-api";
export function useChartHistory<T>( export function useChartHistory<T>(
messageHistory: { data: string }[], messageHistory: { data: string }[],
serverId: number, serverId: number,
formatFn: (wsData: NezhaWebsocketResponse, serverId: number) => T | null, formatFn: (wsData: NezhaWebsocketResponse, serverId: number) => T | null,
) { ) {
const [data, setData] = useState<T[]>([]) const [data, setData] = useState<T[]>([]);
useEffect(() => { useEffect(() => {
if (messageHistory.length > 0 && data.length === 0) { if (messageHistory.length > 0 && data.length === 0) {
const historyData = messageHistory const historyData = messageHistory
.map((msg) => { .map((msg) => {
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse;
return formatFn(wsData, serverId) return formatFn(wsData, serverId);
}) })
.filter((item): item is T => item !== null) .filter((item): item is T => item !== null)
.reverse() .reverse();
setData(historyData) setData(historyData);
} }
}, [messageHistory]) }, [messageHistory, data.length, formatFn, serverId]);
return data return data;
} }
+10
View File
@@ -0,0 +1,10 @@
import { useContext } from "react";
import { CommandContext } from "@/context/command-context";
export function useCommand() {
const context = useContext(CommandContext);
if (context === undefined) {
throw new Error("useCommand must be used within a CommandProvider");
}
return context;
}
+5 -5
View File
@@ -1,10 +1,10 @@
import { SortContext } from "@/context/sort-context" import { useContext } from "react";
import { useContext } from "react" import { SortContext } from "@/context/sort-context";
export function useSort() { export function useSort() {
const context = useContext(SortContext) const context = useContext(SortContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useStatus must be used within a SortProvider") throw new Error("useStatus must be used within a SortProvider");
} }
return context return context;
} }
+5 -5
View File
@@ -1,11 +1,11 @@
import { useContext } from "react" import { useContext } from "react";
import { StatusContext } from "../context/status-context" import { StatusContext } from "../context/status-context";
export function useStatus() { export function useStatus() {
const context = useContext(StatusContext) const context = useContext(StatusContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useStatus must be used within a StatusProvider") throw new Error("useStatus must be used within a StatusProvider");
} }
return context return context;
} }
+6 -6
View File
@@ -1,13 +1,13 @@
import { useContext } from "react" import { useContext } from "react";
import { ThemeProviderContext } from "../components/ThemeProvider" import { ThemeProviderContext } from "../components/ThemeProvider";
export const useTheme = () => { export const useTheme = () => {
const context = useContext(ThemeProviderContext) const context = useContext(ThemeProviderContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider") throw new Error("useTheme must be used within a ThemeProvider");
} }
return context return context;
} };
+7 -7
View File
@@ -1,12 +1,12 @@
import { TooltipContext } from "@/context/tooltip-context" import { useContext } from "react";
import { useContext } from "react" import { TooltipContext } from "@/context/tooltip-context";
export const useTooltip = () => { export const useTooltip = () => {
const context = useContext(TooltipContext) const context = useContext(TooltipContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useTooltip must be used within a TooltipProvider") throw new Error("useTooltip must be used within a TooltipProvider");
} }
return context return context;
} };
export default useTooltip export default useTooltip;
+8 -6
View File
@@ -1,11 +1,13 @@
import { useContext } from "react" import { useContext } from "react";
import { WebSocketContext } from "../context/websocket-context" import { WebSocketContext } from "../context/websocket-context";
export const useWebSocketContext = () => { export const useWebSocketContext = () => {
const context = useContext(WebSocketContext) const context = useContext(WebSocketContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useWebSocketContext must be used within a WebSocketProvider") throw new Error(
"useWebSocketContext must be used within a WebSocketProvider",
);
} }
return context return context;
} };
+16 -16
View File
@@ -1,13 +1,13 @@
import i18n from "i18next" import i18n from "i18next";
import { initReactI18next } from "react-i18next" import { initReactI18next } from "react-i18next";
import deTranslation from "./locales/de/translation.json" import deTranslation from "./locales/de/translation.json";
import enTranslation from "./locales/en/translation.json" import enTranslation from "./locales/en/translation.json";
import esTranslation from "./locales/es/translation.json" import esTranslation from "./locales/es/translation.json";
import ruTranslation from "./locales/ru/translation.json" import ruTranslation from "./locales/ru/translation.json";
import taTranslation from "./locales/ta/translation.json" import taTranslation from "./locales/ta/translation.json";
import zhCNTranslation from "./locales/zh-CN/translation.json" import zhCNTranslation from "./locales/zh-CN/translation.json";
import zhTWTranslation from "./locales/zh-TW/translation.json" import zhTWTranslation from "./locales/zh-TW/translation.json";
const resources = { const resources = {
"en-US": { "en-US": {
@@ -31,11 +31,11 @@ const resources = {
"ta-IN": { "ta-IN": {
translation: taTranslation, translation: taTranslation,
}, },
} };
const getStoredLanguage = () => { const getStoredLanguage = () => {
return localStorage.getItem("language") || "en-US" return localStorage.getItem("language") || "en-US";
} };
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
resources, resources,
@@ -44,11 +44,11 @@ i18n.use(initReactI18next).init({
interpolation: { interpolation: {
escapeValue: false, // react已经安全地转义 escapeValue: false, // react已经安全地转义
}, },
}) });
// 添加语言改变时的处理函数 // 添加语言改变时的处理函数
i18n.on("languageChanged", (lng) => { i18n.on("languageChanged", (lng) => {
localStorage.setItem("language", lng) localStorage.setItem("language", lng);
}) });
export default i18n export default i18n;
+162 -13
View File
@@ -1,8 +1,101 @@
@tailwind base; @import "tailwindcss";
@tailwind components;
@tailwind utilities;
:root { @plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans: var(--font-sans);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
}
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
@utility step {
counter-increment: step;
&:before {
@apply border-background bg-muted absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 text-center -indent-px font-mono text-base font-medium;
@apply mt-[-4px] ml-[-50px];
content: counter(step);
}
}
@layer utilities {
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5; line-height: 1.5;
font-weight: 400; font-weight: 400;
@@ -15,6 +108,14 @@
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
}
}
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
} }
@layer base { @layer base {
@@ -49,6 +150,7 @@
--chart-8: 252 50% 50%; --chart-8: 252 50% 50%;
--chart-9: 288 50% 50%; --chart-9: 288 50% 50%;
--chart-10: 324 50% 50%; --chart-10: 324 50% 50%;
--timing: cubic-bezier(0.4, 0, 0.2, 1);
} }
.dark { .dark {
@@ -99,15 +201,12 @@
} }
} }
@layer utilities { @layer base {
.step { /* Avoid color fade when toggling themes. */
counter-increment: step; html.disable-transitions *,
} html.disable-transitions *::before,
html.disable-transitions *::after {
.step:before { transition: none;
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
} }
} }
@@ -232,3 +331,53 @@
.scrollbar-hidden::-webkit-scrollbar { .scrollbar-hidden::-webkit-scrollbar {
display: none; /* Chrome, Safari 和 Opera */ 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);
}
}
+16 -6
View File
@@ -1,11 +1,21 @@
export function formatBytes(bytes: number, decimals: number = 2) { export function formatBytes(bytes: number, decimals: number = 2) {
if (!+bytes) return "0 Bytes" if (!+bytes) return "0 Bytes";
const k = 1024 const k = 1024;
const dm = decimals < 0 ? 0 : decimals const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] const sizes = [
"Bytes",
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
];
const i = Math.floor(Math.log(bytes) / Math.log(k)) const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}` return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
} }
File diff suppressed because one or more lines are too long
+6 -2
View File
@@ -1,4 +1,7 @@
export const countryCoordinates: Record<string, { lat: number; lng: number; name: string }> = { export const countryCoordinates: Record<
string,
{ lat: number; lng: number; name: string }
> = {
// 亚洲 // 亚洲
AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗 AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗
AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚 AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚
@@ -10,6 +13,7 @@ export const countryCoordinates: Record<string, { lat: number; lng: number; name
KH: { lat: 13.0, lng: 105.0, name: "Cambodia" }, // 柬埔寨 KH: { lat: 13.0, lng: 105.0, name: "Cambodia" }, // 柬埔寨
CN: { lat: 35.0, lng: 105.0, name: "China" }, // 中国 CN: { lat: 35.0, lng: 105.0, name: "China" }, // 中国
HK: { lat: 22.0, lng: 114.0, name: "Hong Kong" }, // 香港 HK: { lat: 22.0, lng: 114.0, name: "Hong Kong" }, // 香港
MO: { lat: 22.1667, lng: 113.55, name: "Macau" }, // 澳门
CY: { lat: 35.0, lng: 33.0, name: "Cyprus" }, // 塞浦路斯 CY: { lat: 35.0, lng: 33.0, name: "Cyprus" }, // 塞浦路斯
GE: { lat: 42.0, lng: 43.5, name: "Georgia" }, // 格鲁吉亚 GE: { lat: 42.0, lng: 43.5, name: "Georgia" }, // 格鲁吉亚
IN: { lat: 20.0, lng: 77.0, name: "India" }, // 印度 IN: { lat: 20.0, lng: 77.0, name: "India" }, // 印度
@@ -205,4 +209,4 @@ export const countryCoordinates: Record<string, { lat: number; lng: number; name
EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉 EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉
ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚 ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚
ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦 ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦
} };
+65 -57
View File
@@ -1,99 +1,107 @@
export const InjectContext = (content: string) => { export const InjectContext = (content: string) => {
const tempDiv = document.createElement("div") const tempDiv = document.createElement("div");
tempDiv.innerHTML = content tempDiv.innerHTML = content;
const INJECTION_MARK = "data-injected" // 自定义属性标识 const INJECTION_MARK = "data-injected"; // 自定义属性标识
// 清理已有的注入资源 // 清理已有的注入资源
const cleanInjectedResources = () => { const cleanInjectedResources = () => {
document.querySelectorAll(`[${INJECTION_MARK}]`).forEach((node) => node.remove()) document.querySelectorAll(`[${INJECTION_MARK}]`).forEach((node) => {
} node.remove();
});
};
const loadExternalScript = (scriptElement: HTMLScriptElement): Promise<void> => { const loadExternalScript = (
scriptElement: HTMLScriptElement,
): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const script = document.createElement("script") const script = document.createElement("script");
script.src = scriptElement.src script.src = scriptElement.src;
script.async = false // 保持顺序执行 script.async = false; // 保持顺序执行
script.setAttribute(INJECTION_MARK, "true") // 添加标识 script.setAttribute(INJECTION_MARK, "true"); // 添加标识
script.onload = () => resolve() script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load script: ${scriptElement.src}`)) script.onerror = () =>
document.head.appendChild(script) reject(new Error(`Failed to load script: ${scriptElement.src}`));
}) document.head.appendChild(script);
} });
};
const executeInlineScript = (scriptContent: string): Promise<void> => { const executeInlineScript = (scriptContent: string): Promise<void> => {
return new Promise((resolve) => { return new Promise((resolve) => {
const script = document.createElement("script") const script = document.createElement("script");
script.textContent = scriptContent script.textContent = scriptContent;
script.setAttribute(INJECTION_MARK, "true") // 添加标识 script.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.body.appendChild(script) document.body.appendChild(script);
resolve() resolve();
}) });
} };
const loadStyle = (styleElement: HTMLStyleElement): Promise<void> => { const loadStyle = (styleElement: HTMLStyleElement): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if ((styleElement as any).href) { if ((styleElement as any).href) {
// 处理 <link> // 处理 <link>
const link = document.createElement("link") const link = document.createElement("link");
link.rel = "stylesheet" link.rel = "stylesheet";
link.href = (styleElement as any).href link.href = (styleElement as any).href;
link.setAttribute(INJECTION_MARK, "true") // 添加标识 link.setAttribute(INJECTION_MARK, "true"); // 添加标识
link.onload = () => resolve() link.onload = () => resolve();
link.onerror = () => reject(new Error(`Failed to load stylesheet: ${link.href}`)) link.onerror = () =>
document.head.appendChild(link) reject(new Error(`Failed to load stylesheet: ${link.href}`));
document.head.appendChild(link);
} else { } else {
const style = document.createElement("style") const style = document.createElement("style");
style.textContent = styleElement.textContent style.textContent = styleElement.textContent;
style.setAttribute(INJECTION_MARK, "true") // 添加标识 style.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.head.appendChild(style) document.head.appendChild(style);
resolve() resolve();
}
})
} }
});
};
const handlers: { [key: string]: (element: HTMLElement) => Promise<void> } = { const handlers: { [key: string]: (element: HTMLElement) => Promise<void> } = {
SCRIPT: (element) => { SCRIPT: (element) => {
const scriptElement = element as HTMLScriptElement const scriptElement = element as HTMLScriptElement;
if (scriptElement.src) { if (scriptElement.src) {
// 加载外部脚本 // 加载外部脚本
return loadExternalScript(scriptElement) return loadExternalScript(scriptElement);
} else { } else {
// 执行内联脚本 // 执行内联脚本
return executeInlineScript(scriptElement.textContent || "") return executeInlineScript(scriptElement.textContent || "");
} }
}, },
STYLE: (element) => loadStyle(element as HTMLStyleElement), STYLE: (element) => loadStyle(element as HTMLStyleElement),
META: (element) => { META: (element) => {
const meta = element.cloneNode(true) as HTMLElement const meta = element.cloneNode(true) as HTMLElement;
meta.setAttribute(INJECTION_MARK, "true") // 添加标识 meta.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.head.appendChild(meta) // 将 meta 标签插入到 <head> document.head.appendChild(meta); // 将 meta 标签插入到 <head>
return Promise.resolve() return Promise.resolve();
}, },
DEFAULT: (element) => { DEFAULT: (element) => {
element.setAttribute(INJECTION_MARK, "true") // 添加标识 element.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.body.appendChild(element) document.body.appendChild(element);
return Promise.resolve() return Promise.resolve();
}, },
} };
// 开始注入前清理已有资源 // 开始注入前清理已有资源
cleanInjectedResources() cleanInjectedResources();
const executeSequentially = async () => { const executeSequentially = async () => {
for (const node of Array.from(tempDiv.childNodes)) { for (const node of Array.from(tempDiv.childNodes)) {
if (node.nodeType === Node.ELEMENT_NODE) { if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement const element = node as HTMLElement;
const handler = handlers[element.tagName] || handlers.DEFAULT const handler = handlers[element.tagName] || handlers.DEFAULT;
await handler(element) // 按顺序等待当前脚本或资源完成处理 await handler(element); // 按顺序等待当前脚本或资源完成处理
} else if (node.nodeType === Node.TEXT_NODE) { } else if (node.nodeType === Node.TEXT_NODE) {
document.body.appendChild(document.createTextNode(node.textContent || "")) document.body.appendChild(
document.createTextNode(node.textContent || ""),
);
} }
} }
console.log("All resources have been injected and executed in sequence.") console.log("All resources have been injected and executed in sequence.");
} };
return executeSequentially().catch((error) => { return executeSequentially().catch((error) => {
console.error("Error during resource injection:", error) console.error("Error during resource injection:", error);
}) });
} };
+29 -23
View File
@@ -1,4 +1,4 @@
import type { SVGProps } from "react" import type { SVGProps } from "react";
export function GetFontLogoClass(platform: string): string { export function GetFontLogoClass(platform: string): string {
if ( if (
@@ -47,24 +47,24 @@ export function GetFontLogoClass(platform: string): string {
"zorin", "zorin",
].indexOf(platform) > -1 ].indexOf(platform) > -1
) { ) {
return platform return platform;
} }
if (platform == "darwin") { if (platform === "darwin") {
return "apple" return "apple";
} }
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) { if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "tux" return "tux";
} }
if (platform == "amazon") { if (platform === "amazon") {
return "redhat" return "redhat";
} }
if (platform == "arch") { if (platform === "arch") {
return "archlinux" return "archlinux";
} }
if (platform.toLowerCase().includes("opensuse")) { if (platform.toLowerCase().includes("opensuse")) {
return "opensuse" return "opensuse";
} }
return "tux" return "tux";
} }
export function GetOsName(platform: string): string { export function GetOsName(platform: string): string {
@@ -110,33 +110,39 @@ export function GetOsName(platform: string): string {
"zorin", "zorin",
].indexOf(platform) > -1 ].indexOf(platform) > -1
) { ) {
return platform.charAt(0).toUpperCase() + platform.slice(1) return platform.charAt(0).toUpperCase() + platform.slice(1);
} }
if (platform == "darwin") { if (platform === "darwin") {
return "macOS" return "macOS";
} }
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) { if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "Linux" return "Linux";
} }
if (platform == "amazon") { if (platform === "amazon") {
return "Redhat" return "Redhat";
} }
if (platform == "arch") { if (platform === "arch") {
return "Archlinux" return "Archlinux";
} }
if (platform.toLowerCase().includes("opensuse")) { if (platform.toLowerCase().includes("opensuse")) {
return "Opensuse" return "Opensuse";
} }
return "Linux" return "Linux";
} }
export function MageMicrosoftWindows(props: SVGProps<SVGSVGElement>) { export function MageMicrosoftWindows(props: SVGProps<SVGSVGElement>) {
return ( return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}> <svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path <path
fill="currentColor" fill="currentColor"
d="M2.75 7.189V2.865c0-.102 0-.115.115-.115h8.622c.128 0 .14 0 .14.128V11.5c0 .128 0 .128-.14.128H2.865c-.102 0-.115 0-.115-.116zM7.189 21.25H2.865c-.102 0-.115 0-.115-.116V12.59c0-.128 0-.128.128-.128h8.635c.102 0 .115 0 .115.115v8.57c0 .09 0 .103-.116.103zM21.25 7.189v4.31c0 .116 0 .116-.116.116h-8.557c-.102 0-.128 0-.128-.115V2.865c0-.09 0-.102.115-.102h8.48c.206 0 .206 0 .206.205zm-8.763 9.661v-4.273c0-.09 0-.115.103-.09h8.621c.026 0 0 .09 0 .142v8.518a.06.06 0 0 1-.017.06a.06.06 0 0 1-.06.017H12.54s-.09 0-.077-.09V16.85z" d="M2.75 7.189V2.865c0-.102 0-.115.115-.115h8.622c.128 0 .14 0 .14.128V11.5c0 .128 0 .128-.14.128H2.865c-.102 0-.115 0-.115-.116zM7.189 21.25H2.865c-.102 0-.115 0-.115-.116V12.59c0-.128 0-.128.128-.128h8.635c.102 0 .115 0 .115.115v8.57c0 .09 0 .103-.116.103zM21.25 7.189v4.31c0 .116 0 .116-.116.116h-8.557c-.102 0-.128 0-.128-.115V2.865c0-.09 0-.102.115-.102h8.48c.206 0 .206 0 .206.205zm-8.763 9.661v-4.273c0-.09 0-.115.103-.09h8.621c.026 0 0 .09 0 .142v8.518a.06.06 0 0 1-.017.06a.06.06 0 0 1-.06.017H12.54s-.09 0-.077-.09V16.85z"
></path> ></path>
</svg> </svg>
) );
} }
+66 -31
View File
@@ -1,55 +1,90 @@
import { LoginUserResponse, MonitorResponse, ServerGroupResponse, ServiceResponse, SettingResponse } from "@/types/nezha-api" import type {
LoginUserResponse,
MetricPeriod,
MetricType,
MonitorResponse,
ServerGroupResponse,
ServerMetricsResponse,
ServiceResponse,
SettingResponse,
} from "@/types/nezha-api";
let lastestRefreshTokenAt = 0 let lastestRefreshTokenAt = 0;
export const fetchServerGroup = async (): Promise<ServerGroupResponse> => { export const fetchServerGroup = async (): Promise<ServerGroupResponse> => {
const response = await fetch("/api/v1/server-group") const response = await fetch("/api/v1/server-group");
const data = await response.json() const data = await response.json();
if (data.error) { if (data.error) {
throw new Error(data.error) throw new Error(data.error);
} }
return data return data;
} };
export const fetchLoginUser = async (): Promise<LoginUserResponse> => { export const fetchLoginUser = async (): Promise<LoginUserResponse> => {
const response = await fetch("/api/v1/profile") const response = await fetch("/api/v1/profile");
const data = await response.json() const data = await response.json();
if (data.error) { if (data.error) {
throw new Error(data.error) throw new Error(data.error);
} }
// auto refresh token // auto refresh token
if (document.cookie && (!lastestRefreshTokenAt || Date.now() - lastestRefreshTokenAt > 1000 * 60 * 60)) { if (
lastestRefreshTokenAt = Date.now() document.cookie &&
fetch("/api/v1/refresh-token") (!lastestRefreshTokenAt ||
Date.now() - lastestRefreshTokenAt > 1000 * 60 * 60)
) {
lastestRefreshTokenAt = Date.now();
fetch("/api/v1/refresh-token");
} }
return data return data;
} };
export const fetchMonitor = async (server_id: number): Promise<MonitorResponse> => { export type MonitorPeriod = "1d" | "7d" | "30d";
const response = await fetch(`/api/v1/service/${server_id}`)
const data = await response.json() export const fetchMonitor = async (
server_id: number,
period?: MonitorPeriod,
): Promise<MonitorResponse> => {
const query = period ? `?period=${period}` : "";
const response = await fetch(`/api/v1/server/${server_id}/service${query}`);
const data = await response.json();
if (data.error) { if (data.error) {
throw new Error(data.error) throw new Error(data.error);
} }
return data return data;
} };
export const fetchService = async (): Promise<ServiceResponse> => { export const fetchService = async (): Promise<ServiceResponse> => {
const response = await fetch("/api/v1/service") const response = await fetch("/api/v1/service");
const data = await response.json() const data = await response.json();
if (data.error) { if (data.error) {
throw new Error(data.error) throw new Error(data.error);
} }
return data return data;
} };
export const fetchSetting = async (): Promise<SettingResponse> => { export const fetchSetting = async (): Promise<SettingResponse> => {
const response = await fetch("/api/v1/setting") const response = await fetch("/api/v1/setting");
const data = await response.json() const data = await response.json();
if (data.error) { if (data.error) {
throw new Error(data.error) throw new Error(data.error);
} }
return data return data;
} };
export const fetchServerMetrics = async (
server_id: number,
metric: MetricType,
period?: MetricPeriod,
): Promise<ServerMetricsResponse> => {
const query = period
? `?metric=${metric}&period=${period}`
: `?metric=${metric}`;
const response = await fetch(`/api/v1/server/${server_id}/metrics${query}`);
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
return data;
};
+136 -114
View File
@@ -1,14 +1,16 @@
import { NezhaServer } from "@/types/nezha-api" import { type ClassValue, clsx } from "clsx";
import { type ClassValue, clsx } from "clsx" import dayjs from "dayjs";
import dayjs from "dayjs" import { twMerge } from "tailwind-merge";
import { twMerge } from "tailwind-merge" import type { NezhaServer } from "@/types/nezha-api";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs));
} }
export function formatNezhaInfo(now: number, serverInfo: NezhaServer) { export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
const lastActiveTime = serverInfo.last_active.startsWith("000") ? 0 : parseISOTimestamp(serverInfo.last_active) const lastActiveTime = serverInfo.last_active.startsWith("000")
? 0
: parseISOTimestamp(serverInfo.last_active);
return { return {
...serverInfo, ...serverInfo,
cpu: serverInfo.state.cpu || 0, cpu: serverInfo.state.cpu || 0,
@@ -16,7 +18,9 @@ export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
process: serverInfo.state.process_count || 0, process: serverInfo.state.process_count || 0,
up: serverInfo.state.net_out_speed / 1024 / 1024 || 0, up: serverInfo.state.net_out_speed / 1024 / 1024 || 0,
down: serverInfo.state.net_in_speed / 1024 / 1024 || 0, down: serverInfo.state.net_in_speed / 1024 / 1024 || 0,
last_active_time_string: lastActiveTime ? dayjs(lastActiveTime).format("YYYY-MM-DD HH:mm:ss") : "", last_active_time_string: lastActiveTime
? dayjs(lastActiveTime).format("YYYY-MM-DD HH:mm:ss")
: "",
online: now - lastActiveTime <= 30000, online: now - lastActiveTime <= 30000,
uptime: serverInfo.state.uptime || 0, uptime: serverInfo.state.uptime || 0,
version: serverInfo.host.version || null, version: serverInfo.host.version || null,
@@ -35,7 +39,9 @@ export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
swap_total: serverInfo.host.swap_total || 0, swap_total: serverInfo.host.swap_total || 0,
disk_total: serverInfo.host.disk_total || 0, disk_total: serverInfo.host.disk_total || 0,
boot_time: serverInfo.host.boot_time || 0, boot_time: serverInfo.host.boot_time || 0,
boot_time_string: serverInfo.host.boot_time ? dayjs(serverInfo.host.boot_time * 1000).format("YYYY-MM-DD HH:mm:ss") : "", boot_time_string: serverInfo.host.boot_time
? dayjs(serverInfo.host.boot_time * 1000).format("YYYY-MM-DD HH:mm:ss")
: "",
platform_version: serverInfo.host.platform_version || "", platform_version: serverInfo.host.platform_version || "",
cpu_info: serverInfo.host.cpu || [], cpu_info: serverInfo.host.cpu || [],
gpu_info: serverInfo.host.gpu || [], gpu_info: serverInfo.host.gpu || [],
@@ -43,17 +49,22 @@ export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
load_5: serverInfo.state.load_5?.toFixed(2) || 0.0, load_5: serverInfo.state.load_5?.toFixed(2) || 0.0,
load_15: serverInfo.state.load_15?.toFixed(2) || 0.0, load_15: serverInfo.state.load_15?.toFixed(2) || 0.0,
public_note: handlePublicNote(serverInfo.id, serverInfo.public_note || ""), public_note: handlePublicNote(serverInfo.id, serverInfo.public_note || ""),
} };
} }
export function getDaysBetweenDatesWithAutoRenewal({ autoRenewal, cycle, startDate, endDate }: BillingData): { export function getDaysBetweenDatesWithAutoRenewal({
days: number autoRenewal,
cycleLabel: string cycle,
remainingPercentage: number startDate,
endDate,
}: BillingData): {
days: number;
cycleLabel: string;
remainingPercentage: number;
} { } {
let months = 1 let months = 1;
// 套餐资费 // 套餐资费
let cycleLabel = cycle let cycleLabel = cycle;
switch (cycle.toLowerCase()) { switch (cycle.toLowerCase()) {
case "月": case "月":
@@ -61,49 +72,52 @@ export function getDaysBetweenDatesWithAutoRenewal({ autoRenewal, cycle, startDa
case "mo": case "mo":
case "month": case "month":
case "monthly": case "monthly":
cycleLabel = "月" cycleLabel = "月";
months = 1 months = 1;
break break;
case "年": case "年":
case "y": case "y":
case "yr": case "yr":
case "year": case "year":
case "annual": case "annual":
cycleLabel = "年" cycleLabel = "年";
months = 12 months = 12;
break break;
case "季": case "季":
case "q": case "q":
case "qr": case "qr":
case "quarterly": case "quarterly":
cycleLabel = "季" cycleLabel = "季";
months = 3 months = 3;
break break;
case "半": case "半":
case "半年": case "半年":
case "h": case "h":
case "half": case "half":
case "semi-annually": case "semi-annually":
cycleLabel = "半年" cycleLabel = "半年";
months = 6 months = 6;
break break;
default: default:
cycleLabel = cycle cycleLabel = cycle;
break break;
} }
const nowTime = new Date().getTime() const nowTime = Date.now();
const endTime = dayjs(endDate).valueOf() const endTime = dayjs(endDate).valueOf();
if (autoRenewal !== "1") { if (autoRenewal !== "1") {
return { return {
days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()), days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()),
cycleLabel: cycleLabel, cycleLabel: cycleLabel,
remainingPercentage: remainingPercentage:
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / dayjs(endDate).diff(startDate, "day") > 1 getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
dayjs(endDate).diff(startDate, "day") >
1
? 1 ? 1
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / dayjs(endDate).diff(startDate, "day"), : getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
} dayjs(endDate).diff(startDate, "day"),
};
} }
if (nowTime < endTime) { if (nowTime < endTime) {
@@ -111,149 +125,157 @@ export function getDaysBetweenDatesWithAutoRenewal({ autoRenewal, cycle, startDa
days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()), days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()),
cycleLabel: cycleLabel, cycleLabel: cycleLabel,
remainingPercentage: remainingPercentage:
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / (30 * months) > 1 getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
(30 * months) >
1
? 1 ? 1
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / (30 * months), : getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
} (30 * months),
};
} }
const nextTime = getNextCycleTime(endTime, months, nowTime) const nextTime = getNextCycleTime(endTime, months, nowTime);
const diff = dayjs(nextTime).diff(dayjs(), "day") + 1 const diff = dayjs(nextTime).diff(dayjs(), "day") + 1;
const remainingPercentage = diff / (30 * months) > 1 ? 1 : diff / (30 * months) const remainingPercentage =
diff / (30 * months) > 1 ? 1 : diff / (30 * months);
return { return {
days: diff, days: diff,
cycleLabel: cycleLabel, cycleLabel: cycleLabel,
remainingPercentage: remainingPercentage, remainingPercentage: remainingPercentage,
} };
} }
// Thanks to hi2shark for the code // Thanks to hi2shark for the code
// https://github.com/hi2shark/nazhua/blob/main/src/utils/date.js#L86 // https://github.com/hi2shark/nazhua/blob/main/src/utils/date.js#L86
export function getNextCycleTime(startDate: number, months: number, specifiedDate: number): number { export function getNextCycleTime(
const start = dayjs(startDate) startDate: number,
const checkDate = dayjs(specifiedDate) months: number,
specifiedDate: number,
): number {
const start = dayjs(startDate);
const checkDate = dayjs(specifiedDate);
if (!start.isValid() || months <= 0) { if (!start.isValid() || months <= 0) {
throw new Error("参数无效:请检查起始日期、周期月份数和指定日期。") throw new Error("参数无效:请检查起始日期、周期月份数和指定日期。");
} }
let nextDate = start let nextDate = start;
// 循环增加周期直到大于当前日期 // 循环增加周期直到大于当前日期
let whileStatus = true let whileStatus = true;
while (whileStatus) { while (whileStatus) {
nextDate = nextDate.add(months, "month") nextDate = nextDate.add(months, "month");
whileStatus = nextDate.valueOf() <= checkDate.valueOf() whileStatus = nextDate.valueOf() <= checkDate.valueOf();
} }
return nextDate.valueOf() // 返回时间毫秒数 return nextDate.valueOf(); // 返回时间毫秒数
} }
export function getDaysBetweenDates(date1: string, date2: string): number { export function getDaysBetweenDates(date1: string, date2: string): number {
const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数 const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数
const firstDate = new Date(date1) const firstDate = new Date(date1);
const secondDate = new Date(date2) const secondDate = new Date(date2);
// 计算两个日期之间的天数差异 // 计算两个日期之间的天数差异
return Math.round((firstDate.getTime() - secondDate.getTime()) / oneDay) return Math.round((firstDate.getTime() - secondDate.getTime()) / oneDay);
} }
export const fetcher = (url: string) => export const fetcher = (url: string) =>
fetch(url) fetch(url)
.then((res) => { .then((res) => {
if (!res.ok) { if (!res.ok) {
throw new Error(res.statusText) throw new Error(res.statusText);
} }
return res.json() return res.json();
}) })
.then((data) => data.data) .then((data) => data.data)
.catch((err) => { .catch((err) => {
console.error(err) console.error(err);
throw err throw err;
}) });
export const nezhaFetcher = async (url: string) => { export const nezhaFetcher = async (url: string) => {
const res = await fetch(url) const res = await fetch(url);
if (!res.ok) { if (!res.ok) {
const error = new Error("An error occurred while fetching the data.") const error = new Error("An error occurred while fetching the data.");
// @ts-expect-error - res.json() returns a Promise<any> // @ts-expect-error - res.json() returns a Promise<any>
error.info = await res.json() error.info = await res.json();
// @ts-expect-error - res.status is a number // @ts-expect-error - res.status is a number
error.status = res.status error.status = res.status;
throw error throw error;
} }
return res.json() return res.json();
} };
export function parseISOTimestamp(isoString: string): number { export function parseISOTimestamp(isoString: string): number {
return new Date(isoString).getTime() return new Date(isoString).getTime();
} }
export function formatRelativeTime(timestamp: number): string { export function formatRelativeTime(timestamp: number): string {
const now = Date.now() const now = Date.now();
const diff = now - timestamp const diff = now - timestamp;
const hours = Math.floor(diff / (1000 * 60 * 60)) const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000) const seconds = Math.floor((diff % (1000 * 60)) / 1000);
if (hours > 24) { if (hours > 24) {
const days = Math.floor(hours / 24) const days = Math.floor(hours / 24);
return `${days}d` return `${days}d`;
} else if (hours > 0) { } else if (hours > 0) {
return `${hours}h` return `${hours}h`;
} else if (minutes > 0) { } else if (minutes > 0) {
return `${minutes}m` return `${minutes}m`;
} else if (seconds >= 0) { } else if (seconds >= 0) {
return `${seconds}s` return `${seconds}s`;
} }
return "0s" return "0s";
} }
export function formatTime(timestamp: number): string { export function formatTime(timestamp: number): string {
const date = new Date(timestamp) const date = new Date(timestamp);
const year = date.getFullYear() const year = date.getFullYear();
const month = date.getMonth() + 1 const month = date.getMonth() + 1;
const day = date.getDate() const day = date.getDate();
const hours = date.getHours().toString().padStart(2, "0") const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0") const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0") const seconds = date.getSeconds().toString().padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} }
interface BillingData { interface BillingData {
startDate: string startDate: string;
endDate: string endDate: string;
autoRenewal: string autoRenewal: string;
cycle: string cycle: string;
amount: string amount: string;
} }
interface PlanData { interface PlanData {
bandwidth: string bandwidth: string;
trafficVol: string trafficVol: string;
trafficType: string trafficType: string;
IPv4: string IPv4: string;
IPv6: string IPv6: string;
networkRoute: string networkRoute: string;
extra: string extra: string;
} }
export interface PublicNoteData { export interface PublicNoteData {
billingDataMod?: BillingData billingDataMod?: BillingData;
planDataMod?: PlanData planDataMod?: PlanData;
} }
export function parsePublicNote(publicNote: string): PublicNoteData | null { export function parsePublicNote(publicNote: string): PublicNoteData | null {
try { try {
if (!publicNote) { if (!publicNote) {
return null return null;
} }
const data = JSON.parse(publicNote) const data = JSON.parse(publicNote);
if (!data.billingDataMod && !data.planDataMod) { if (!data.billingDataMod && !data.planDataMod) {
return null return null;
} }
if (data.billingDataMod && !data.planDataMod) { if (data.billingDataMod && !data.planDataMod) {
return { return {
@@ -264,7 +286,7 @@ export function parsePublicNote(publicNote: string): PublicNoteData | null {
cycle: data.billingDataMod.cycle || "", cycle: data.billingDataMod.cycle || "",
amount: data.billingDataMod.amount || "", amount: data.billingDataMod.amount || "",
}, },
} };
} }
if (!data.billingDataMod && data.planDataMod) { if (!data.billingDataMod && data.planDataMod) {
return { return {
@@ -277,7 +299,7 @@ export function parsePublicNote(publicNote: string): PublicNoteData | null {
networkRoute: data.planDataMod.networkRoute || "", networkRoute: data.planDataMod.networkRoute || "",
extra: data.planDataMod.extra || "", extra: data.planDataMod.extra || "",
}, },
} };
} }
return { return {
@@ -297,26 +319,26 @@ export function parsePublicNote(publicNote: string): PublicNoteData | null {
networkRoute: data.planDataMod.networkRoute || "", networkRoute: data.planDataMod.networkRoute || "",
extra: data.planDataMod.extra || "", extra: data.planDataMod.extra || "",
}, },
} };
} catch (error) { } catch (error) {
console.error("Error parsing public note:", error) console.error("Error parsing public note:", error);
return null return null;
} }
} }
// Function to handle public_note with sessionStorage // Function to handle public_note with sessionStorage
export function handlePublicNote(serverId: number, publicNote: string): string { export function handlePublicNote(serverId: number, publicNote: string): string {
const storageKey = `server_${serverId}_public_note` const storageKey = `server_${serverId}_public_note`;
const storedNote = sessionStorage.getItem(storageKey) const storedNote = sessionStorage.getItem(storageKey);
if (!publicNote && storedNote) { if (!publicNote && storedNote) {
return storedNote return storedNote;
} }
if (publicNote) { if (publicNote) {
sessionStorage.setItem(storageKey, publicNote) sessionStorage.setItem(storageKey, publicNote);
return publicNote return publicNote;
} }
return "" return "";
} }
+7 -1
View File
@@ -45,7 +45,13 @@
"monitor": { "monitor": {
"monitorCount": "Services", "monitorCount": "Services",
"noData": "Kein Server Monitoring Daten, bitte fügen sie zuerst einen Monitor hinzu", "noData": "Kein Server Monitoring Daten, bitte fügen sie zuerst einen Monitor hinzu",
"avgDelay": "Latenz" "avgDelay": "Latenz",
"packetLoss": "Paketverlust",
"clearSelections": "Löschen",
"peakCut": "Peak cut",
"period1d": "1 Tag",
"period7d": "7 Tage",
"period30d": "30 Tage"
}, },
"billingInfo": { "billingInfo": {
"error": "Fehler", "error": "Fehler",
+15 -2
View File
@@ -77,7 +77,13 @@
"mem": "Mem", "mem": "Mem",
"swap": "Swap", "swap": "Swap",
"upload": "Upload", "upload": "Upload",
"download": "Download" "download": "Download",
"realtime": "Realtime",
"period1d": "1 Day",
"period7d": "7 Days",
"period30d": "30 Days",
"tsdbRequired": "Enable TSDB to use historical data",
"loginRequired": "Please login to view"
}, },
"footer": { "footer": {
"themeBy": "Theme by " "themeBy": "Theme by "
@@ -107,7 +113,14 @@
"monitor": { "monitor": {
"noData": "No server monitor data, please add a service monitor first", "noData": "No server monitor data, please add a service monitor first",
"avgDelay": "Latency", "avgDelay": "Latency",
"monitorCount": "Services" "monitorCount": "Services",
"packetLoss": "Packet Loss",
"clearSelections": "Clear",
"peakCut": "Peak cut",
"loginRequired": "Please login to view",
"period1d": "1 Day",
"period7d": "7 Days",
"period30d": "30 Days"
}, },
"pwa": { "pwa": {
"offlineReady": "App ready to work offline", "offlineReady": "App ready to work offline",

Some files were not shown because too many files have changed in this diff Show More