mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 09:31:55 +08:00
Merge branch 'main' of https://github.com/hamster1963/nezha-dash-v1 into hamster1963-main
This commit is contained in:
commit
b3df24ea08
19
.cert/cert.pem
Normal file
19
.cert/cert.pem
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDCTCCAfGgAwIBAgIUQxY5HJAktPoEWU9osMraUrm/DEAwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDIxMzAzMTA0MVoXDTI2MDIx
|
||||||
|
MzAzMTA0MVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
||||||
|
AAOCAQ8AMIIBCgKCAQEAraDt2UXkzKLRskNtVDo1iXe1tBTYTAFtl+m7JOvdYdmS
|
||||||
|
oenV3Cn/8Cd8JuusQVl9jovcMFb3pwrQzodSQ9oN70B/MSqA/Pjgpji+uu4Hjcas
|
||||||
|
VhaAHregBsV8ULl+OikPPFWcGKRZMtRyta3Sy/2E5Y44wr8vdERKDl/6ydDVioe5
|
||||||
|
dQQS+klyzamy9ayQj8fpSTR96H+WpDd6gGuDf+XlrqlnrgatiUIJiDkeJPCIUNJi
|
||||||
|
VSw8lq3KO8O4K376smCAdngdyYg+q/Sk2r5MnHi9VqNknwmos06yPk6vTWIpZ+mK
|
||||||
|
bz9W2HW4sukU0nwRXP0p29SKoW5ZKPvrLvfNDp0P3QIDAQABo1MwUTAdBgNVHQ4E
|
||||||
|
FgQUYSHtj6LjfaQ0BmuCdlHf/EXKm5AwHwYDVR0jBBgwFoAUYSHtj6LjfaQ0BmuC
|
||||||
|
dlHf/EXKm5AwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAQnM/
|
||||||
|
MIYunEp8ITMtllILW9TJhZVertfuux4S1rgRZ3VADmHgHftCgUKpm4kh8w2gEZ0M
|
||||||
|
DXTmnIwqaBa+lpiCcALECUQ1L2jPcwCYowmEfnKLF6Ob3Tnznz0eqr8TnvuKCX4c
|
||||||
|
ehSlfqOcUn8rveLDX91j+FJ+LSggf/kYjhE0ACtZHJyEM9csWu5chu8cCjpq5pn/
|
||||||
|
ahiPw5eUnxsyBWdqlkMvY+lofH7SaunXrbLcIDg67wMl0FpZ39z/UAhIVNiyUIDe
|
||||||
|
k7pNzRu99r5hIqdyfx5zULG2mzJCSsJj63t4BeDwr6u+zXSlyVMqh5cXj9mk4LJ6
|
||||||
|
DhJlnudcCV5t/RGyOw==
|
||||||
|
-----END CERTIFICATE-----
|
28
.cert/key.pem
Normal file
28
.cert/key.pem
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCtoO3ZReTMotGy
|
||||||
|
Q21UOjWJd7W0FNhMAW2X6bsk691h2ZKh6dXcKf/wJ3wm66xBWX2Oi9wwVvenCtDO
|
||||||
|
h1JD2g3vQH8xKoD8+OCmOL667geNxqxWFoAet6AGxXxQuX46KQ88VZwYpFky1HK1
|
||||||
|
rdLL/YTljjjCvy90REoOX/rJ0NWKh7l1BBL6SXLNqbL1rJCPx+lJNH3of5akN3qA
|
||||||
|
a4N/5eWuqWeuBq2JQgmIOR4k8IhQ0mJVLDyWrco7w7grfvqyYIB2eB3JiD6r9KTa
|
||||||
|
vkyceL1Wo2SfCaizTrI+Tq9NYiln6YpvP1bYdbiy6RTSfBFc/Snb1Iqhblko++su
|
||||||
|
980OnQ/dAgMBAAECggEAI/6N+GI9N7AUVUaVqmWj1iL/Q/0jRwRvxhOyFIoiG6gp
|
||||||
|
dg/+IhWB5bUlz4LBc8270fqME+hfkF1VYs9aXk8c3unJxHVJhsgIeGUgoyt33Owg
|
||||||
|
K3ugJV4PWoAD0M9Xi/KZojokMVaW2EsDGcdWgSwGKjmk6jiMu6dxi8/Zc4+ryTsY
|
||||||
|
3+KMUocFyqMfYK1/sYSTPzlPWcCGMuaO36Df++cAzKLlqHRh7BLgSiCXBrV8ITFf
|
||||||
|
LTkQFDf/c+yVC6mJG/GXzqdKXS3OT97sW34tdmQPNhReCSkSEDVQt+tnFa5be1R2
|
||||||
|
18mODkaSv4DxnMXnlfexon/pGuXukgrMTZQXq2+pIQKBgQDaNvBmJrSCU9NSDNSj
|
||||||
|
I0yTX8DUzEv1bxErbfptSlSoUcEIPcLsxt+xZFVfU8IcMvQ43gHSsRquCfZsUZ0r
|
||||||
|
/ZIfJ7pWTqbxd/EybsMiC2ZSS8NdVX3MJhKinrZXMTRAA8l5a4AFr2YKMtNqQpGY
|
||||||
|
xWu8TS7PR8N9B6vZqGC9hhID4QKBgQDLsZLrezomWTthFAOfACj/ebIEyZ30YVNw
|
||||||
|
7IaaVTkeWtYGJXasMrts1+n15dPwR6a18c65hSywJKsCEYD6z/uXxaoX1bK32oLw
|
||||||
|
49thMw+qSilA1jMQ/XQxx9TFsmrCvwSm5xIjSV+0pD1sApiivGQAU+2oHZeEwLue
|
||||||
|
v51JxnaLfQKBgQDVYUWgThbTHk8U+7DuObVGoyp3q7JXNJ1wf2GTf0zbLt54RZSX
|
||||||
|
Xj0dRMRqrAey9Wx1MzpLIZ26M8nAz+nGO3Woe3utq8l5c9TqgP7VCpqqvKU0XkXd
|
||||||
|
3Bj65gHdryKtukZIMgOFC6fXLy4mySOAZQRdpIeybzVMzLSR6SF4EmMJYQKBgGtz
|
||||||
|
xVlLrCVGtThE4pQh9X6vp+U2poigPvA3FdqcUoFc0cJ0SOIV8SE91UHOd7stURhx
|
||||||
|
8ueTBTv2W++/ZBbrWIF72HqyVJEASErjKHtiAEWI0bJOTKoNyhnonKmdsQwC0GVr
|
||||||
|
R/otXrtgWLZ9uB9A2lAB9kDVO3TgZxkbY9HjS+3RAoGBAKVKcJFErNZhQxCx5ll3
|
||||||
|
u9wtE7duiVcS3jZhFa7tvcSc4O5+ahEQG/gy2M6kgqB/f3nMH6Rd9wsTzwPp1uZz
|
||||||
|
qiumr3ZOvpTWuLiIMQi3sE9pBGz7p+ZTeP8Z0Wez98v9MVmgsCsPqDOpa1JhnJIq
|
||||||
|
2AgG3D/RUJylOPYnMq8vdAyx
|
||||||
|
-----END PRIVATE KEY-----
|
14
index.html
14
index.html
@ -107,9 +107,17 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/apple-touch-icon.png" />
|
<link rel="icon" type="image/png" href="/apple-touch-icon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>CZL SVR</title>
|
<title>CZL Server</title>
|
||||||
<link rel="stylesheet" href="https://i-aws.czl.net/jsdelivr/gh/lipis/flag-icons@7.0.0/css/flag-icons.min.css" />
|
|
||||||
<link rel="stylesheet" href="https://i-aws.czl.net/jsdelivr/npm/font-logos@1/assets/font-logos.css" />
|
<!-- PWA -->
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<meta name="theme-color" content="hsl(0 0% 98%)" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="CZL Server" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://i.czl.net/jsdelivr/gh/lipis/flag-icons@7.0.0/css/flag-icons.min.css" />
|
||||||
|
<link rel="stylesheet" href="https://i.czl.net/jsdelivr/npm/font-logos@1/assets/font-logos.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
72
package.json
72
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "nazha-dashboard-vite",
|
"name": "nazha-dash-v1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.1",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@ -13,58 +13,60 @@
|
|||||||
"@fontsource/inter": "^5.1.1",
|
"@fontsource/inter": "^5.1.1",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@number-flow/react": "^0.5.5",
|
"@number-flow/react": "^0.5.5",
|
||||||
"@radix-ui/react-accordion": "^1.2.2",
|
"@radix-ui/react-accordion": "^1.2.3",
|
||||||
"@radix-ui/react-checkbox": "^1.1.3",
|
"@radix-ui/react-checkbox": "^1.1.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.1.2",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.1.6",
|
||||||
"@radix-ui/react-progress": "^1.1.1",
|
"@radix-ui/react-progress": "^1.1.2",
|
||||||
"@radix-ui/react-separator": "^1.1.1",
|
"@radix-ui/react-select": "^2.1.6",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-separator": "^1.1.2",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@tanstack/react-query": "^5.65.1",
|
"@radix-ui/react-switch": "^1.1.3",
|
||||||
"@tanstack/react-query-devtools": "^5.65.1",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@tanstack/react-table": "^8.20.6",
|
"@tanstack/react-query": "^5.66.7",
|
||||||
|
"@tanstack/react-query-devtools": "^5.66.7",
|
||||||
|
"@tanstack/react-table": "^8.21.2",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.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.0",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "1.0.0",
|
||||||
"country-flag-icons": "^1.5.14",
|
"country-flag-icons": "^1.5.18",
|
||||||
"d3-geo": "^3.1.1",
|
"d3-geo": "^3.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"framer-motion": "^12.0.6",
|
"framer-motion": "^12.4.5",
|
||||||
"i18n-iso-countries": "^7.13.0",
|
"i18n-iso-countries": "^7.14.0",
|
||||||
"i18next": "^24.2.2",
|
"i18next": "^24.2.2",
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.1",
|
||||||
"react-router-dom": "^7.1.3",
|
"react-router-dom": "^7.2.0",
|
||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"sonner": "^1.7.3",
|
"sonner": "^1.7.4",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.19.0",
|
"@eslint/js": "^9.20.0",
|
||||||
"@types/node": "^22.12.0",
|
"@types/node": "^22.13.4",
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.19.0",
|
"eslint": "^9.20.1",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.18",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.15.0",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.3",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.6.3",
|
||||||
"typescript-eslint": "^8.22.0",
|
"typescript-eslint": "^8.24.1",
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
public/android-chrome-192x192.png
Normal file
BIN
public/android-chrome-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
17
public/manifest.json
Normal file
17
public/manifest.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "Nezha Monitoring",
|
||||||
|
"short_name": "Nezha Monitoring",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "hsl(0 0% 98%)",
|
||||||
|
"background_color": "hsl(0 0% 98%)",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait"
|
||||||
|
}
|
@ -17,7 +17,7 @@ export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({ serv
|
|||||||
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-2 gap-2 md:gap-4">
|
<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
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import { formatBytes } from "@/lib/format"
|
import { formatBytes } from "@/lib/format"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { CircleStackIcon } from "@heroicons/react/24/outline"
|
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar"
|
|
||||||
|
|
||||||
interface CycleTransferStatsClientProps {
|
interface CycleTransferStatsClientProps {
|
||||||
name: string
|
name: string
|
||||||
from: string
|
from: string
|
||||||
@ -26,7 +23,7 @@ export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> =
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full bg-white px-4 py-3 rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none space-y-2",
|
"w-full bg-white px-4 py-3.5 rounded-lg border bg-card text-card-foreground hover:shadow-sm transition-all duration-200 dark:shadow-none",
|
||||||
className,
|
className,
|
||||||
{
|
{
|
||||||
"bg-card/70": customBackgroundImage,
|
"bg-card/70": customBackgroundImage,
|
||||||
@ -37,43 +34,41 @@ export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> =
|
|||||||
const progress = (transfer / max) * 100
|
const progress = (transfer / max) * 100
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={serverId}>
|
<div key={serverId} className="space-y-3">
|
||||||
<section className="flex justify-between items-center">
|
{/* Header */}
|
||||||
<div className="bg-green-600 w-fit text-white px-1.5 py-0.5 rounded-full text-[10px]">{name}</div>
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-stone-600 dark:text-stone-400 text-xs">
|
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{serverName}</span>
|
||||||
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()}
|
<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>
|
||||||
</span>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="flex justify-between items-center mt-2">
|
|
||||||
<div className="flex gap-1 items-center">
|
|
||||||
<CircleStackIcon className="size-3 text-neutral-400 dark:text-neutral-600" />
|
|
||||||
<span className="text-sm font-semibold">{serverName}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<p className="text-xs text-end w-10 font-medium">{progress.toFixed(0)}%</p>
|
|
||||||
<AnimatedCircularProgressBar className="size-4 text-[0px]" max={100} min={0} value={progress} primaryColor="hsl(var(--chart-5))" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="w-full bg-neutral-100 dark:bg-neutral-800 rounded-full overflow-hidden h-2.5 mt-2">
|
|
||||||
<div className="bg-green-600 h-2.5 rounded-full" style={{ width: `${Math.min(progress, 100)}%` }} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="flex justify-between items-center mt-2">
|
{/* Progress Section */}
|
||||||
<span className="text-[13px] text-stone-800 dark:text-stone-400 font-medium">
|
<div className="space-y-1.5">
|
||||||
{formatBytes(transfer)} {t("cycleTransfer.used")}
|
<div className="flex items-center justify-between">
|
||||||
</span>
|
<div className="flex items-baseline gap-1">
|
||||||
<span className="text-xs text-stone-500 dark:text-stone-400 font-normal">
|
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{formatBytes(transfer)}</span>
|
||||||
{formatBytes(max)} {t("cycleTransfer.total")}
|
<span className="text-xs text-neutral-500 dark:text-neutral-400">/ {formatBytes(max)}</span>
|
||||||
</span>
|
</div>
|
||||||
</section>
|
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">{progress.toFixed(1)}%</span>
|
||||||
|
|
||||||
<section className="flex justify-between items-center mt-2">
|
|
||||||
<div className="text-xs text-stone-500 dark:text-stone-400">
|
|
||||||
{t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
<div className="relative h-1.5">
|
||||||
|
<div className="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full" />
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300"
|
||||||
|
style={{ width: `${Math.min(progress, 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400">
|
||||||
|
<span>
|
||||||
|
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
const Footer: React.FC = () => {
|
const Footer: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const isMac = /macintosh|mac os x/i.test(navigator.userAgent)
|
||||||
|
|
||||||
|
const { data: settingData } = useQuery({
|
||||||
|
queryKey: ["setting"],
|
||||||
|
queryFn: () => fetchSetting(),
|
||||||
|
refetchOnMount: 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">
|
||||||
<section className="flex flex-col">
|
<section className="flex flex-col">
|
||||||
@ -11,9 +21,16 @@ const Footer: React.FC = () => {
|
|||||||
CZL LTD
|
CZL LTD
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<p className="server-footer-theme">
|
<div className="server-footer-theme flex flex-col items-center sm:items-end">
|
||||||
|
<p className="mt-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
|
||||||
|
<kbd className="pointer-events-none mx-1 inline-flex h-4 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||||
|
<span className="text-xs">⌘</span>K
|
||||||
|
</kbd>
|
||||||
|
</p>
|
||||||
|
<p className="server-footer-theme">
|
||||||
All Rights Reserved
|
All Rights Reserved
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -293,7 +293,7 @@ function Overview() {
|
|||||||
<div style={{ fontVariantNumeric: "tabular-nums" }} className="flex text-sm font-medium mt-0.5">
|
<div style={{ fontVariantNumeric: "tabular-nums" }} className="flex text-sm font-medium mt-0.5">
|
||||||
<NumberFlow trend={1} value={time.hh} format={{ minimumIntegerDigits: 2 }} />
|
<NumberFlow trend={1} value={time.hh} format={{ minimumIntegerDigits: 2 }} />
|
||||||
<NumberFlow prefix=":" trend={1} value={time.mm} digits={{ 1: { max: 5 } }} format={{ minimumIntegerDigits: 2 }} />
|
<NumberFlow prefix=":" trend={1} value={time.mm} digits={{ 1: { max: 5 } }} format={{ minimumIntegerDigits: 2 }} />
|
||||||
<NumberFlow prefix=":" trend={1} value={time.ss} digits={{ 1: { max: 5 } }} format={{ minimumIntegerDigits: 2 }} />
|
<p className="mt-[0.5px]">:{time.ss.toString().padStart(2, "0")}</p>
|
||||||
</div>
|
</div>
|
||||||
</NumberFlowGroup>
|
</NumberFlowGroup>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
|||||||
import { ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
|
import { ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
|
||||||
import { fetchMonitor } from "@/lib/nezha-api"
|
import { fetchMonitor } from "@/lib/nezha-api"
|
||||||
import { cn, formatTime } from "@/lib/utils"
|
import { cn, formatTime } from "@/lib/utils"
|
||||||
import { formatRelativeTime } from "@/lib/utils"
|
|
||||||
import { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api"
|
import { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
@ -95,8 +94,10 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
|||||||
|
|
||||||
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 [activeChart, setActiveChart] = React.useState(defaultChart)
|
const [activeChart, setActiveChart] = React.useState(defaultChart)
|
||||||
const [isPeakEnabled, setIsPeakEnabled] = React.useState(false)
|
const [isPeakEnabled, setIsPeakEnabled] = React.useState(forcePeakCutEnabled)
|
||||||
|
|
||||||
const handleButtonClick = useCallback(
|
const handleButtonClick = useCallback(
|
||||||
(chart: string) => {
|
(chart: string) => {
|
||||||
@ -264,12 +265,36 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
|||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="created_at"
|
dataKey="created_at"
|
||||||
tickLine={false}
|
tickLine={true}
|
||||||
|
tickSize={3}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickMargin={8}
|
tickMargin={8}
|
||||||
minTickGap={32}
|
minTickGap={80}
|
||||||
interval={"preserveStartEnd"}
|
ticks={processedData
|
||||||
tickFormatter={(value) => formatRelativeTime(value)}
|
.filter((item, index, array) => {
|
||||||
|
if (array.length < 6) {
|
||||||
|
return index === 0 || index === array.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算数据的总时间跨度(毫秒)
|
||||||
|
const timeSpan = array[array.length - 1].created_at - array[0].created_at
|
||||||
|
const hours = timeSpan / (1000 * 60 * 60)
|
||||||
|
|
||||||
|
// 根据时间跨度调整显示间隔
|
||||||
|
if (hours <= 12) {
|
||||||
|
// 12小时内,每60分钟显示一个刻度
|
||||||
|
return index === 0 || index === array.length - 1 || new Date(item.created_at).getMinutes() % 60 === 0
|
||||||
|
}
|
||||||
|
// 超过12小时,每2小时显示一个刻度
|
||||||
|
const date = new Date(item.created_at)
|
||||||
|
return date.getMinutes() === 0 && date.getHours() % 2 === 0
|
||||||
|
})
|
||||||
|
.map((item) => item.created_at)}
|
||||||
|
tickFormatter={(value) => {
|
||||||
|
const date = new Date(value)
|
||||||
|
const minutes = date.getMinutes()
|
||||||
|
return minutes === 0 ? `${date.getHours()}:00` : `${date.getHours()}:${minutes}`
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<YAxis tickLine={false} axisLine={false} tickMargin={15} minTickGap={20} tickFormatter={(value) => `${value}ms`} />
|
<YAxis tickLine={false} axisLine={false} tickMargin={15} minTickGap={20} tickFormatter={(value) => `${value}ms`} />
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
|
@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next"
|
|||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion"
|
import { 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({ server_id }: { server_id: string }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -165,17 +166,26 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{country_code && (
|
{country_code && (
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
<TooltipProvider delayDuration={100}>
|
||||||
<CardContent className="px-1.5 py-1">
|
<Tooltip>
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
<TooltipTrigger asChild>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.region")}</p>
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
<section className="flex items-start gap-1">
|
<CardContent className="px-1.5 py-1">
|
||||||
<div className="text-xs text-start">{countries.getName(country_code?.toUpperCase(), "en")}</div>
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
{country_code && <ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />}
|
<p className="text-xs text-muted-foreground">{t("serverDetail.region")}</p>
|
||||||
</section>
|
<section className="flex items-start gap-1">
|
||||||
</section>
|
<div className="text-xs text-start">{country_code?.toUpperCase()}</div>
|
||||||
</CardContent>
|
{country_code && <ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />}
|
||||||
</Card>
|
</section>
|
||||||
|
</section>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{countries.getName(country_code?.toUpperCase(), "en")}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
<section className="flex flex-wrap gap-2 mt-1">
|
<section className="flex flex-wrap gap-2 mt-1">
|
||||||
|
@ -19,10 +19,16 @@ export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const processServiceData = (serviceData: ServiceData) => {
|
const processServiceData = (serviceData: ServiceData) => {
|
||||||
const days = serviceData.up.map((up, index) => ({
|
const days = serviceData.up.map((up, index) => {
|
||||||
completed: up > serviceData.down[index],
|
const totalChecks = up + serviceData.down[index]
|
||||||
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
|
const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0
|
||||||
}))
|
return {
|
||||||
|
completed: up > serviceData.down[index],
|
||||||
|
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
|
||||||
|
uptime: dailyUptime,
|
||||||
|
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 = serviceData.up.reduce((a, b) => a + b, 0) + serviceData.down.reduce((a, b) => a + b, 0)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
@ -8,6 +9,8 @@ interface ServiceTrackerProps {
|
|||||||
days: Array<{
|
days: Array<{
|
||||||
completed: boolean
|
completed: boolean
|
||||||
date?: Date
|
date?: Date
|
||||||
|
uptime: number
|
||||||
|
delay: number
|
||||||
}>
|
}>
|
||||||
className?: string
|
className?: string
|
||||||
title?: string
|
title?: string
|
||||||
@ -18,6 +21,25 @@ interface ServiceTrackerProps {
|
|||||||
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, className, title, uptime = 100, avgDelay = 0 }) => {
|
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, className, title, uptime = 100, avgDelay = 0 }) => {
|
||||||
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 getUptimeColor = (uptime: number) => {
|
||||||
|
if (uptime >= 99) return "text-emerald-500"
|
||||||
|
if (uptime >= 95) return "text-amber-500"
|
||||||
|
return "text-rose-500"
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDelayColor = (delay: number) => {
|
||||||
|
if (delay < 100) return "text-emerald-500"
|
||||||
|
if (delay < 300) return "text-amber-500"
|
||||||
|
return "text-rose-500"
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (uptime: number) => {
|
||||||
|
if (uptime >= 99) return "bg-emerald-500"
|
||||||
|
if (uptime >= 95) return "bg-amber-500"
|
||||||
|
return "bg-rose-500"
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -30,27 +52,58 @@ 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="w-5 h-5 rounded-full bg-green-600 flex items-center justify-center">
|
<div className={cn("w-2.5 h-2.5 rounded-full transition-colors", getStatusColor(uptime))} />
|
||||||
<div className="w-3 h-3 rounded-full bg-white dark:bg-black" />
|
|
||||||
</div>
|
|
||||||
<span className="font-medium text-sm">{title}</span>
|
<span className="font-medium text-sm">{title}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-stone-600 dark:text-stone-400 font-medium text-sm">{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 mx-0" orientation="vertical" />
|
<Separator className="h-4" orientation="vertical" />
|
||||||
<span className="text-green-600 font-medium text-sm">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-[2px]">
|
<div className="flex gap-[3px] bg-muted/30 p-1 rounded-lg">
|
||||||
{days.map((day, index) => (
|
{days.map((day, index) => (
|
||||||
<div
|
<TooltipProvider delayDuration={50} key={index}>
|
||||||
key={index}
|
<Tooltip>
|
||||||
className={cn("flex-1 h-6 rounded-[5px] transition-colors", day.completed ? "bg-green-600" : "bg-red-500/60")}
|
<TooltipTrigger asChild>
|
||||||
title={day.date ? day.date.toLocaleDateString() : `Day ${index + 1}`}
|
<div
|
||||||
/>
|
className={cn(
|
||||||
|
"relative flex-1 h-7 rounded-[4px] transition-all duration-200 cursor-help",
|
||||||
|
"before:absolute before:inset-0 before:rounded-[4px] before:opacity-0 hover:before:opacity-100 before:bg-white/10 before:transition-opacity",
|
||||||
|
"after:absolute after:inset-0 after:rounded-[4px] after:shadow-[inset_0_1px_theme(colors.white/10%)]",
|
||||||
|
day.completed
|
||||||
|
? "bg-gradient-to-b from-green-500/90 to-green-600 shadow-[0_1px_2px_theme(colors.green.600/30%)]"
|
||||||
|
: "bg-gradient-to-b from-red-500/80 to-red-600/90 shadow-[0_1px_2px_theme(colors.red.600/30%)]",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="p-0 overflow-hidden">
|
||||||
|
<div className="px-3 py-2 bg-popover">
|
||||||
|
<p className="font-medium text-sm mb-2">{day.date?.toLocaleDateString()}</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className="text-xs text-muted-foreground">{t("serviceTracker.uptime")}:</span>
|
||||||
|
<span className={cn("text-xs font-medium", 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
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
39
src/components/ThemeColorManager.tsx
Normal file
39
src/components/ThemeColorManager.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useTheme } from "@/hooks/use-theme"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
|
export function ThemeColorManager() {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateThemeColor = () => {
|
||||||
|
const currentTheme = theme
|
||||||
|
const meta = document.querySelector('meta[name="theme-color"]')
|
||||||
|
|
||||||
|
if (!meta) {
|
||||||
|
const newMeta = document.createElement("meta")
|
||||||
|
newMeta.name = "theme-color"
|
||||||
|
document.head.appendChild(newMeta)
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeColor =
|
||||||
|
currentTheme === "dark"
|
||||||
|
? "hsl(30 15% 8%)" // 深色模式背景色
|
||||||
|
: "hsl(0 0% 98%)" // 浅色模式背景色
|
||||||
|
|
||||||
|
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update on mount and theme change
|
||||||
|
updateThemeColor()
|
||||||
|
|
||||||
|
// Listen for system theme changes
|
||||||
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
mediaQuery.addEventListener("change", updateThemeColor)
|
||||||
|
|
||||||
|
return () => mediaQuery.removeEventListener("change", updateThemeColor)
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
126
src/components/ui/select.tsx
Normal file
126
src/components/ui/select.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
))
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
))
|
||||||
|
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
))
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label 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>>(
|
||||||
|
({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<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} />)
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
}
|
27
src/components/ui/tooltip.tsx
Normal file
27
src/components/ui/tooltip.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider
|
||||||
|
|
||||||
|
const Tooltip = TooltipPrimitive.Root
|
||||||
|
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 overflow-hidden rounded-[8px] border font-medium bg-popover px-1.5 py-0.5 text-xs text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
@ -4,6 +4,10 @@ declare global {
|
|||||||
interface Window {
|
interface Window {
|
||||||
CustomBackgroundImage: string
|
CustomBackgroundImage: string
|
||||||
CustomMobileBackgroundImage: string
|
CustomMobileBackgroundImage: string
|
||||||
|
ForceShowServices: boolean
|
||||||
|
ForceCardInline: boolean
|
||||||
|
ForceShowMap: boolean
|
||||||
|
ForcePeakCutEnabled: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +72,8 @@ export function getDaysBetweenDatesWithAutoRenewal({ autoRenewal, cycle, startDa
|
|||||||
months = 12
|
months = 12
|
||||||
break
|
break
|
||||||
case "季":
|
case "季":
|
||||||
|
case "q":
|
||||||
|
case "qr":
|
||||||
case "quarterly":
|
case "quarterly":
|
||||||
cycleLabel = "季"
|
cycleLabel = "季"
|
||||||
months = 3
|
months = 3
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
"serviceTracker": {
|
"serviceTracker": {
|
||||||
"noService": "No service data",
|
"noService": "No service data",
|
||||||
"uptime": "Uptime",
|
"uptime": "Uptime",
|
||||||
|
"delay": "Delay",
|
||||||
"daysAgo": "days ago",
|
"daysAgo": "days ago",
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
"loading": "Loading..."
|
"loading": "Loading..."
|
||||||
|
@ -44,8 +44,9 @@
|
|||||||
"nextUpdate": "下次更新"
|
"nextUpdate": "下次更新"
|
||||||
},
|
},
|
||||||
"serviceTracker": {
|
"serviceTracker": {
|
||||||
"noService": "没有服务监测数据",
|
"noService": "无服务数据",
|
||||||
"uptime": "可用率",
|
"uptime": "在线率",
|
||||||
|
"delay": "延迟",
|
||||||
"daysAgo": "天前",
|
"daysAgo": "天前",
|
||||||
"today": "今天",
|
"today": "今天",
|
||||||
"loading": "加载中..."
|
"loading": "加载中..."
|
||||||
|
@ -44,8 +44,9 @@
|
|||||||
"nextUpdate": "下次更新"
|
"nextUpdate": "下次更新"
|
||||||
},
|
},
|
||||||
"serviceTracker": {
|
"serviceTracker": {
|
||||||
"noService": "沒有服務監控數據",
|
"noService": "無服務數據",
|
||||||
"uptime": "可用率",
|
"uptime": "在線率",
|
||||||
|
"delay": "延遲",
|
||||||
"daysAgo": "天前",
|
"daysAgo": "天前",
|
||||||
"today": "今天",
|
"today": "今天",
|
||||||
"loading": "載入中..."
|
"loading": "載入中..."
|
||||||
|
@ -4,6 +4,7 @@ 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 { ThemeProvider } from "./components/ThemeProvider"
|
import { ThemeProvider } from "./components/ThemeProvider"
|
||||||
import { MotionProvider } from "./components/motion/motion-provider"
|
import { MotionProvider } from "./components/motion/motion-provider"
|
||||||
import { SortProvider } from "./context/sort-provider"
|
import { SortProvider } from "./context/sort-provider"
|
||||||
@ -18,6 +19,7 @@ const queryClient = new QueryClient()
|
|||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<MotionProvider>
|
<MotionProvider>
|
||||||
<ThemeProvider storageKey="vite-ui-theme">
|
<ThemeProvider storageKey="vite-ui-theme">
|
||||||
|
<ThemeColorManager />
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<WebSocketProvider url="/api/v1/ws/server">
|
<WebSocketProvider url="/api/v1/ws/server">
|
||||||
<StatusProvider>
|
<StatusProvider>
|
||||||
|
@ -7,6 +7,7 @@ import { ServiceTracker } from "@/components/ServiceTracker"
|
|||||||
import { Loader } from "@/components/loading/Loader"
|
import { Loader } from "@/components/loading/Loader"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
import { Popover, 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 { SORT_ORDERS, SORT_TYPES } from "@/context/sort-context"
|
||||||
import { useSort } from "@/hooks/use-sort"
|
import { useSort } from "@/hooks/use-sort"
|
||||||
import { useStatus } from "@/hooks/use-status"
|
import { useStatus } from "@/hooks/use-status"
|
||||||
@ -53,18 +54,31 @@ export default function Servers() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const showServicesState = localStorage.getItem("showServices")
|
const showServicesState = localStorage.getItem("showServices")
|
||||||
if (showServicesState !== null) {
|
if (window.ForceShowServices) {
|
||||||
|
setShowServices("1")
|
||||||
|
} else if (showServicesState !== null) {
|
||||||
setShowServices(showServicesState)
|
setShowServices(showServicesState)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const inlineState = localStorage.getItem("inline")
|
const inlineState = localStorage.getItem("inline")
|
||||||
if (inlineState !== null) {
|
if (window.ForceCardInline) {
|
||||||
|
setInline("1")
|
||||||
|
} else if (inlineState !== null) {
|
||||||
setInline(inlineState)
|
setInline(inlineState)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const showMapState = localStorage.getItem("showMap")
|
||||||
|
if (window.ForceShowMap) {
|
||||||
|
setShowMap("1")
|
||||||
|
} else if (showMapState !== null) {
|
||||||
|
setShowMap(showMapState)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedGroup = sessionStorage.getItem("selectedGroup") || "All"
|
const savedGroup = sessionStorage.getItem("selectedGroup") || "All"
|
||||||
setCurrentGroup(savedGroup)
|
setCurrentGroup(savedGroup)
|
||||||
@ -212,18 +226,24 @@ export default function Servers() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowMap(showMap === "0" ? "1" : "0")
|
setShowMap(showMap === "0" ? "1" : "0")
|
||||||
|
localStorage.setItem("showMap", showMap === "0" ? "1" : "0")
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
|
"rounded-[50px] bg-white dark:bg-stone-800 cursor-pointer p-[10px] transition-all border dark:border-none border-stone-200 dark:border-stone-700 hover:bg-stone-100 dark:hover:bg-stone-700 shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]",
|
||||||
{
|
{
|
||||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": showMap === "1",
|
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] !bg-blue-600 hover:!bg-blue-600 border-blue-600 dark:border-blue-600": showMap === "1",
|
||||||
|
"text-white": showMap === "1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bg-opacity-70": customBackgroundImage,
|
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MapIcon className="size-[13px]" />
|
<MapIcon
|
||||||
|
className={cn("size-[13px]", {
|
||||||
|
"text-white": showMap === "1",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -231,16 +251,21 @@ export default function Servers() {
|
|||||||
localStorage.setItem("showServices", showServices === "0" ? "1" : "0")
|
localStorage.setItem("showServices", showServices === "0" ? "1" : "0")
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
|
"rounded-[50px] bg-white dark:bg-stone-800 cursor-pointer p-[10px] transition-all border dark:border-none border-stone-200 dark:border-stone-700 hover:bg-stone-100 dark:hover:bg-stone-700 shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]",
|
||||||
{
|
{
|
||||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": showServices === "1",
|
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] !bg-blue-600 hover:!bg-blue-600 border-blue-600 dark:border-blue-600": showServices === "1",
|
||||||
|
"text-white": showServices === "1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bg-opacity-70": customBackgroundImage,
|
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ChartBarSquareIcon className="size-[13px]" />
|
<ChartBarSquareIcon
|
||||||
|
className={cn("size-[13px]", {
|
||||||
|
"text-white": showServices === "1",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -248,16 +273,21 @@ export default function Servers() {
|
|||||||
localStorage.setItem("inline", inline === "0" ? "1" : "0")
|
localStorage.setItem("inline", inline === "0" ? "1" : "0")
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
|
"rounded-[50px] bg-white dark:bg-stone-800 cursor-pointer p-[10px] transition-all border dark:border-none border-stone-200 dark:border-stone-700 hover:bg-stone-100 dark:hover:bg-stone-700 shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]",
|
||||||
{
|
{
|
||||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": inline === "1",
|
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] !bg-blue-600 hover:!bg-blue-600 border-blue-600 dark:border-blue-600": inline === "1",
|
||||||
|
"text-white": inline === "1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bg-opacity-70": customBackgroundImage,
|
"bg-opacity-70 dark:bg-opacity-70": customBackgroundImage,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ViewColumnsIcon className="size-[13px]" />
|
<ViewColumnsIcon
|
||||||
|
className={cn("size-[13px]", {
|
||||||
|
"text-white": inline === "1",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<GroupSwitch tabs={groupTabs} currentTab={currentGroup} setCurrentTab={handleTagChange} />
|
<GroupSwitch tabs={groupTabs} currentTab={currentGroup} setCurrentTab={handleTagChange} />
|
||||||
</section>
|
</section>
|
||||||
@ -265,7 +295,7 @@ export default function Servers() {
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<button
|
<button
|
||||||
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-stone-100 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,
|
||||||
},
|
},
|
||||||
@ -284,47 +314,38 @@ export default function Servers() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="py-2 px-2 w-fit max-w-60 rounded-[8px]">
|
<PopoverContent className="p-4 w-[240px] rounded-lg">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="space-y-4">
|
||||||
<section className="flex flex-col gap-1">
|
<div className="space-y-2">
|
||||||
<Label className=" text-stone-500 text-xs">Sort by</Label>
|
<Label className="text-xs font-medium text-muted-foreground">Sort by</Label>
|
||||||
<section className="flex items-center gap-1 flex-wrap">
|
<Select value={sortType} onValueChange={setSortType}>
|
||||||
{SORT_TYPES.map((type) => (
|
<SelectTrigger className="w-full text-xs h-8">
|
||||||
<button
|
<SelectValue placeholder="Choose type" />
|
||||||
key={type}
|
</SelectTrigger>
|
||||||
onClick={() => setSortType(type)}
|
<SelectContent>
|
||||||
className={cn(
|
{SORT_TYPES.map((type) => (
|
||||||
"rounded-[5px] text-[11px] w-fit px-1 py-0.5 cursor-pointer bg-transparent border transition-all dark:shadow-none ",
|
<SelectItem key={type} value={type} className="text-xs">
|
||||||
{
|
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||||
"bg-black text-white dark:bg-white dark:text-black shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]": sortType === type,
|
</SelectItem>
|
||||||
},
|
))}
|
||||||
)}
|
</SelectContent>
|
||||||
>
|
</Select>
|
||||||
{type}
|
</div>
|
||||||
</button>
|
<div className="space-y-2">
|
||||||
))}
|
<Label className="text-xs font-medium text-muted-foreground">Sort order</Label>
|
||||||
</section>
|
<Select value={sortOrder} onValueChange={setSortOrder} disabled={sortType === "default"}>
|
||||||
</section>
|
<SelectTrigger className="w-full text-xs h-8">
|
||||||
<section className="flex flex-col gap-1">
|
<SelectValue placeholder="Choose order" />
|
||||||
<Label className=" text-stone-500 text-xs">Sort order</Label>
|
</SelectTrigger>
|
||||||
<section className="flex items-center gap-1">
|
<SelectContent>
|
||||||
{SORT_ORDERS.map((order) => (
|
{SORT_ORDERS.map((order) => (
|
||||||
<button
|
<SelectItem key={order} value={order} className="text-xs">
|
||||||
disabled={sortType === "default"}
|
{order.charAt(0).toUpperCase() + order.slice(1)}
|
||||||
key={order}
|
</SelectItem>
|
||||||
onClick={() => setSortOrder(order)}
|
))}
|
||||||
className={cn(
|
</SelectContent>
|
||||||
"rounded-[5px] text-[11px] w-fit px-1 py-0.5 cursor-pointer bg-transparent border transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] dark:shadow-none",
|
</Select>
|
||||||
{
|
</div>
|
||||||
"bg-black text-white dark:bg-white dark:text-black": sortOrder === order && sortType !== "default",
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{order}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import react from "@vitejs/plugin-react-swc"
|
import react from "@vitejs/plugin-react-swc"
|
||||||
import { execSync } from "child_process"
|
import { execSync } from "child_process"
|
||||||
|
import fs from "fs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite"
|
||||||
|
|
||||||
@ -26,6 +27,10 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
|
https: {
|
||||||
|
key: fs.readFileSync("./.cert/key.pem"),
|
||||||
|
cert: fs.readFileSync("./.cert/cert.pem"),
|
||||||
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api/v1/ws/server": {
|
"/api/v1/ws/server": {
|
||||||
target: "ws://localhost:18009",
|
target: "ws://localhost:18009",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user