perf: use biome

This commit is contained in:
hamster1963
2025-12-28 18:05:02 +08:00
parent 3bfd4ef4d2
commit 29e349505d
115 changed files with 9924 additions and 8381 deletions
-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"]
}
+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"
}
}
}
}
+16 -270
View File
@@ -21,7 +21,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",
@@ -35,7 +34,6 @@
"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.7.2",
"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",
@@ -46,20 +44,16 @@
"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.1.18", "@tailwindcss/postcss": "^4.1.18",
"@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-swc": "3.8.0",
"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": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "~5.6.3", "typescript": "~5.6.3",
"typescript-eslint": "8.24.1",
"vite": "6.1.1", "vite": "6.1.1",
}, },
}, },
@@ -67,23 +61,25 @@
"packages": { "packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
"@babel/generator": ["@babel/generator@7.26.5", "", { "dependencies": { "@babel/parser": "^7.26.5", "@babel/types": "^7.26.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"@babel/parser": ["@babel/parser@7.26.7", "", { "dependencies": { "@babel/types": "^7.26.7" }, "bin": "./bin/babel-parser.js" }, "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w=="],
"@babel/runtime": ["@babel/runtime@7.26.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw=="], "@babel/runtime": ["@babel/runtime@7.26.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw=="],
"@babel/template": ["@babel/template@7.25.9", "", { "dependencies": { "@babel/code-frame": "^7.25.9", "@babel/parser": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg=="], "@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=="],
"@babel/traverse": ["@babel/traverse@7.26.7", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/template": "^7.25.9", "@babel/types": "^7.26.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="],
"@babel/types": ["@babel/types@7.26.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg=="], "@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.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="],
@@ -135,22 +131,6 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="], "@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.0", "", { "dependencies": { "eslint-visitor-keys": "^3.3.0" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="],
"@eslint/core": ["@eslint/core@0.11.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ=="],
"@eslint/js": ["@eslint/js@9.20.0", "", {}, "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="],
"@floating-ui/core": ["@floating-ui/core@1.6.8", "", { "dependencies": { "@floating-ui/utils": "^0.2.8" } }, "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA=="], "@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/dom": ["@floating-ui/dom@1.6.11", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.8" } }, "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ=="],
@@ -163,14 +143,6 @@
"@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="], "@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
"@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/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/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -183,12 +155,6 @@
"@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=="], "@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=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@radix-ui/number": ["@radix-ui/number@1.1.0", "", {}, "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="], "@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/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
@@ -373,8 +339,6 @@
"@tanstack/table-core": ["@tanstack/table-core@8.21.2", "", {}, "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA=="], "@tanstack/table-core": ["@tanstack/table-core@8.21.2", "", {}, "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA=="],
"@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@5.2.2", "", { "dependencies": { "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/traverse": "^7.26.7", "@babel/types": "^7.26.7", "javascript-natural-sort": "^0.7.1", "lodash": "^4.17.21" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-svelte", "svelte"] }, "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="], "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
@@ -401,8 +365,6 @@
"@types/geojson": ["@types/geojson@7946.0.14", "", {}, "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg=="], "@types/geojson": ["@types/geojson@7946.0.14", "", {}, "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/luxon": ["@types/luxon@3.4.2", "", {}, "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="], "@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/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="],
@@ -411,64 +373,20 @@
"@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="], "@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.24.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/type-utils": "8.24.1", "@typescript-eslint/utils": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.24.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1" } }, "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.24.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/utils": "8.24.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.24.1", "", {}, "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.24.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg=="],
"@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=="], "@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=="],
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="], "aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "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=="], "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=="], "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=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"country-flag-icons": ["country-flag-icons@1.5.18", "", {}, "sha512-z+Uzesi8u8IdkViqqbzzbkf3+a7WJpcET5B7sPwTg7GXqPYpVEgNlZ/FC3l8KO4mEf+mNkmzKLppKTN4PlCJEQ=="], "country-flag-icons": ["country-flag-icons@1.5.18", "", {}, "sha512-z+Uzesi8u8IdkViqqbzzbkf3+a7WJpcET5B7sPwTg7GXqPYpVEgNlZ/FC3l8KO4mEf+mNkmzKLppKTN4PlCJEQ=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
@@ -497,12 +415,8 @@
"dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="],
"debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
@@ -515,110 +429,32 @@
"esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="], "esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.20.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.1.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw=="],
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.19", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ=="],
"eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
"espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-equals": ["fast-equals@5.0.1", "", {}, "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ=="], "fast-equals": ["fast-equals@5.0.1", "", {}, "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ=="],
"fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="],
"framer-motion": ["framer-motion@12.4.5", "", { "dependencies": { "motion-dom": "^12.4.5", "motion-utils": "^12.0.0", "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-9+8wglyIJFeUpVg4U8Ohvoo5x7zmvRqawWXhEUThcYdwL/5A1/OkLvQo68Zz5taUE11HKG/Ex+LPaN2+fMkRdA=="], "framer-motion": ["framer-motion@12.4.5", "", { "dependencies": { "motion-dom": "^12.4.5", "motion-utils": "^12.0.0", "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-9+8wglyIJFeUpVg4U8Ohvoo5x7zmvRqawWXhEUThcYdwL/5A1/OkLvQo68Zz5taUE11HKG/Ex+LPaN2+fMkRdA=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], "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=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], "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=="], "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=="], "i18next": ["i18next@24.2.2", "", { "dependencies": { "@babel/runtime": "^7.23.2" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"javascript-natural-sort": ["javascript-natural-sort@0.7.1", "", {}, "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="],
"jiti": ["jiti@1.21.6", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w=="], "jiti": ["jiti@1.21.6", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"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": ["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-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
@@ -643,12 +479,8 @@
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"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=="], "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=="], "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=="],
@@ -657,54 +489,20 @@
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"motion-dom": ["motion-dom@12.5.0", "", { "dependencies": { "motion-utils": "^12.5.0" } }, "sha512-uH2PETDh7m+Hjd1UQQ56yHqwn83SAwNjimNPE/kC+Kds0t4Yh7+29rfo5wezVFpPOv57U4IuWved5d1x0kNhbQ=="], "motion-dom": ["motion-dom@12.5.0", "", { "dependencies": { "motion-utils": "^12.5.0" } }, "sha512-uH2PETDh7m+Hjd1UQQ56yHqwn83SAwNjimNPE/kC+Kds0t4Yh7+29rfo5wezVFpPOv57U4IuWved5d1x0kNhbQ=="],
"motion-utils": ["motion-utils@12.5.0", "", {}, "sha512-+hFFzvimn0sBMP9iPxBa9OtRX35ZQ3py0UHnb8U29VD+d8lQ8zH3dTygJWqK7av2v6yhg7scj9iZuvTS0f4+SA=="], "motion-utils": ["motion-utils@12.5.0", "", {}, "sha512-+hFFzvimn0sBMP9iPxBa9OtRX35ZQ3py0UHnb8U29VD+d8lQ8zH3dTygJWqK7av2v6yhg7scj9iZuvTS0f4+SA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"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=="], "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=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.2", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA=="],
"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=="], "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=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], "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-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
@@ -733,32 +531,16 @@
"regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
"rollup": ["rollup@4.34.6", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.6", "@rollup/rollup-android-arm64": "4.34.6", "@rollup/rollup-darwin-arm64": "4.34.6", "@rollup/rollup-darwin-x64": "4.34.6", "@rollup/rollup-freebsd-arm64": "4.34.6", "@rollup/rollup-freebsd-x64": "4.34.6", "@rollup/rollup-linux-arm-gnueabihf": "4.34.6", "@rollup/rollup-linux-arm-musleabihf": "4.34.6", "@rollup/rollup-linux-arm64-gnu": "4.34.6", "@rollup/rollup-linux-arm64-musl": "4.34.6", "@rollup/rollup-linux-loongarch64-gnu": "4.34.6", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.6", "@rollup/rollup-linux-riscv64-gnu": "4.34.6", "@rollup/rollup-linux-s390x-gnu": "4.34.6", "@rollup/rollup-linux-x64-gnu": "4.34.6", "@rollup/rollup-linux-x64-musl": "4.34.6", "@rollup/rollup-win32-arm64-msvc": "4.34.6", "@rollup/rollup-win32-ia32-msvc": "4.34.6", "@rollup/rollup-win32-x64-msvc": "4.34.6", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ=="], "rollup": ["rollup@4.34.6", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.6", "@rollup/rollup-android-arm64": "4.34.6", "@rollup/rollup-darwin-arm64": "4.34.6", "@rollup/rollup-darwin-x64": "4.34.6", "@rollup/rollup-freebsd-arm64": "4.34.6", "@rollup/rollup-freebsd-x64": "4.34.6", "@rollup/rollup-linux-arm-gnueabihf": "4.34.6", "@rollup/rollup-linux-arm-musleabihf": "4.34.6", "@rollup/rollup-linux-arm64-gnu": "4.34.6", "@rollup/rollup-linux-arm64-musl": "4.34.6", "@rollup/rollup-linux-loongarch64-gnu": "4.34.6", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.6", "@rollup/rollup-linux-riscv64-gnu": "4.34.6", "@rollup/rollup-linux-s390x-gnu": "4.34.6", "@rollup/rollup-linux-x64-gnu": "4.34.6", "@rollup/rollup-linux-x64-musl": "4.34.6", "@rollup/rollup-win32-arm64-msvc": "4.34.6", "@rollup/rollup-win32-ia32-msvc": "4.34.6", "@rollup/rollup-win32-x64-msvc": "4.34.6", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], "scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="],
"semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"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=="], "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=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="], "tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="],
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
@@ -769,24 +551,14 @@
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="],
"tslib": ["tslib@2.8.0", "", {}, "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="], "tslib": ["tslib@2.8.0", "", {}, "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="],
"turbo-stream": ["turbo-stream@2.4.0", "", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="], "turbo-stream": ["turbo-stream@2.4.0", "", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], "typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
"typescript-eslint": ["typescript-eslint@8.24.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/parser": "8.24.1", "@typescript-eslint/utils": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"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-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=="], "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=="],
@@ -797,28 +569,8 @@
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"yaml": ["yaml@2.6.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="], "yaml": ["yaml@2.6.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@babel/template/@babel/parser": ["@babel/parser@7.26.3", "", { "dependencies": { "@babel/types": "^7.26.3" }, "bin": "./bin/babel-parser.js" }, "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA=="],
"@babel/template/@babel/types": ["@babel/types@7.26.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA=="],
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], "@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=="], "@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
@@ -837,12 +589,6 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.0", "", { "bundled": true }, "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.0", "", { "bundled": true }, "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
} }
} }
-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
+6 -10
View File
@@ -6,9 +6,11 @@
"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": {
@@ -29,7 +31,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",
@@ -43,7 +44,6 @@
"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.7.2",
"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",
@@ -54,20 +54,16 @@
"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.1.18", "@tailwindcss/postcss": "^4.1.18",
"@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-swc": "3.8.0",
"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": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "~5.6.3", "typescript": "~5.6.3",
"typescript-eslint": "8.24.1",
"vite": "6.1.1" "vite": "6.1.1"
} }
} }
+1 -1
View File
@@ -2,4 +2,4 @@ export default {
plugins: { plugins: {
"@tailwindcss/postcss": {}, "@tailwindcss/postcss": {},
}, },
} };
+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);
+56 -44
View File
@@ -1,26 +1,27 @@
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";
// Route checker component // Route checker component
const RouteChecker: React.FC = () => { const RouteChecker: React.FC = () => {
return <MainApp /> return <MainApp />;
} };
const MainApp: React.FC = () => { const MainApp: React.FC = () => {
const { data: settingData, error } = useQuery({ const { data: settingData, error } = useQuery({
@@ -28,62 +29,73 @@ const MainApp: React.FC = () => {
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 (
<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})` }}
/> />
)} )}
@@ -106,8 +118,8 @@ const MainApp: React.FC = () => {
</main> </main>
</div> </div>
</ErrorBoundary> </ErrorBoundary>
) );
} };
// Main App wrapper with router // Main App wrapper with router
const App: React.FC = () => { const App: React.FC = () => {
@@ -115,7 +127,7 @@ const App: React.FC = () => {
<Router basename={import.meta.env.BASE_URL}> <Router basename={import.meta.env.BASE_URL}>
<RouteChecker /> <RouteChecker />
</Router> </Router>
) );
} };
export default App export default App;
+55 -25
View File
@@ -1,16 +1,24 @@
import { cn } from "@/lib/utils" import { useEffect, useState } from "react";
import { useEffect, useState } from "react" import { cn } from "@/lib/utils";
export function AnimateCountClient({ count, className, minDigits }: { count: number; className?: string; minDigits?: number }) { export function AnimateCountClient({
const [previousCount, setPreviousCount] = useState(count) count,
className,
minDigits,
}: {
count: number;
className?: string;
minDigits?: number;
}) {
const [previousCount, setPreviousCount] = useState(count);
useEffect(() => { useEffect(() => {
if (count !== previousCount) { if (count !== previousCount) {
setTimeout(() => { setTimeout(() => {
setPreviousCount(count) setPreviousCount(count);
}, 300) }, 300);
} }
}, [count]) }, [count, previousCount]);
return ( return (
<AnimateCount <AnimateCount
key={count} key={count}
@@ -21,10 +29,10 @@ export function AnimateCountClient({ count, className, minDigits }: { count: num
> >
{count} {count}
</AnimateCount> </AnimateCount>
) );
} }
export default AnimateCountClient export default AnimateCountClient;
export function AnimateCount({ export function AnimateCount({
children: count, children: count,
@@ -33,47 +41,69 @@ export function AnimateCount({
minDigits = 1, minDigits = 1,
...props ...props
}: { }: {
children: number children: number;
className?: string className?: string;
preCount?: number preCount?: number;
minDigits?: number minDigits?: number;
}) { }) {
const currentDigits = count.toString().split("") const currentDigits = count.toString().split("");
const previousDigits = (preCount !== undefined ? preCount.toString() : count - 1 >= 0 ? (count - 1).toString() : "0").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 // Ensure both numbers meet the minimum length requirement and maintain the same length for animation
const maxLength = Math.max(previousDigits.length, currentDigits.length, minDigits) const maxLength = Math.max(
previousDigits.length,
currentDigits.length,
minDigits,
);
while (previousDigits.length < maxLength) { while (previousDigits.length < maxLength) {
previousDigits.unshift("0") previousDigits.unshift("0");
} }
while (currentDigits.length < maxLength) { while (currentDigits.length < maxLength) {
currentDigits.unshift("0") currentDigits.unshift("0");
} }
return ( return (
<div {...props} className={cn("flex h-[1em] items-center", className)}> <div {...props} className={cn("flex h-[1em] items-center", className)}>
{currentDigits.map((digit, index) => { {currentDigits.map((digit, index) => {
const hasChanged = digit !== previousDigits[index] const hasChanged = digit !== previousDigits[index];
return ( return (
<div <div
key={`${index}-${digit}`} key={`${index}-${digit}`}
className={cn("relative flex h-full min-w-[0.6em] items-center text-center", { className={cn(
"relative flex h-full min-w-[0.6em] items-center text-center",
{
"min-w-[0.2em]": digit === ".", "min-w-[0.2em]": digit === ".",
})} },
)}
> >
<div <div
aria-hidden aria-hidden
data-issues-count-exit data-issues-count-exit
className={cn("absolute inset-0 flex items-center justify-center", hasChanged ? "animate" : "opacity-0")} className={cn(
"absolute inset-0 flex items-center justify-center",
hasChanged ? "animate" : "opacity-0",
)}
> >
{previousDigits[index]} {previousDigits[index]}
</div> </div>
<div data-issues-count-enter className={cn("absolute inset-0 flex items-center justify-center", hasChanged && "animate")}> <div
data-issues-count-enter
className={cn(
"absolute inset-0 flex items-center justify-center",
hasChanged && "animate",
)}
>
{digit} {digit}
</div> </div>
</div> </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;
+46 -29
View File
@@ -1,25 +1,30 @@
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(
@@ -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;
+46 -36
View File
@@ -1,40 +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 { useCommand } from "@/hooks/use-command" import { useEffect, useState } from "react";
import { useTheme } from "@/hooks/use-theme" import { useTranslation } from "react-i18next";
import { useWebSocketContext } from "@/hooks/use-websocket-context" import { useNavigate } from "react-router-dom";
import { formatNezhaInfo } from "@/lib/utils" import {
import { NezhaWebsocketResponse } from "@/types/nezha-api" CommandDialog,
import { Home, Moon, Sun, SunMoon } from "lucide-react" CommandEmpty,
import { useEffect, useState } from "react" CommandGroup,
import { useTranslation } from "react-i18next" CommandInput,
import { useNavigate } from "react-router-dom" 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 { isOpen, closeCommand, toggleCommand } = useCommand() 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();
toggleCommand() toggleCommand();
}
} }
};
document.addEventListener("keydown", down) document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down) return () => document.removeEventListener("keydown", down);
}, [toggleCommand]) }, [toggleCommand]);
if (!connected || !nezhaWsData) return null if (!connected || !nezhaWsData) return null;
const shortcuts = [ const shortcuts = [
{ {
@@ -64,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={isOpen} onOpenChange={closeCommand}>
<CommandInput placeholder={t("TypeCommand")} value={search} onValueChange={setSearch} /> <CommandInput
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}`);
closeCommand() closeCommand();
}} }}
> >
{formatNezhaInfo(nezhaWsData.now, server).online ? ( {formatNezhaInfo(nezhaWsData.now, server).online ? (
@@ -93,7 +105,6 @@ export function DashCommand() {
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
</>
)} )}
<CommandSeparator /> <CommandSeparator />
@@ -103,8 +114,8 @@ export function DashCommand() {
key={item.label} key={item.label}
value={item.value} value={item.value}
onSelect={() => { onSelect={() => {
item.action() item.action();
closeCommand() closeCommand();
}} }}
> >
{item.icon} {item.icon}
@@ -114,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;
+24 -13
View File
@@ -1,18 +1,18 @@
import { fetchSetting } from "@/lib/nezha-api" import { useQuery } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query" import type React from "react";
import React from "react" import { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next" import { fetchSetting } from "@/lib/nezha-api";
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">
@@ -20,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"> <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>
@@ -33,11 +37,18 @@ const Footer: React.FC = () => {
</p> </p>
<section> <section>
{t("footer.themeBy")} {t("footer.themeBy")}
<a href={"https://github.com/hamster1963/nezha-dash"} target="_blank"> <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"> <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>
)} )}
@@ -46,7 +57,7 @@ const Footer: React.FC = () => {
</section> </section>
</section> </section>
</footer> </footer>
) );
} };
export default Footer export default Footer;
+108 -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,63 +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, 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, 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"
> >
@@ -176,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>
) );
} }
+46 -36
View File
@@ -1,74 +1,82 @@
import { cn } from "@/lib/utils" import { m } from "framer-motion";
import { m } from "framer-motion" import { createRef, useEffect, useRef } from "react";
import { s } from "node_modules/framer-motion/dist/types.d-6pKw1mTI" import { cn } from "@/lib/utils";
import { createRef, useEffect, useRef } from "react"
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(() => {
if (tabs.length === 1 && tabs[0] === "All") { if (tabs.length === 1 && tabs[0] === "All") {
setCurrentTab("All") setCurrentTab("All");
return return;
} }
const savedGroup = sessionStorage.getItem("selectedGroup") 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]) }, [currentTab, tabs.indexOf]);
if (tabs.length === 1 && tabs[0] === "All") { if (tabs.length === 1 && tabs[0] === "All") {
return null return null;
} }
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
@@ -77,7 +85,9 @@ export default function GroupSwitch({
onClick={() => setCurrentTab(tab)} onClick={() => setCurrentTab(tab)}
className={cn( className={cn(
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold 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 && (
@@ -97,5 +107,5 @@ export default function GroupSwitch({
))} ))}
</div> </div>
</div> </div>
) );
} }
+134 -100
View File
@@ -1,28 +1,28 @@
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 { useQuery } from "@tanstack/react-query" import { ModeToggle } from "@/components/ThemeSwitcher";
import { AnimatePresence, m } from "framer-motion" import { Separator } from "@/components/ui/separator";
import { ImageMinus } from "lucide-react" import { Skeleton } from "@/components/ui/skeleton";
import { DateTime } from "luxon" import { useBackground } from "@/hooks/use-background";
import { useEffect, useRef, useState } from "react" import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { useTranslation } from "react-i18next" import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api";
import { useNavigate } from "react-router-dom" import { cn } from "@/lib/utils";
import AnimateCountClient from "./AnimatedCount" import AnimateCountClient from "./AnimatedCount";
import { LanguageSwitcher } from "./LanguageSwitcher" import { LanguageSwitcher } from "./LanguageSwitcher";
import { SearchButton } from "./SearchButton" import { Loader, LoadingSpinner } from "./loading/Loader";
import { Loader, LoadingSpinner } from "./loading/Loader" import { SearchButton } from "./SearchButton";
import { Button } from "./ui/button" import { Button } from "./ui/button";
interface TimeState { interface TimeState {
hh: number hh: number;
mm: number mm: number;
ss: number ss: number;
} }
const useCurrentTime = () => { const useCurrentTime = () => {
@@ -30,88 +30,100 @@ const useCurrentTime = () => {
hh: DateTime.now().setLocale("en-US").hour, hh: DateTime.now().setLocale("en-US").hour,
mm: DateTime.now().setLocale("en-US").minute, mm: DateTime.now().setLocale("en-US").minute,
ss: DateTime.now().setLocale("en-US").second, ss: DateTime.now().setLocale("en-US").second,
}) });
useEffect(() => { useEffect(() => {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
const now = DateTime.now().setLocale("en-US") const now = DateTime.now().setLocale("en-US");
setTime({ setTime({
hh: now.hour, hh: now.hour,
mm: now.minute, mm: now.minute,
ss: now.second, ss: now.second,
}) });
}, 1000) }, 1000);
return () => clearInterval(intervalId) return () => clearInterval(intervalId);
}, []) }, []);
return time 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"
> >
@@ -124,9 +136,18 @@ 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-px 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">
@@ -136,7 +157,8 @@ function Header() {
<SearchButton /> <SearchButton />
<LanguageSwitcher /> <LanguageSwitcher />
<ModeToggle /> <ModeToggle />
{(customBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) && ( {(customBackgroundImage ||
sessionStorage.getItem("savedBackgroundImage")) && (
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -152,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,
@@ -172,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">
@@ -201,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 (
@@ -239,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,
@@ -260,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">
@@ -293,17 +327,17 @@ function DashboardLink() {
{isLogin && t("dashboard")} {isLogin && t("dashboard")}
</a> </a>
</div> </div>
) );
} }
function Overview() { function Overview() {
const { t } = useTranslation() const { t } = useTranslation();
const time = useCurrentTime() const time = useCurrentTime();
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false);
useEffect(() => { useEffect(() => {
setMounted(true) setMounted(true);
}, []) }, []);
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"}>
@@ -325,6 +359,6 @@ function Overview() {
)} )}
</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"
/> />
</> </>
) );
} }
+26 -16
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>
@@ -54,15 +62,17 @@ export function LanguageSwitcher() {
}, },
{ {
"rounded-t-[5px]": index === localeItems.length - 1, "rounded-t-[5px]": index === localeItems.length - 1,
"rounded-[5px]": index !== 0 && index !== localeItems.length - 1, "rounded-[5px]":
index !== 0 && index !== localeItems.length - 1,
"rounded-b-[5px]": index === 0, "rounded-b-[5px]": index === 0,
}, },
)} )}
> >
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />} {item.name}{" "}
{locale === item.code && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) );
} }
+23 -17
View File
@@ -1,15 +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 { 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 navigate = useNavigate() const navigate = useNavigate();
const { tooltipData } = useTooltip() const { tooltipData } = useTooltip();
if (!tooltipData) return null if (!tooltipData) return null;
return ( return (
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
@@ -25,11 +25,15 @@ 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">
{tooltipData.country === "China"
? "Mainland China"
: tooltipData.country}
</p>
<p className="text-neutral-600 dark:text-neutral-400 text-xs font-light mb-1"> <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>
@@ -47,18 +51,20 @@ const MapTooltip = memo(function MapTooltip() {
type="button" 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" 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={() => { onClick={() => {
sessionStorage.setItem("fromMainPage", "true") sessionStorage.setItem("fromMainPage", "true");
navigate(`/server/${server.id}`) 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={`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>
</button> </button>
))} ))}
</div> </div>
</m.div> </m.div>
</AnimatePresence> </AnimatePresence>
) );
}) });
export default MapTooltip export default MapTooltip;
+284 -182
View File
@@ -1,84 +1,120 @@
"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 * as React from "react";
import { fetchMonitor } from "@/lib/nezha-api" import { useCallback, useMemo } from "react";
import { cn, formatTime } from "@/lib/utils" import { useTranslation } from "react-i18next";
import { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api" import {
import { useQuery } from "@tanstack/react-query" Area,
import * as React from "react" CartesianGrid,
import { useCallback, useMemo } from "react" ComposedChart,
import { useTranslation } from "react-i18next" Line,
import { Area, CartesianGrid, ComposedChart, Line, XAxis, YAxis } from "recharts" 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 { fetchMonitor } 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;
} }
/** /**
* Helper method to calculate packet loss from delay data * Helper method to calculate packet loss from delay data
*/ */
const calculatePacketLoss = (delays: number[]): number[] => { const calculatePacketLoss = (delays: number[]): number[] => {
if (!delays || delays.length === 0) return [] if (!delays || delays.length === 0) return [];
const packetLossRates: number[] = [] const packetLossRates: number[] = [];
const windowSize = Math.min(10, Math.max(3, Math.floor(delays.length / 10))) const windowSize = Math.min(10, Math.max(3, Math.floor(delays.length / 10)));
const timeoutThreshold = 3000 const timeoutThreshold = 3000;
const extremeDelayThreshold = 10000 const extremeDelayThreshold = 10000;
for (let i = 0; i < delays.length; i++) { for (let i = 0; i < delays.length; i++) {
const currentDelay = delays[i] const currentDelay = delays[i];
let lossRate = 0 let lossRate = 0;
if (currentDelay === 0 || currentDelay === null || currentDelay === undefined) { if (
lossRate = 100 currentDelay === 0 ||
currentDelay === null ||
currentDelay === undefined
) {
lossRate = 100;
} else if (currentDelay >= extremeDelayThreshold) { } else if (currentDelay >= extremeDelayThreshold) {
lossRate = Math.min(95, 60 + (currentDelay - extremeDelayThreshold) / 1000) lossRate = Math.min(
95,
60 + (currentDelay - extremeDelayThreshold) / 1000,
);
} else if (currentDelay >= timeoutThreshold) { } else if (currentDelay >= timeoutThreshold) {
lossRate = Math.min(50, (currentDelay - timeoutThreshold) / 200) lossRate = Math.min(50, (currentDelay - timeoutThreshold) / 200);
} else { } else {
const start = Math.max(0, i - Math.floor(windowSize / 2)) const start = Math.max(0, i - Math.floor(windowSize / 2));
const end = Math.min(delays.length, i + Math.ceil(windowSize / 2)) const end = Math.min(delays.length, i + Math.ceil(windowSize / 2));
const windowDelays = delays.slice(start, end).filter((d) => d > 0) const windowDelays = delays.slice(start, end).filter((d) => d > 0);
if (windowDelays.length > 2) { if (windowDelays.length > 2) {
const mean = windowDelays.reduce((sum, d) => sum + d, 0) / windowDelays.length const mean =
const variance = windowDelays.reduce((sum, d) => sum + (d - mean) ** 2, 0) / windowDelays.length windowDelays.reduce((sum, d) => sum + d, 0) / windowDelays.length;
const standardDeviation = Math.sqrt(variance) const variance =
const coefficientOfVariation = standardDeviation / mean windowDelays.reduce((sum, d) => sum + (d - mean) ** 2, 0) /
windowDelays.length;
const standardDeviation = Math.sqrt(variance);
const coefficientOfVariation = standardDeviation / mean;
if (coefficientOfVariation > 0.8) { if (coefficientOfVariation > 0.8) {
lossRate = Math.min(25, coefficientOfVariation * 15) lossRate = Math.min(25, coefficientOfVariation * 15);
} else if (coefficientOfVariation > 0.5) { } else if (coefficientOfVariation > 0.5) {
lossRate = Math.min(10, coefficientOfVariation * 8) lossRate = Math.min(10, coefficientOfVariation * 8);
} else if (coefficientOfVariation > 0.3) { } else if (coefficientOfVariation > 0.3) {
lossRate = Math.min(5, coefficientOfVariation * 5) lossRate = Math.min(5, coefficientOfVariation * 5);
} }
if (currentDelay > mean * 2.5) { if (currentDelay > mean * 2.5) {
lossRate += Math.min(15, (currentDelay / mean - 2.5) * 10) lossRate += Math.min(15, (currentDelay / mean - 2.5) * 10);
} }
} }
} }
if (i > 0) { if (i > 0) {
const alpha = 0.3 const alpha = 0.3;
lossRate = alpha * lossRate + (1 - alpha) * packetLossRates[i - 1] lossRate = alpha * lossRate + (1 - alpha) * packetLossRates[i - 1];
} }
packetLossRates.push(Math.max(0, Math.min(100, lossRate))) packetLossRates.push(Math.max(0, Math.min(100, lossRate)));
} }
return packetLossRates.map((rate) => Number(rate.toFixed(2))) return packetLossRates.map((rate) => Number(rate.toFixed(2)));
} };
export function NetworkChart({ server_id, show }: { server_id: number; show: boolean }) { export function NetworkChart({
const { t } = useTranslation() server_id,
show,
}: {
server_id: number;
show: boolean;
}) {
const { t } = useTranslation();
const { data: monitorData } = useQuery({ const { data: monitorData } = useQuery({
queryKey: ["monitor", server_id], queryKey: ["monitor", server_id],
@@ -87,27 +123,29 @@ export function NetworkChart({ server_id, show }: { server_id: number; show: boo
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 chartDataKey = Object.keys(transformedData);
const initChartConfig = { const initChartConfig = {
avg_delay: { avg_delay: {
@@ -116,10 +154,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
@@ -129,7 +167,7 @@ export function NetworkChart({ server_id, show }: { server_id: number; show: boo
serverName={monitorData.data[0].server_name} serverName={monitorData.data[0].server_name}
formattedData={formattedData} formattedData={formattedData}
/> />
) );
} }
export const NetworkChartClient = React.memo(function NetworkChart({ export const NetworkChartClient = React.memo(function NetworkChart({
@@ -139,56 +177,68 @@ export const NetworkChartClient = React.memo(function NetworkChart({
serverName, serverName,
formattedData, formattedData,
}: { }: {
chartDataKey: string[] chartDataKey: string[];
chartConfig: ChartConfig chartConfig: ChartConfig;
chartData: ServerMonitorChart chartData: ServerMonitorChart;
serverName: string serverName: string;
formattedData: ResultItem[] formattedData: ResultItem[];
}) { }) {
const { t } = useTranslation() const { t } = useTranslation();
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const forcePeakCutEnabled = (window.ForcePeakCutEnabled as boolean) ?? false 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 chartButtons = useMemo( const chartButtons = useMemo(
() => () =>
chartDataKey.map((key) => { chartDataKey.map((key) => {
const monitorData = chartData[key] const monitorData = chartData[key];
const lastDelay = monitorData[monitorData.length - 1].avg_delay const lastDelay = monitorData[monitorData.length - 1].avg_delay;
// Calculate average packet loss if available // Calculate average packet loss if available
const packetLossData = monitorData.filter((item) => item.packet_loss !== undefined).map((item) => item.packet_loss!) const packetLossData = monitorData.reduce<number[]>((acc, item) => {
const avgPacketLoss = packetLossData.length > 0 ? packetLossData.reduce((sum, loss) => sum + loss, 0) / packetLossData.length : null 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 ( return (
<button <button
@@ -197,23 +247,31 @@ export const NetworkChartClient = React.memo(function NetworkChart({
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">
{key}
</span>
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<span className="text-md font-bold leading-none sm:text-lg">{lastDelay.toFixed(2)}ms</span> <span className="text-md font-bold leading-none sm:text-lg">
{avgPacketLoss !== null && <span className="text-xs text-muted-foreground">{avgPacketLoss.toFixed(2)}% avg loss</span>} {lastDelay.toFixed(2)}ms
</span>
{avgPacketLoss !== null && (
<span className="text-xs text-muted-foreground">
{avgPacketLoss.toFixed(2)}% avg loss
</span>
)}
</div> </div>
</button> </button>
) );
}), }),
[chartDataKey, activeCharts, chartData, handleButtonClick], [chartDataKey, activeCharts, chartData, handleButtonClick],
) );
const chartElements = useMemo(() => { const chartElements = useMemo(() => {
const elements = [] const elements = [];
// If exactly one chart is selected, show delay line and packet loss area // If exactly one chart is selected, show delay line and packet loss area
if (activeCharts.length === 1) { if (activeCharts.length === 1) {
const chart = activeCharts[0] const chart = activeCharts[0];
elements.push( elements.push(
<Area <Area
key="packet-loss-area" key="packet-loss-area"
@@ -235,7 +293,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
yAxisId="delay" yAxisId="delay"
connectNulls={true} connectNulls={true}
/>, />,
) );
} else if (activeCharts.length > 1) { } else if (activeCharts.length > 1) {
// Multiple charts selected - show only delay lines for selected monitors // Multiple charts selected - show only delay lines for selected monitors
elements.push( elements.push(
@@ -253,7 +311,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
yAxisId="delay" yAxisId="delay"
/> />
)), )),
) );
} else { } else {
// No selection - show all charts (default view) // No selection - show all charts (default view)
elements.push( elements.push(
@@ -270,117 +328,126 @@ export const NetworkChartClient = React.memo(function NetworkChart({
yAxisId="delay" yAxisId="delay"
/> />
)), )),
) );
} }
return elements return elements;
}, [activeCharts, chartDataKey, getColorByIndex]) }, [activeCharts, chartDataKey, getColorByIndex]);
const processedData = useMemo(() => { const processedData = useMemo(() => {
// Special handling for single chart selection // Special handling for single chart selection
let baseData = formattedData let baseData = formattedData;
if (activeCharts.length === 1) { if (activeCharts.length === 1) {
const selectedChart = activeCharts[0] const selectedChart = activeCharts[0];
baseData = chartData[selectedChart].map((item) => ({ baseData = chartData[selectedChart].map((item) => ({
created_at: item.created_at, created_at: item.created_at,
avg_delay: item.avg_delay, avg_delay: item.avg_delay,
packet_loss: item.packet_loss ?? 0, packet_loss: item.packet_loss ?? 0,
})) }));
} }
if (!isPeakEnabled) { if (!isPeakEnabled) {
return baseData return baseData;
} }
// For peak cutting, use the base data // For peak cutting, use the base data
const data = baseData const data = baseData;
const windowSize = 11 // 增加窗口大小以获取更好的统计效果 const windowSize = 11; // 增加窗口大小以获取更好的统计效果
const alpha = 0.3 // EWMA平滑因子 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;
// Special handling for single chart selection // Special handling for single chart selection
if (activeCharts.length === 1) { if (activeCharts.length === 1) {
// Process avg_delay for single chart // Process avg_delay for single chart
const values = window.map((w) => w.avg_delay as number).filter((v) => v !== undefined && v !== null) const values = window
.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 (processed !== null) {
if (ewmaHistory.avg_delay === undefined) { if (ewmaHistory.avg_delay === undefined) {
ewmaHistory.avg_delay = processed ewmaHistory.avg_delay = processed;
} else { } else {
ewmaHistory.avg_delay = alpha * processed + (1 - alpha) * ewmaHistory.avg_delay ewmaHistory.avg_delay =
alpha * processed + (1 - alpha) * ewmaHistory.avg_delay;
} }
smoothed.avg_delay = ewmaHistory.avg_delay smoothed.avg_delay = ewmaHistory.avg_delay;
} }
} }
} else { } else {
// Process all chart keys or just the selected ones // Process all chart keys or just the selected ones
const keysToProcess = activeCharts.length > 0 ? activeCharts : chartDataKey const keysToProcess =
activeCharts.length > 0 ? activeCharts : chartDataKey;
keysToProcess.forEach((key) => { keysToProcess.forEach((key) => {
const values = window.map((w) => w[key]).filter((v) => v !== undefined && v !== null) as number[] const values = window
.map((w) => w[key])
.filter((v) => v !== undefined && v !== null) as number[];
if (values.length > 0) { if (values.length > 0) {
const processed = processValues(values) 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, chartData, chartDataKey]) }, [isPeakEnabled, activeCharts, formattedData, chartData, chartDataKey]);
return ( return (
<Card <Card
@@ -390,12 +457,18 @@ export const NetworkChartClient = React.memo(function NetworkChart({
> >
<CardHeader className="flex flex-col items-stretch space-y-0 overflow-hidden rounded-t-lg 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"> <div className="flex items-center mt-0.5 space-x-2">
<Switch id="Peak" checked={isPeakEnabled} onCheckedChange={setIsPeakEnabled} /> <Switch
id="Peak"
checked={isPeakEnabled}
onCheckedChange={setIsPeakEnabled}
/>
<Label className="text-xs" htmlFor="Peak"> <Label className="text-xs" htmlFor="Peak">
Peak cut Peak cut
</Label> </Label>
@@ -413,8 +486,15 @@ export const NetworkChartClient = React.memo(function NetworkChart({
{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
<ComposedChart 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"
@@ -426,30 +506,44 @@ 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 yAxisId="delay" 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 && ( {activeCharts.length === 1 && (
<YAxis <YAxis
yAxisId="packet-loss" yAxisId="packet-loss"
@@ -468,100 +562,108 @@ 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) => { formatter={(value, name) => {
let formattedValue: string let formattedValue: string;
let label: string let label: string;
if (name === "packet_loss") { if (name === "packet_loss") {
formattedValue = `${Number(value).toFixed(2)}%` formattedValue = `${Number(value).toFixed(2)}%`;
label = t("monitor.packetLoss", "Packet Loss") label = t("monitor.packetLoss", "Packet Loss");
} else if (name === "avg_delay") { } else if (name === "avg_delay") {
formattedValue = `${Number(value).toFixed(2)}ms` formattedValue = `${Number(value).toFixed(2)}ms`;
label = t("monitor.avgDelay", "Avg Delay") label = t("monitor.avgDelay", "Avg Delay");
} else { } else {
// For monitor names (in multi-chart view) - delay data // For monitor names (in multi-chart view) - delay data
formattedValue = `${Number(value).toFixed(2)}ms` formattedValue = `${Number(value).toFixed(2)}ms`;
label = name as string label = name as string;
} }
return ( return (
<div className="flex flex-1 items-center justify-between leading-none"> <div className="flex flex-1 items-center justify-between leading-none">
<span className="text-muted-foreground">{label}</span> <span className="text-muted-foreground">{label}</span>
<span className="ml-2 font-medium text-foreground tabular-nums">{formattedValue}</span> <span className="ml-2 font-medium text-foreground tabular-nums">
{formattedValue}
</span>
</div> </div>
) );
}} }}
/> />
} }
/> />
{activeCharts.length !== 1 && <ChartLegend content={<ChartLegendContent />} />} {activeCharts.length !== 1 && (
<ChartLegend content={<ChartLegendContent />} />
)}
{chartElements} {chartElements}
</ComposedChart> </ComposedChart>
</ChartContainer> </ChartContainer>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
) );
}) });
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 // Calculate packet loss from delay data if not provided
const packetLoss = item.packet_loss || calculatePacketLoss(item.avg_delay) 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], 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 // Calculate packet loss if not provided
const packetLoss = item.packet_loss || calculatePacketLoss(avg_delay) 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 // Add packet loss data if available
if (packetLoss) { if (packetLoss) {
// @ts-expect-error - packet_loss is calculated // @ts-expect-error - packet_loss is calculated
result[time][`${monitor_name}_packet_loss`] = timeIndex !== -1 ? packetLoss[timeIndex] : null 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>
) );
} }
+46 -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,62 @@ 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];
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]",
)}
>
{parsedData.planDataMod.networkRoute
.split(",")
.map((route, index) => {
return (
route +
(index ===
parsedData.planDataMod?.networkRoute.split(",").length - 1
? ""
: "")
);
})} })}
</p> </p>
)} )}
@@ -47,12 +77,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)}
/> />
) );
} }
+11 -8
View File
@@ -1,15 +1,18 @@
"use client" "use client";
import { useCommand } from "@/hooks/use-command" import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
import { cn } from "@/lib/utils" import { useCommand } from "@/hooks/use-command";
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid" import { cn } from "@/lib/utils";
import { Button } from "./ui/button" import { Button } from "./ui/button";
export function SearchButton() { export function SearchButton() {
const { openCommand } = useCommand() const { openCommand } = useCommand();
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
return ( return (
<Button <Button
@@ -24,5 +27,5 @@ export function SearchButton() {
<MagnifyingGlassIcon className="size-4" /> <MagnifyingGlassIcon className="size-4" />
<span className="sr-only">Search</span> <span className="sr-only">Search</span>
</Button> </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-px" 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>
) );
} }
+33 -18
View File
@@ -1,30 +1,39 @@
"use client" "use client";
import { Progress } from "@/components/ui/progress" import { Progress } from "@/components/ui/progress";
import { useWebSocketContext } from "@/hooks/use-websocket-context" import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { formatNezhaInfo } from "@/lib/utils" import { formatNezhaInfo } from "@/lib/utils";
import { NezhaWebsocketResponse } from "@/types/nezha-api" import type { NezhaWebsocketResponse } from "@/types/nezha-api";
export default function ServerDetailSummary({ server_id }: { server_id: number }) { export default function ServerDetailSummary({
const { lastMessage, connected } = useWebSocketContext() server_id,
}: {
server_id: number;
}) {
const { lastMessage, connected } = useWebSocketContext();
if (!connected && !lastMessage) { if (!connected && !lastMessage) {
return null return null;
} }
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 null return null;
} }
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 null return null;
} }
const { cpu, mem, disk, up, down, tcp, udp, process } = formatNezhaInfo(nezhaWsData.now, server) const { cpu, mem, disk, up, down, tcp, udp, process } = formatNezhaInfo(
nezhaWsData.now,
server,
);
return ( return (
<div className="mb-2 flex flex-wrap items-center gap-4"> <div className="mb-2 flex flex-wrap items-center gap-4">
@@ -76,12 +85,12 @@ export default function ServerDetailSummary({ server_id }: { server_id: number }
</section> </section>
</section> </section>
</div> </div>
) );
} }
type UsageBarProps = { type UsageBarProps = {
value: number value: number;
} };
function UsageBar({ value }: UsageBarProps) { function UsageBar({ value }: UsageBarProps) {
return ( return (
@@ -89,8 +98,14 @@ function UsageBar({ value }: UsageBarProps) {
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 bg-stone-200 dark:bg-stone-800"} 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>
) );
} }
+61 -34
View File
@@ -1,38 +1,51 @@
import { Card, CardContent } from "@/components/ui/card" import {
import { useStatus } from "@/hooks/use-status" ArrowDownCircleIcon,
import { formatBytes } from "@/lib/format" ArrowUpCircleIcon,
import { cn } from "@/lib/utils" } from "@heroicons/react/20/solid";
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid" import { useTranslation } from "react-i18next";
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";
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 downSpeed: number;
} };
export default function ServerOverview({ online, offline, total, up, down, upSpeed, downSpeed }: ServerOverviewProps) { export default function ServerOverview({
const { t } = useTranslation() online,
const { status, setStatus } = useStatus() offline,
total,
up,
down,
upSpeed,
downSpeed,
}: ServerOverviewProps) {
const { t } = useTranslation();
const { status, setStatus } = useStatus();
// @ts-expect-error DisableAnimatedMan is a global variable // @ts-expect-error DisableAnimatedMan is a global variable
const disableAnimatedMan = window.DisableAnimatedMan as boolean const disableAnimatedMan = window.DisableAnimatedMan as boolean;
// @ts-expect-error CustomIllustration is a global variable // @ts-expect-error CustomIllustration is a global variable
const customIllustration = window.CustomIllustration || "/animated-man.webp" const customIllustration = window.CustomIllustration || "/animated-man.webp";
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
return ( return (
<>
<section className="grid grid-cols-2 gap-4 lg:grid-cols-4 server-overview"> <section className="grid grid-cols-2 gap-4 lg:grid-cols-4 server-overview">
<Card <Card
onClick={() => { onClick={() => {
setStatus("all") setStatus("all");
}} }}
className={cn("hover:border-blue-500 cursor-pointer transition-all", { className={cn("hover:border-blue-500 cursor-pointer transition-all", {
"bg-card/70": customBackgroundImage, "bg-card/70": customBackgroundImage,
@@ -40,7 +53,9 @@ export default function ServerOverview({ online, offline, total, up, down, upSpe
> >
<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>
@@ -52,7 +67,7 @@ export default function ServerOverview({ online, offline, total, up, down, upSpe
</Card> </Card>
<Card <Card
onClick={() => { onClick={() => {
setStatus("online") setStatus("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",
@@ -66,7 +81,9 @@ export default function ServerOverview({ online, offline, total, up, down, upSpe
> >
<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>
@@ -80,7 +97,7 @@ export default function ServerOverview({ online, offline, total, up, down, upSpe
</Card> </Card>
<Card <Card
onClick={() => { onClick={() => {
setStatus("offline") setStatus("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",
@@ -94,7 +111,9 @@ export default function ServerOverview({ online, offline, total, up, down, upSpe
> >
<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>
@@ -106,18 +125,27 @@ export default function ServerOverview({ online, offline, total, up, down, upSpe
</CardContent> </CardContent>
</Card> </Card>
<Card <Card
className={cn("hover:ring-purple-500 ring-1 ring-transparent transition-all", { className={cn(
"hover:ring-purple-500 ring-1 ring-transparent transition-all",
{
"bg-card/70": customBackgroundImage, "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"> <div className="flex items-center w-full justify-between">
<p className="text-sm font-medium md:text-base">{t("serverOverview.network")}</p> <p className="text-sm font-medium md:text-base">
{t("serverOverview.network")}
</p>
</div> </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"> <p className="text-[11px] flex items-center text-nowrap font-semibold">
@@ -141,6 +169,5 @@ export default function ServerOverview({ online, offline, total, up, down, upSpe
</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;
+90 -42
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>
@@ -82,18 +113,35 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, clas
</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;
+27 -11
View File
@@ -1,16 +1,30 @@
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
@@ -18,7 +32,9 @@ export default function TabSwitch({ tabs, currentTab, setCurrentTab }: { tabs: s
onClick={() => setCurrentTab(tab)} onClick={() => setCurrentTab(tab)}
className={cn( className={cn(
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold 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;
} }
+53 -36
View File
@@ -1,64 +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.add("disable-transitions") root.classList.add("disable-transitions");
root.classList.remove("light", "dark") 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%)";
document
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", themeColor);
const timeoutId = window.setTimeout(() => { const timeoutId = window.setTimeout(() => {
root.classList.remove("disable-transitions") root.classList.remove("disable-transitions");
}, 0) }, 0);
return () => window.clearTimeout(timeoutId) 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
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", themeColor);
const timeoutId = window.setTimeout(() => { const timeoutId = window.setTimeout(() => {
root.classList.remove("disable-transitions") root.classList.remove("disable-transitions");
}, 0) }, 0);
return () => window.clearTimeout(timeoutId) return () => window.clearTimeout(timeoutId);
}, [theme]) }, [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 }
+32 -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>
@@ -36,21 +44,27 @@ export function ModeToggle() {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="flex flex-col gap-0.5" align="end"> <DropdownMenuContent className="flex flex-col gap-0.5" align="end">
<DropdownMenuItem <DropdownMenuItem
className={cn("rounded-b-[5px] text-xs", { "gap-3 bg-muted font-semibold": theme === "light" })} className={cn("rounded-b-[5px] text-xs", {
"gap-3 bg-muted font-semibold": theme === "light",
})}
onSelect={(e) => handleSelect(e, "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 <DropdownMenuItem
className={cn("rounded-[5px] text-xs", { "gap-3 bg-muted font-semibold": theme === "dark" })} className={cn("rounded-[5px] text-xs", {
"gap-3 bg-muted font-semibold": theme === "dark",
})}
onSelect={(e) => handleSelect(e, "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 <DropdownMenuItem
className={cn("rounded-t-[5px] text-xs", { "gap-3 bg-muted font-semibold": theme === "system" })} className={cn("rounded-t-[5px] text-xs", {
"gap-3 bg-muted font-semibold": theme === "system",
})}
onSelect={(e) => handleSelect(e, "system")} onSelect={(e) => handleSelect(e, "system")}
> >
{t("theme.system")} {t("theme.system")}
@@ -58,5 +72,5 @@ export function ModeToggle() {
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) );
} }
+57 -24
View File
@@ -1,68 +1,101 @@
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,
} };
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}
<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} />} {!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>
</> </>
) );
} }
+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
} }
/> />
@@ -69,11 +83,15 @@ export default function AnimatedCircularProgressBar({ max = 100, min = 0, value
{ {
"--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>
) );
} }
+17 -10
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-hidden 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 };
+29 -16
View File
@@ -1,7 +1,7 @@
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-hidden 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",
@@ -9,9 +9,12 @@ const buttonVariants = cva(
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,
};
+168 -83
View File
@@ -1,42 +1,47 @@
import { cn } from "@/lib/utils" import * as React from "react";
import * as React from "react" import * as RechartsPrimitive from "recharts";
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 in 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 }}>
@@ -50,18 +55,22 @@ const ChartContainer = React.forwardRef<
{...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 +82,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 +94,20 @@ ${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<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
hideLabel?: boolean hideLabel?: boolean;
hideIndicator?: boolean hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed" indicator?: "line" | "dot" | "dashed";
nameKey?: string nameKey?: string;
labelKey?: string labelKey?: string;
} }
>( >(
( (
@@ -117,34 +128,49 @@ 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" const nestLabel = payload.length === 1 && indicator !== "dot";
return ( return (
<div <div
@@ -157,9 +183,9 @@ const ChartTooltipContent = React.forwardRef<
{!nestLabel ? tooltipLabel : null} {!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5"> <div className="grid gap-1.5">
{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 +204,16 @@ const ChartTooltipContent = React.forwardRef<
) : ( ) : (
!hideIndicator && ( !hideIndicator && (
<div <div
className={cn("shrink-0 rounded-[2px] border-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 +223,76 @@ 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="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</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"> & { Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean hideIcon?: boolean;
nameKey?: string 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 />
) : ( ) : (
@@ -248,30 +305,58 @@ const ChartLegendContent = React.forwardRef<
)} )}
{itemConfig?.label} {itemConfig?.label}
</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,
};
+14 -11
View File
@@ -1,10 +1,12 @@
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(
@@ -13,12 +15,13 @@ const Checkbox = React.forwardRef<React.ElementRef<typeof CheckboxPrimitive.Root
)} )}
{...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 };
+100 -48
View File
@@ -1,22 +1,26 @@
"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 (
@@ -28,12 +32,17 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
</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}
@@ -44,27 +53,43 @@ const CommandInput = React.forwardRef<React.ElementRef<typeof CommandPrimitive.I
{...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
ref={ref}
className={cn(
"max-h-[300px] mb-1 overflow-y-auto overflow-x-hidden",
className,
)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Empty>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>>( const CommandEmpty = React.forwardRef<
(props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />, 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 CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Group>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>>( const CommandGroup = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group <CommandPrimitive.Group
ref={ref} ref={ref}
className={cn( className={cn(
@@ -73,19 +98,26 @@ const CommandGroup = React.forwardRef<React.ElementRef<typeof CommandPrimitive.G
)} )}
{...props} {...props}
/> />
), ));
)
CommandGroup.displayName = CommandPrimitive.Group.displayName 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(
@@ -94,14 +126,34 @@ const CommandItem = React.forwardRef<React.ElementRef<typeof CommandPrimitive.It
)} )}
{...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,
};
+72 -29
View File
@@ -1,15 +1,15 @@
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" import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -23,8 +23,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>,
@@ -47,30 +47,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,
};
+60 -35
View File
@@ -1,24 +1,24 @@
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
@@ -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>,
@@ -48,8 +49,9 @@ const DropdownMenuSubContent = React.forwardRef<
)} )}
{...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>,
@@ -66,13 +68,13 @@ const DropdownMenuContent = React.forwardRef<
{...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
@@ -84,8 +86,8 @@ const DropdownMenuItem = React.forwardRef<
)} )}
{...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>,
@@ -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>,
@@ -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,
} };
+11 -8
View File
@@ -1,9 +1,11 @@
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}
@@ -14,8 +16,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type,
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 };
+8 -8
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>,
@@ -22,7 +22,7 @@ const PopoverContent = React.forwardRef<
{...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 };
+63 -32
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>,
@@ -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>,
@@ -67,25 +82,36 @@ const SelectContent = React.forwardRef<
> >
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
className={cn("p-1", position === "popper" && "h-(--radix-select-trigger-height) w-full min-w-(--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(
@@ -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-px w-full" : "h-full w-px", 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 };
+10 -9
View File
@@ -1,9 +1,11 @@
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-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", "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",
@@ -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 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} 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 };
+8 -6
View File
@@ -1,10 +1,12 @@
import { createContext } from "react" import { createContext } from "react";
export interface CommandContextType { export interface CommandContextType {
isOpen: boolean isOpen: boolean;
openCommand: () => void openCommand: () => void;
closeCommand: () => void closeCommand: () => void;
toggleCommand: () => void toggleCommand: () => void;
} }
export const CommandContext = createContext<CommandContextType | undefined>(undefined) export const CommandContext = createContext<CommandContextType | undefined>(
undefined,
);
+7 -7
View File
@@ -1,13 +1,13 @@
import { ReactNode, useCallback, useState } from "react" import { type ReactNode, useCallback, useState } from "react";
import { CommandContext } from "./command-context" import { CommandContext } from "./command-context";
export function CommandProvider({ children }: { children: ReactNode }) { export function CommandProvider({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false);
const openCommand = useCallback(() => setIsOpen(true), []) const openCommand = useCallback(() => setIsOpen(true), []);
const closeCommand = useCallback(() => setIsOpen(false), []) const closeCommand = useCallback(() => setIsOpen(false), []);
const toggleCommand = useCallback(() => setIsOpen((prev) => !prev), []) const toggleCommand = useCallback(() => setIsOpen((prev) => !prev), []);
return ( return (
<CommandContext.Provider <CommandContext.Provider
@@ -20,5 +20,5 @@ export function CommandProvider({ children }: { children: ReactNode }) {
> >
{children} {children}
</CommandContext.Provider> </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 -11
View File
@@ -1,19 +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<{
id: number id: number;
name: string name: string;
status: boolean 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;
} }
+5 -5
View File
@@ -1,10 +1,10 @@
import { CommandContext } from "@/context/command-context" import { useContext } from "react";
import { useContext } from "react" import { CommandContext } from "@/context/command-context";
export function useCommand() { export function useCommand() {
const context = useContext(CommandContext) const context = useContext(CommandContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useCommand must be used within a CommandProvider") throw new Error("useCommand must be used within a CommandProvider");
} }
return context 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;
+1 -1
View File
@@ -206,7 +206,7 @@
html.disable-transitions *, html.disable-transitions *,
html.disable-transitions *::before, html.disable-transitions *::before,
html.disable-transitions *::after { html.disable-transitions *::after {
transition: none !important; transition: none;
} }
} }
+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
+5 -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" }, // 亚美尼亚
@@ -206,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>
) );
} }
+43 -31
View File
@@ -1,55 +1,67 @@
import { LoginUserResponse, MonitorResponse, ServerGroupResponse, ServiceResponse, SettingResponse } from "@/types/nezha-api" import type {
LoginUserResponse,
MonitorResponse,
ServerGroupResponse,
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 const fetchMonitor = async (
const response = await fetch(`/api/v1/service/${server_id}`) server_id: number,
const data = await response.json() ): Promise<MonitorResponse> => {
const response = await fetch(`/api/v1/service/${server_id}`);
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;
};
+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 "";
} }
+25 -19
View File
@@ -1,23 +1,28 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools" import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import ReactDOM from "react-dom/client" import ReactDOM from "react-dom/client";
import { Toaster } from "sonner" import { Toaster } from "sonner";
import App from "./App" import App from "./App";
import { ThemeColorManager } from "./components/ThemeColorManager" import { MotionProvider } from "./components/motion/motion-provider";
import { ThemeProvider } from "./components/ThemeProvider" import { ThemeColorManager } from "./components/ThemeColorManager";
import { MotionProvider } from "./components/motion/motion-provider" import { ThemeProvider } from "./components/ThemeProvider";
import { CommandProvider } from "./context/command-provider" import { CommandProvider } from "./context/command-provider";
import { SortProvider } from "./context/sort-provider" import { SortProvider } from "./context/sort-provider";
import { StatusProvider } from "./context/status-provider" import { StatusProvider } from "./context/status-provider";
import { TooltipProvider } from "./context/tooltip-provider" import { TooltipProvider } from "./context/tooltip-provider";
import { WebSocketProvider } from "./context/websocket-provider" import { WebSocketProvider } from "./context/websocket-provider";
import "./i18n" import "./i18n";
import "./index.css" import "./index.css";
const queryClient = new QueryClient() const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root")!).render( const rootElement = document.getElementById("root");
if (!rootElement) {
throw new Error("Root element not found");
}
ReactDOM.createRoot(rootElement).render(
<MotionProvider> <MotionProvider>
<ThemeProvider storageKey="vite-ui-theme"> <ThemeProvider storageKey="vite-ui-theme">
<ThemeColorManager /> <ThemeColorManager />
@@ -32,7 +37,8 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
duration={1000} duration={1000}
toastOptions={{ toastOptions={{
classNames: { classNames: {
default: "w-fit rounded-full px-2.5 py-1.5 bg-neutral-100 border border-neutral-200 backdrop-blur-xl shadow-none", default:
"w-fit rounded-full px-2.5 py-1.5 bg-neutral-100 border border-neutral-200 backdrop-blur-xl shadow-none",
}, },
}} }}
position="top-center" position="top-center"
@@ -47,4 +53,4 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
</QueryClientProvider> </QueryClientProvider>
</ThemeProvider> </ThemeProvider>
</MotionProvider>, </MotionProvider>,
) );
+8 -6
View File
@@ -1,19 +1,21 @@
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next";
interface ErrorPageProps { interface ErrorPageProps {
code?: string | number code?: string | number;
message?: string message?: string;
} }
export default function ErrorPage({ code = "500", message }: ErrorPageProps) { export default function ErrorPage({ code = "500", message }: ErrorPageProps) {
const { t } = useTranslation() const { t } = useTranslation();
return ( return (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<h1 className="text-4xl font-semibold">{code}</h1> <h1 className="text-4xl font-semibold">{code}</h1>
<p className="text-xl text-muted-foreground">{message || t("error.somethingWentWrong")}</p> <p className="text-xl text-muted-foreground">
{message || t("error.somethingWentWrong")}
</p>
</div> </div>
</div> </div>
) );
} }
+9 -7
View File
@@ -1,20 +1,22 @@
import { Button } from "@/components/ui/button" import { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom" import { Button } from "@/components/ui/button";
export default function NotFound() { export default function NotFound() {
const navigate = useNavigate() const navigate = useNavigate();
const { t } = useTranslation() const { t } = useTranslation();
return ( return (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<h1 className="text-4xl font-semibold">404</h1> <h1 className="text-4xl font-semibold">404</h1>
<p className="text-xl text-muted-foreground">{t("error.pageNotFound")}</p> <p className="text-xl text-muted-foreground">
{t("error.pageNotFound")}
</p>
<Button onClick={() => navigate("/")} className="mt-2"> <Button onClick={() => navigate("/")} className="mt-2">
{t("error.backToHome")} {t("error.backToHome")}
</Button> </Button>
</div> </div>
</div> </div>
) );
} }
+242 -137
View File
@@ -1,115 +1,144 @@
import GlobalMap from "@/components/GlobalMap" import {
import GroupSwitch from "@/components/GroupSwitch" ArrowDownIcon,
import ServerCard from "@/components/ServerCard" ArrowsUpDownIcon,
import ServerCardInline from "@/components/ServerCardInline" ArrowUpIcon,
import ServerOverview from "@/components/ServerOverview" ChartBarSquareIcon,
import { ServiceTracker } from "@/components/ServiceTracker" MapIcon,
import { Loader } from "@/components/loading/Loader" ViewColumnsIcon,
import { Label } from "@/components/ui/label" } from "@heroicons/react/20/solid";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { useQuery } from "@tanstack/react-query";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { useCallback, useEffect, useRef, useState } from "react";
import { SORT_ORDERS, SORT_TYPES } from "@/context/sort-context" import { useTranslation } from "react-i18next";
import { useSort } from "@/hooks/use-sort" import GlobalMap from "@/components/GlobalMap";
import { useStatus } from "@/hooks/use-status" import GroupSwitch from "@/components/GroupSwitch";
import { useWebSocketContext } from "@/hooks/use-websocket-context" import { Loader } from "@/components/loading/Loader";
import { fetchServerGroup } from "@/lib/nezha-api" import ServerCard from "@/components/ServerCard";
import { cn, formatNezhaInfo } from "@/lib/utils" import ServerCardInline from "@/components/ServerCardInline";
import { NezhaWebsocketResponse } from "@/types/nezha-api" import ServerOverview from "@/components/ServerOverview";
import { ServerGroup } from "@/types/nezha-api" import { ServiceTracker } from "@/components/ServiceTracker";
import { ArrowDownIcon, ArrowUpIcon, ArrowsUpDownIcon, ChartBarSquareIcon, MapIcon, ViewColumnsIcon } from "@heroicons/react/20/solid" import { Label } from "@/components/ui/label";
import { useQuery } from "@tanstack/react-query" import {
import { useEffect, useRef, useState } from "react" Popover,
import { useTranslation } from "react-i18next" PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { SORT_ORDERS, SORT_TYPES } from "@/context/sort-context";
import { useSort } from "@/hooks/use-sort";
import { useStatus } from "@/hooks/use-status";
import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { fetchServerGroup } from "@/lib/nezha-api";
import { cn, formatNezhaInfo } from "@/lib/utils";
import type { NezhaWebsocketResponse, ServerGroup } from "@/types/nezha-api";
export default function Servers() { export default function Servers() {
const { t } = useTranslation() const { t } = useTranslation();
const { sortType, sortOrder, setSortOrder, setSortType } = useSort() const { sortType, sortOrder, setSortOrder, setSortType } = useSort();
const { data: groupData } = useQuery({ const { data: groupData } = useQuery({
queryKey: ["server-group"], queryKey: ["server-group"],
queryFn: () => fetchServerGroup(), queryFn: () => fetchServerGroup(),
}) });
const { lastMessage, connected } = useWebSocketContext() const { lastMessage, connected } = useWebSocketContext();
const { status } = useStatus() const { status } = useStatus();
const [showServices, setShowServices] = useState<string>("0") const [showServices, setShowServices] = useState<string>("0");
const [showMap, setShowMap] = useState<string>("0") const [showMap, setShowMap] = useState<string>("0");
const [inline, setInline] = useState<string>("0") const [inline, setInline] = useState<string>("0");
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null);
const [settingsOpen, setSettingsOpen] = useState<boolean>(false) const [settingsOpen, setSettingsOpen] = useState<boolean>(false);
const [currentGroup, setCurrentGroup] = useState<string>("All") const [currentGroup, setCurrentGroup] = useState<string>("All");
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const restoreScrollPosition = () => { const restoreScrollPosition = useCallback(() => {
const savedPosition = sessionStorage.getItem("scrollPosition") const savedPosition = sessionStorage.getItem("scrollPosition");
if (savedPosition && containerRef.current) { if (savedPosition && containerRef.current) {
containerRef.current.scrollTop = Number(savedPosition) containerRef.current.scrollTop = Number(savedPosition);
}
} }
}, []);
const handleTagChange = (newGroup: string) => { const handleTagChange = (newGroup: string) => {
setCurrentGroup(newGroup) setCurrentGroup(newGroup);
sessionStorage.setItem("selectedGroup", newGroup) sessionStorage.setItem("selectedGroup", newGroup);
sessionStorage.setItem("scrollPosition", String(containerRef.current?.scrollTop || 0)) sessionStorage.setItem(
} "scrollPosition",
String(containerRef.current?.scrollTop || 0),
);
};
useEffect(() => { useEffect(() => {
const showServicesState = localStorage.getItem("showServices") const showServicesState = localStorage.getItem("showServices");
if (window.ForceShowServices) { if (window.ForceShowServices) {
setShowServices("1") setShowServices("1");
} else if (showServicesState !== null) { } else if (showServicesState !== null) {
setShowServices(showServicesState) setShowServices(showServicesState);
} }
}, []) }, []);
useEffect(() => { useEffect(() => {
const checkInlineSettings = () => { const checkInlineSettings = () => {
const isMobile = window.innerWidth < 768 const isMobile = window.innerWidth < 768;
if (!isMobile) { if (!isMobile) {
const inlineState = localStorage.getItem("inline") const inlineState = localStorage.getItem("inline");
if (window.ForceCardInline) { if (window.ForceCardInline) {
setInline("1") setInline("1");
} else if (inlineState !== null) { } else if (inlineState !== null) {
setInline(inlineState) setInline(inlineState);
}
} }
} }
};
checkInlineSettings() checkInlineSettings();
window.addEventListener("resize", checkInlineSettings) window.addEventListener("resize", checkInlineSettings);
return () => { return () => {
window.removeEventListener("resize", checkInlineSettings) window.removeEventListener("resize", checkInlineSettings);
} };
}, []) }, []);
useEffect(() => { useEffect(() => {
const showMapState = localStorage.getItem("showMap") const showMapState = localStorage.getItem("showMap");
if (window.ForceShowMap) { if (window.ForceShowMap) {
setShowMap("1") setShowMap("1");
} else if (showMapState !== null) { } else if (showMapState !== null) {
setShowMap(showMapState) setShowMap(showMapState);
} }
}, []) }, []);
useEffect(() => { useEffect(() => {
const savedGroup = sessionStorage.getItem("selectedGroup") || "All" const savedGroup = sessionStorage.getItem("selectedGroup") || "All";
setCurrentGroup(savedGroup) setCurrentGroup(savedGroup);
restoreScrollPosition() restoreScrollPosition();
}, []) }, [restoreScrollPosition]);
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null const nezhaWsData = lastMessage
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
: null;
const groupTabs = [ const groupTabs = [
"All", "All",
...(groupData?.data ...(groupData?.data
?.filter((item: ServerGroup) => { ?.filter((item: ServerGroup) => {
return Array.isArray(item.servers) && item.servers.some((serverId) => nezhaWsData?.servers?.some((server) => server.id === serverId)) return (
Array.isArray(item.servers) &&
item.servers.some((serverId) =>
nezhaWsData?.servers?.some((server) => server.id === serverId),
)
);
}) })
?.map((item: ServerGroup) => item.group.name) || []), ?.map((item: ServerGroup) => item.group.name) || []),
] ];
if (!connected && !lastMessage) { if (!connected && !lastMessage) {
return ( return (
@@ -119,7 +148,7 @@ export default function Servers() {
{t("info.websocketConnecting")} {t("info.websocketConnecting")}
</div> </div>
</div> </div>
) );
} }
if (!nezhaWsData) { if (!nezhaWsData) {
@@ -127,102 +156,137 @@ export default function Servers() {
<div className="flex flex-col items-center justify-center "> <div className="flex flex-col items-center justify-center ">
<p className="font-semibold text-sm">{t("info.processing")}</p> <p className="font-semibold text-sm">{t("info.processing")}</p>
</div> </div>
) );
} }
let filteredServers = let filteredServers =
nezhaWsData?.servers?.filter((server) => { nezhaWsData?.servers?.filter((server) => {
if (currentGroup === "All") return true if (currentGroup === "All") return true;
const group = groupData?.data?.find( const group = groupData?.data?.find(
(g: ServerGroup) => g.group.name === currentGroup && Array.isArray(g.servers) && g.servers.includes(server.id), (g: ServerGroup) =>
) g.group.name === currentGroup &&
return !!group Array.isArray(g.servers) &&
}) || [] g.servers.includes(server.id),
);
return !!group;
}) || [];
const totalServers = filteredServers.length || 0 const totalServers = filteredServers.length || 0;
const onlineServers = filteredServers.filter((server) => formatNezhaInfo(nezhaWsData.now, server).online)?.length || 0 const onlineServers =
const offlineServers = filteredServers.filter((server) => !formatNezhaInfo(nezhaWsData.now, server).online)?.length || 0 filteredServers.filter(
(server) => formatNezhaInfo(nezhaWsData.now, server).online,
)?.length || 0;
const offlineServers =
filteredServers.filter(
(server) => !formatNezhaInfo(nezhaWsData.now, server).online,
)?.length || 0;
const up = const up =
filteredServers.reduce( filteredServers.reduce(
(total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_out_transfer ?? 0) : total), (total, server) =>
formatNezhaInfo(nezhaWsData.now, server).online
? total + (server.state?.net_out_transfer ?? 0)
: total,
0, 0,
) || 0 ) || 0;
const down = const down =
filteredServers.reduce( filteredServers.reduce(
(total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_in_transfer ?? 0) : total), (total, server) =>
formatNezhaInfo(nezhaWsData.now, server).online
? total + (server.state?.net_in_transfer ?? 0)
: total,
0, 0,
) || 0 ) || 0;
const upSpeed = const upSpeed =
filteredServers.reduce( filteredServers.reduce(
(total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_out_speed ?? 0) : total), (total, server) =>
formatNezhaInfo(nezhaWsData.now, server).online
? total + (server.state?.net_out_speed ?? 0)
: total,
0, 0,
) || 0 ) || 0;
const downSpeed = const downSpeed =
filteredServers.reduce( filteredServers.reduce(
(total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_in_speed ?? 0) : total), (total, server) =>
formatNezhaInfo(nezhaWsData.now, server).online
? total + (server.state?.net_in_speed ?? 0)
: total,
0, 0,
) || 0 ) || 0;
filteredServers = filteredServers =
status === "all" status === "all"
? filteredServers ? filteredServers
: filteredServers.filter((server) => [status].includes(formatNezhaInfo(nezhaWsData.now, server).online ? "online" : "offline")) : filteredServers.filter((server) =>
[status].includes(
formatNezhaInfo(nezhaWsData.now, server).online
? "online"
: "offline",
),
);
filteredServers = filteredServers.sort((a, b) => { filteredServers = filteredServers.sort((a, b) => {
const serverAInfo = formatNezhaInfo(nezhaWsData.now, a) const serverAInfo = formatNezhaInfo(nezhaWsData.now, a);
const serverBInfo = formatNezhaInfo(nezhaWsData.now, b) const serverBInfo = formatNezhaInfo(nezhaWsData.now, b);
if (sortType !== "name") { if (sortType !== "name") {
// 仅在非 "name" 排序时,先按在线状态排序 // 仅在非 "name" 排序时,先按在线状态排序
if (!serverAInfo.online && serverBInfo.online) return 1 if (!serverAInfo.online && serverBInfo.online) return 1;
if (serverAInfo.online && !serverBInfo.online) return -1 if (serverAInfo.online && !serverBInfo.online) return -1;
if (!serverAInfo.online && !serverBInfo.online) { if (!serverAInfo.online && !serverBInfo.online) {
// 如果两者都离线,可以继续按照其他条件排序,或者保持原序 // 如果两者都离线,可以继续按照其他条件排序,或者保持原序
// 这里选择保持原序 // 这里选择保持原序
return 0 return 0;
} }
} }
let comparison = 0 let comparison = 0;
switch (sortType) { switch (sortType) {
case "name": case "name":
comparison = a.name.localeCompare(b.name) comparison = a.name.localeCompare(b.name);
break break;
case "uptime": case "uptime":
comparison = (a.state?.uptime ?? 0) - (b.state?.uptime ?? 0) comparison = (a.state?.uptime ?? 0) - (b.state?.uptime ?? 0);
break break;
case "system": case "system":
comparison = a.host.platform.localeCompare(b.host.platform) comparison = a.host.platform.localeCompare(b.host.platform);
break break;
case "cpu": case "cpu":
comparison = (a.state?.cpu ?? 0) - (b.state?.cpu ?? 0) comparison = (a.state?.cpu ?? 0) - (b.state?.cpu ?? 0);
break break;
case "mem": case "mem":
comparison = (formatNezhaInfo(nezhaWsData.now, a).mem ?? 0) - (formatNezhaInfo(nezhaWsData.now, b).mem ?? 0) comparison =
break (formatNezhaInfo(nezhaWsData.now, a).mem ?? 0) -
(formatNezhaInfo(nezhaWsData.now, b).mem ?? 0);
break;
case "disk": case "disk":
comparison = (formatNezhaInfo(nezhaWsData.now, a).disk ?? 0) - (formatNezhaInfo(nezhaWsData.now, b).disk ?? 0) comparison =
break (formatNezhaInfo(nezhaWsData.now, a).disk ?? 0) -
(formatNezhaInfo(nezhaWsData.now, b).disk ?? 0);
break;
case "up": case "up":
comparison = (a.state?.net_out_speed ?? 0) - (b.state?.net_out_speed ?? 0) comparison =
break (a.state?.net_out_speed ?? 0) - (b.state?.net_out_speed ?? 0);
break;
case "down": case "down":
comparison = (a.state?.net_in_speed ?? 0) - (b.state?.net_in_speed ?? 0) comparison =
break (a.state?.net_in_speed ?? 0) - (b.state?.net_in_speed ?? 0);
break;
case "up total": case "up total":
comparison = (a.state?.net_out_transfer ?? 0) - (b.state?.net_out_transfer ?? 0) comparison =
break (a.state?.net_out_transfer ?? 0) - (b.state?.net_out_transfer ?? 0);
break;
case "down total": case "down total":
comparison = (a.state?.net_in_transfer ?? 0) - (b.state?.net_in_transfer ?? 0) comparison =
break (a.state?.net_in_transfer ?? 0) - (b.state?.net_in_transfer ?? 0);
break;
default: default:
comparison = 0 comparison = 0;
} }
return sortOrder === "asc" ? comparison : -comparison return sortOrder === "asc" ? comparison : -comparison;
}) });
return ( return (
<div className="mx-auto w-full max-w-5xl px-0"> <div className="mx-auto w-full max-w-5xl px-0">
@@ -239,13 +303,14 @@ export default function Servers() {
<section className="flex items-center gap-2 w-full overflow-hidden"> <section className="flex items-center gap-2 w-full overflow-hidden">
<button <button
onClick={() => { onClick={() => {
setShowMap(showMap === "0" ? "1" : "0") setShowMap(showMap === "0" ? "1" : "0");
localStorage.setItem("showMap", showMap === "0" ? "1" : "0") localStorage.setItem("showMap", showMap === "0" ? "1" : "0");
}} }}
className={cn( className={cn(
"inset-shadow-2xs inset-shadow-white/20 flex cursor-pointer flex-col items-center gap-0 rounded-[50px] bg-blue-100 p-2.5 text-blue-600 transition-all dark:bg-blue-900 dark:text-blue-100", "inset-shadow-2xs inset-shadow-white/20 flex cursor-pointer flex-col items-center gap-0 rounded-[50px] bg-blue-100 p-2.5 text-blue-600 transition-all dark:bg-blue-900 dark:text-blue-100",
{ {
"inset-shadow-black/20 bg-blue-600 text-white dark:bg-blue-100 dark:text-blue-600": showMap === "1", "inset-shadow-black/20 bg-blue-600 text-white dark:bg-blue-100 dark:text-blue-600":
showMap === "1",
}, },
{ {
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage, "bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
@@ -256,13 +321,17 @@ export default function Servers() {
</button> </button>
<button <button
onClick={() => { onClick={() => {
setShowServices(showServices === "0" ? "1" : "0") setShowServices(showServices === "0" ? "1" : "0");
localStorage.setItem("showServices", showServices === "0" ? "1" : "0") localStorage.setItem(
"showServices",
showServices === "0" ? "1" : "0",
);
}} }}
className={cn( className={cn(
"inset-shadow-2xs inset-shadow-white/20 flex cursor-pointer flex-col items-center gap-0 rounded-[50px] bg-blue-100 p-2.5 text-blue-600 transition-all dark:bg-blue-900 dark:text-blue-100", "inset-shadow-2xs inset-shadow-white/20 flex cursor-pointer flex-col items-center gap-0 rounded-[50px] bg-blue-100 p-2.5 text-blue-600 transition-all dark:bg-blue-900 dark:text-blue-100",
{ {
"inset-shadow-black/20 bg-blue-600 text-white dark:bg-blue-100 dark:text-blue-600": showServices === "1", "inset-shadow-black/20 bg-blue-600 text-white dark:bg-blue-100 dark:text-blue-600":
showServices === "1",
}, },
{ {
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage, "bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
@@ -273,13 +342,14 @@ export default function Servers() {
</button> </button>
<button <button
onClick={() => { onClick={() => {
setInline(inline === "0" ? "1" : "0") setInline(inline === "0" ? "1" : "0");
localStorage.setItem("inline", inline === "0" ? "1" : "0") localStorage.setItem("inline", inline === "0" ? "1" : "0");
}} }}
className={cn( className={cn(
"inset-shadow-2xs inset-shadow-white/20 flex cursor-pointer flex-col items-center gap-0 rounded-[50px] bg-blue-100 p-2.5 text-blue-600 transition-all dark:bg-blue-900 dark:text-blue-100", "inset-shadow-2xs inset-shadow-white/20 flex cursor-pointer flex-col items-center gap-0 rounded-[50px] bg-blue-100 p-2.5 text-blue-600 transition-all dark:bg-blue-900 dark:text-blue-100",
{ {
"inset-shadow-black/20 bg-blue-600 text-white dark:bg-blue-100 dark:text-blue-600": inline === "1", "inset-shadow-black/20 bg-blue-600 text-white dark:bg-blue-100 dark:text-blue-600":
inline === "1",
}, },
{ {
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage, "bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
@@ -292,7 +362,11 @@ export default function Servers() {
})} })}
/> />
</button> </button>
<GroupSwitch tabs={groupTabs} currentTab={currentGroup} setCurrentTab={handleTagChange} /> <GroupSwitch
tabs={groupTabs}
currentTab={currentGroup}
setCurrentTab={handleTagChange}
/>
</section> </section>
<Popover onOpenChange={setSettingsOpen}> <Popover onOpenChange={setSettingsOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@@ -300,14 +374,18 @@ export default function Servers() {
className={cn( className={cn(
"rounded-[50px] flex items-center gap-1 dark:text-white border dark:border-none text-black cursor-pointer dark:[text-shadow:0_1px_0_rgb(0_0_0/20%)] dark:bg-stone-800 bg-white p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ", "rounded-[50px] flex items-center gap-1 dark:text-white border dark:border-none text-black cursor-pointer dark:[text-shadow:0_1px_0_rgb(0_0_0/20%)] dark:bg-stone-800 bg-white p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
{ {
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] dark:bg-stone-700 bg-stone-200": settingsOpen, "shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] dark:bg-stone-700 bg-stone-200":
settingsOpen,
}, },
{ {
"dark:bg-stone-800/70 bg-stone-100/70 ": customBackgroundImage, "dark:bg-stone-800/70 bg-stone-100/70 ":
customBackgroundImage,
}, },
)} )}
> >
<p className="text-[10px] font-bold whitespace-nowrap">{sortType === "default" ? "Sort" : sortType.toUpperCase()}</p> <p className="text-[10px] font-bold whitespace-nowrap">
{sortType === "default" ? "Sort" : sortType.toUpperCase()}
</p>
{sortOrder === "asc" && sortType !== "default" ? ( {sortOrder === "asc" && sortType !== "default" ? (
<ArrowUpIcon className="size-[13px]" /> <ArrowUpIcon className="size-[13px]" />
) : sortOrder === "desc" && sortType !== "default" ? ( ) : sortOrder === "desc" && sortType !== "default" ? (
@@ -320,7 +398,9 @@ export default function Servers() {
<PopoverContent className="p-4 w-[240px] rounded-lg"> <PopoverContent className="p-4 w-[240px] rounded-lg">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-xs font-medium text-muted-foreground">Sort by</Label> <Label className="text-xs font-medium text-muted-foreground">
Sort by
</Label>
<Select value={sortType} onValueChange={setSortType}> <Select value={sortType} onValueChange={setSortType}>
<SelectTrigger className="w-full text-xs h-8"> <SelectTrigger className="w-full text-xs h-8">
<SelectValue placeholder="Choose type" /> <SelectValue placeholder="Choose type" />
@@ -335,8 +415,14 @@ export default function Servers() {
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-xs font-medium text-muted-foreground">Sort order</Label> <Label className="text-xs font-medium text-muted-foreground">
<Select value={sortOrder} onValueChange={setSortOrder} disabled={sortType === "default"}> Sort order
</Label>
<Select
value={sortOrder}
onValueChange={setSortOrder}
disabled={sortType === "default"}
>
<SelectTrigger className="w-full text-xs h-8"> <SelectTrigger className="w-full text-xs h-8">
<SelectValue placeholder="Choose order" /> <SelectValue placeholder="Choose order" />
</SelectTrigger> </SelectTrigger>
@@ -353,22 +439,41 @@ export default function Servers() {
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
{showMap === "1" && <GlobalMap now={nezhaWsData.now} serverList={nezhaWsData?.servers || []} />} {showMap === "1" && (
<GlobalMap
now={nezhaWsData.now}
serverList={nezhaWsData?.servers || []}
/>
)}
{showServices === "1" && <ServiceTracker serverList={filteredServers} />} {showServices === "1" && <ServiceTracker serverList={filteredServers} />}
{inline === "1" && ( {inline === "1" && (
<section ref={containerRef} className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden mt-6 server-inline-list"> <section
ref={containerRef}
className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden mt-6 server-inline-list"
>
{filteredServers.map((serverInfo) => ( {filteredServers.map((serverInfo) => (
<ServerCardInline now={nezhaWsData.now} key={serverInfo.id} serverInfo={serverInfo} /> <ServerCardInline
now={nezhaWsData.now}
key={serverInfo.id}
serverInfo={serverInfo}
/>
))} ))}
</section> </section>
)} )}
{inline === "0" && ( {inline === "0" && (
<section ref={containerRef} className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6 server-card-list"> <section
ref={containerRef}
className="grid grid-cols-1 gap-2 md:grid-cols-2 mt-6 server-card-list"
>
{filteredServers.map((serverInfo) => ( {filteredServers.map((serverInfo) => (
<ServerCard now={nezhaWsData.now} key={serverInfo.id} serverInfo={serverInfo} /> <ServerCard
now={nezhaWsData.now}
key={serverInfo.id}
serverInfo={serverInfo}
/>
))} ))}
</section> </section>
)} )}
</div> </div>
) );
} }
+26 -19
View File
@@ -1,27 +1,27 @@
import { NetworkChart } from "@/components/NetworkChart" import { useEffect, useState } from "react";
import ServerDetailChart from "@/components/ServerDetailChart" import { useNavigate, useParams } from "react-router-dom";
import ServerDetailOverview from "@/components/ServerDetailOverview" import { NetworkChart } from "@/components/NetworkChart";
import ServerDetailSummary from "@/components/ServerDetailSummary" import ServerDetailChart from "@/components/ServerDetailChart";
import TabSwitch from "@/components/TabSwitch" import ServerDetailOverview from "@/components/ServerDetailOverview";
import { Separator } from "@/components/ui/separator" import ServerDetailSummary from "@/components/ServerDetailSummary";
import { useEffect, useState } from "react" import TabSwitch from "@/components/TabSwitch";
import { useNavigate, useParams } from "react-router-dom" import { Separator } from "@/components/ui/separator";
export default function ServerDetail() { export default function ServerDetail() {
const navigate = useNavigate() const navigate = useNavigate();
useEffect(() => { useEffect(() => {
window.scrollTo({ top: 0, left: 0, behavior: "instant" }) window.scrollTo({ top: 0, left: 0, behavior: "instant" });
}, []) }, []);
const tabs = ["Detail", "Network"] const tabs = ["Detail", "Network"];
const [currentTab, setCurrentTab] = useState(tabs[0]) const [currentTab, setCurrentTab] = useState(tabs[0]);
const { id: server_id } = useParams() const { id: server_id } = useParams();
if (!server_id) { if (!server_id) {
navigate("/404") navigate("/404");
return null return null;
} }
return ( return (
@@ -30,7 +30,11 @@ export default function ServerDetail() {
<section className="flex items-center my-2 w-full"> <section className="flex items-center my-2 w-full">
<Separator className="flex-1" /> <Separator className="flex-1" />
<div className="flex justify-center w-full max-w-[200px]"> <div className="flex justify-center w-full max-w-[200px]">
<TabSwitch tabs={tabs} currentTab={currentTab} setCurrentTab={setCurrentTab} /> <TabSwitch
tabs={tabs}
currentTab={currentTab}
setCurrentTab={setCurrentTab}
/>
</div> </div>
<Separator className="flex-1" /> <Separator className="flex-1" />
</section> </section>
@@ -43,8 +47,11 @@ export default function ServerDetail() {
<ServerDetailChart server_id={server_id} /> <ServerDetailChart server_id={server_id} />
</div> </div>
<div style={{ display: currentTab === tabs[1] ? "block" : "none" }}> <div style={{ display: currentTab === tabs[1] ? "block" : "none" }}>
<NetworkChart server_id={Number(server_id)} show={currentTab === tabs[1]} /> <NetworkChart
server_id={Number(server_id)}
show={currentTab === tabs[1]}
/>
</div> </div>
</div> </div>
) );
} }
+2 -2
View File
@@ -1,4 +1,4 @@
declare module "*.css" { declare module "*.css" {
const css: { [key: string]: string } const css: { [key: string]: string };
export default css export default css;
} }
+103 -103
View File
@@ -1,160 +1,160 @@
export interface NezhaWebsocketResponse { export interface NezhaWebsocketResponse {
now: number now: number;
servers: NezhaServer[] servers: NezhaServer[];
} }
export interface NezhaServer { export interface NezhaServer {
id: number id: number;
name: string name: string;
public_note: string public_note: string;
last_active: string last_active: string;
country_code: string country_code: string;
host: NezhaServerHost host: NezhaServerHost;
state: NezhaServerStatus state: NezhaServerStatus;
} }
export interface NezhaServerHost { export interface NezhaServerHost {
platform: string platform: string;
platform_version: string platform_version: string;
cpu: string[] cpu: string[];
gpu: string[] gpu: string[];
mem_total: number mem_total: number;
disk_total: number disk_total: number;
swap_total: number swap_total: number;
arch: string arch: string;
boot_time: number boot_time: number;
version: string version: string;
} }
export interface NezhaServerStatus { export interface NezhaServerStatus {
cpu: number cpu: number;
mem_used: number mem_used: number;
swap_used: number swap_used: number;
disk_used: number disk_used: number;
net_in_transfer: number net_in_transfer: number;
net_out_transfer: number net_out_transfer: number;
net_in_speed: number net_in_speed: number;
net_out_speed: number net_out_speed: number;
uptime: number uptime: number;
load_1: number load_1: number;
load_5: number load_5: number;
load_15: number load_15: number;
tcp_conn_count: number tcp_conn_count: number;
udp_conn_count: number udp_conn_count: number;
process_count: number process_count: number;
temperatures: temperature[] temperatures: temperature[];
gpu: number[] gpu: number[];
} }
interface temperature { interface temperature {
Name: string Name: string;
Temperature: number Temperature: number;
} }
export interface ServerGroupResponse { export interface ServerGroupResponse {
success: boolean success: boolean;
data: ServerGroup[] data: ServerGroup[];
} }
export interface ServerGroup { export interface ServerGroup {
group: { group: {
id: number id: number;
created_at: string created_at: string;
updated_at: string updated_at: string;
name: string name: string;
} };
servers: number[] servers: number[];
} }
export interface LoginUserResponse { export interface LoginUserResponse {
success: boolean success: boolean;
data: { data: {
id: number id: number;
username: string username: string;
password: string password: string;
created_at: string created_at: string;
updated_at: string updated_at: string;
} };
} }
export interface MonitorResponse { export interface MonitorResponse {
success: boolean success: boolean;
data: NezhaMonitor[] data: NezhaMonitor[];
} }
export type ServerMonitorChart = { export type ServerMonitorChart = {
[key: string]: { [key: string]: {
created_at: number created_at: number;
avg_delay: number avg_delay: number;
packet_loss?: number packet_loss?: number;
}[] }[];
} };
export interface NezhaMonitor { export interface NezhaMonitor {
monitor_id: number monitor_id: number;
monitor_name: string monitor_name: string;
server_id: number server_id: number;
server_name: string server_name: string;
created_at: number[] created_at: number[];
avg_delay: number[] avg_delay: number[];
packet_loss?: number[] packet_loss?: number[];
} }
export interface ServiceResponse { export interface ServiceResponse {
success: boolean success: boolean;
data: { data: {
services: { services: {
[key: string]: ServiceData [key: string]: ServiceData;
} };
cycle_transfer_stats: CycleTransferStats cycle_transfer_stats: CycleTransferStats;
} };
} }
export interface ServiceData { export interface ServiceData {
service_name: string service_name: string;
current_up: number current_up: number;
current_down: number current_down: number;
total_up: number total_up: number;
total_down: number total_down: number;
delay: number[] delay: number[];
up: number[] up: number[];
down: number[] down: number[];
} }
export interface CycleTransferStats { export interface CycleTransferStats {
[key: string]: CycleTransferData [key: string]: CycleTransferData;
} }
export interface CycleTransferData { export interface CycleTransferData {
name: string name: string;
from: string from: string;
to: string to: string;
max: number max: number;
min: number min: number;
server_name: { server_name: {
[key: string]: string [key: string]: string;
} };
transfer: { transfer: {
[key: string]: number [key: string]: number;
} };
next_update: { next_update: {
[key: string]: string [key: string]: string;
} };
} }
type SettingConfig = { type SettingConfig = {
debug: boolean debug: boolean;
language: string language: string;
site_name: string site_name: string;
user_template: string user_template: string;
admin_template: string admin_template: string;
custom_code: string custom_code: string;
} };
export interface SettingResponse { export interface SettingResponse {
success: boolean success: boolean;
data: { data: {
config: SettingConfig config: SettingConfig;
version: string version: string;
} };
} }
+4 -1
View File
@@ -1,6 +1,9 @@
{ {
"files": [], "files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }], "references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {

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