Compare commits

...

10 Commits

Author SHA1 Message Date
hamster1963
39c1eca1b0 docs: new image 2024-12-26 16:28:44 +08:00
hamster1963
83122ad867 fix: chart 2024-12-26 15:17:03 +08:00
hamster1963
93a90f4a33 v2.0.0 2024-12-26 14:42:57 +08:00
hamster1963
8a93bda5b6 feat: use biome 2024-12-26 14:36:51 +08:00
hamster1963
8575aee27e style: DropdownMenuItem rounded 2024-12-26 14:09:21 +08:00
hamster1963
4c7ffb509c feat: loading text 2024-12-26 14:01:53 +08:00
hamster1963
2a44cc7afb chore: deps 2024-12-26 13:48:37 +08:00
hamster1963
72472f7cc1 feat: detail chart history point 2024-12-26 13:48:19 +08:00
hamster1963
a9a7a367c0 fix: set dedupingInterval to 500 2024-12-26 13:22:01 +08:00
hamster1963
6b52a8dedb perf: refactor server data fetch 2024-12-26 13:20:49 +08:00
37 changed files with 417 additions and 146 deletions

View File

@ -1,8 +0,0 @@
{
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@next/next/no-img-element": "off",
"react-hooks/exhaustive-deps": "off"
}
}

BIN
.github/1-dark.webp vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

BIN
.github/1.webp vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

BIN
.github/2-dark.webp vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

BIN
.github/2.webp vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

BIN
.github/3-dark.webp vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

BIN
.github/3.webp vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

BIN
.github/4-dark.webp vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

BIN
.github/4.webp vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

BIN
.github/v2-1.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

BIN
.github/v2-2.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

BIN
.github/v2-3.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
.github/v2-4.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
.github/v2-dark.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

View File

@ -23,11 +23,8 @@ jobs:
- name: Install dependencies
run: bun install
- name: Run linter and fix issues
run: bun run lint:fix
- name: Run formatter
run: bun run format
- name: Run linter & formatter and fix issues
run: bun run check:fix
- name: Check for changes
id: check_changes

View File

@ -1,12 +0,0 @@
{
"semi": false,
"singleQuote": false,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "all",
"importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss", "@trivago/prettier-plugin-sort-imports"]
}

View File

@ -31,11 +31,8 @@
[环境变量介绍](https://nezhadash-docs.vercel.app/environment)
![screen](/.github/1.webp)
![screen](/.github/2.webp)
![screen](/.github/3.webp)
![screen](/.github/4.webp)
![screen](/.github/1-dark.webp)
![screen](/.github/2-dark.webp)
![screen](/.github/3-dark.webp)
![screen](/.github/4-dark.webp)
![screen](/.github/v2-1.webp)
![screen](/.github/v2-2.webp)
![screen](/.github/v2-3.webp)
![screen](/.github/v2-4.webp)
![screen](/.github/v2-dark.webp)

View File

@ -1,15 +1,15 @@
"use client"
import { NezhaAPISafe, ServerApi } from "@/app/types/nezha-api"
import { ServerDataWithTimestamp, useServerData } from "@/app/lib/server-data-context"
import { NezhaAPISafe } from "@/app/types/nezha-api"
import { ServerDetailChartLoading } from "@/components/loading/ServerDetailLoading"
import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar"
import { Card, CardContent } from "@/components/ui/card"
import { ChartConfig, ChartContainer } from "@/components/ui/chart"
import { formatBytes, formatNezhaInfo, formatRelativeTime, nezhaFetcher } from "@/lib/utils"
import { formatBytes, formatNezhaInfo, formatRelativeTime } from "@/lib/utils"
import { useTranslations } from "next-intl"
import { useEffect, useState } from "react"
import { useEffect, useRef, useState } from "react"
import { Area, AreaChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
import useSWRImmutable from "swr/immutable"
type cpuChartData = {
timeStamp: string
@ -52,16 +52,9 @@ export default function ServerDetailChartClient({
}) {
const t = useTranslations("ServerDetailChartClient")
const { data: allFallbackData } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
const fallbackData = allFallbackData?.result?.find((item) => item.id === server_id)
const { data: serverList, error, history } = useServerData()
const { data, error } = useSWRImmutable<NezhaAPISafe>(
`/api/detail?server_id=${server_id}`,
nezhaFetcher,
{
fallbackData,
},
)
const data = serverList?.result?.find((item) => item.id === server_id)
if (error) {
return (
@ -77,23 +70,48 @@ export default function ServerDetailChartClient({
return (
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
<CpuChart data={data} />
<ProcessChart data={data} />
<DiskChart data={data} />
<MemChart data={data} />
<NetworkChart data={data} />
<ConnectChart data={data} />
<CpuChart data={data} history={history} />
<ProcessChart data={data} history={history} />
<DiskChart data={data} history={history} />
<MemChart data={data} history={history} />
<NetworkChart data={data} history={history} />
<ConnectChart data={data} history={history} />
</section>
)
}
function CpuChart({ data }: { data: NezhaAPISafe }) {
function CpuChart({ history, data }: { history: ServerDataWithTimestamp[]; data: NezhaAPISafe }) {
const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[])
const hasInitialized = useRef(false)
const [historyLoaded, setHistoryLoaded] = useState(false)
useEffect(() => {
if (!hasInitialized.current && history.length > 0) {
const historyData = history
.map((msg) => {
const server = msg.data?.result?.find((item) => item.id === data.id)
if (!server) return null
const { cpu } = formatNezhaInfo(server)
return {
timeStamp: msg.timestamp.toString(),
cpu: cpu,
}
})
.filter((item): item is cpuChartData => item !== null)
.reverse() // 保持时间顺序
setCpuChartData(historyData)
hasInitialized.current = true
setHistoryLoaded(true)
} else if (history.length === 0) {
setHistoryLoaded(true)
}
}, [])
const { cpu } = formatNezhaInfo(data)
useEffect(() => {
if (data) {
if (data && historyLoaded) {
const timestamp = Date.now().toString()
let newData = [] as cpuChartData[]
if (cpuChartData.length === 0) {
@ -109,7 +127,7 @@ function CpuChart({ data }: { data: NezhaAPISafe }) {
}
setCpuChartData(newData)
}
}, [data])
}, [data, historyLoaded])
const chartConfig = {
cpu: {
@ -178,15 +196,45 @@ function CpuChart({ data }: { data: NezhaAPISafe }) {
)
}
function ProcessChart({ data }: { data: NezhaAPISafe }) {
function ProcessChart({
data,
history,
}: {
data: NezhaAPISafe
history: ServerDataWithTimestamp[]
}) {
const t = useTranslations("ServerDetailChartClient")
const [processChartData, setProcessChartData] = useState([] as processChartData[])
const hasInitialized = useRef(false)
const [historyLoaded, setHistoryLoaded] = useState(false)
useEffect(() => {
if (!hasInitialized.current && history.length > 0) {
const historyData = history
.map((msg) => {
const server = msg.data?.result?.find((item) => item.id === data.id)
if (!server) return null
const { process } = formatNezhaInfo(server)
return {
timeStamp: msg.timestamp.toString(),
process: process,
}
})
.filter((item): item is processChartData => item !== null)
.reverse()
setProcessChartData(historyData)
hasInitialized.current = true
setHistoryLoaded(true)
} else if (history.length === 0) {
setHistoryLoaded(true)
}
}, [])
const { process } = formatNezhaInfo(data)
useEffect(() => {
if (data) {
if (data && historyLoaded) {
const timestamp = Date.now().toString()
let newData = [] as processChartData[]
if (processChartData.length === 0) {
@ -202,7 +250,7 @@ function ProcessChart({ data }: { data: NezhaAPISafe }) {
}
setProcessChartData(newData)
}
}, [data])
}, [data, historyLoaded])
const chartConfig = {
process: {
@ -257,15 +305,40 @@ function ProcessChart({ data }: { data: NezhaAPISafe }) {
)
}
function MemChart({ data }: { data: NezhaAPISafe }) {
function MemChart({ data, history }: { data: NezhaAPISafe; history: ServerDataWithTimestamp[] }) {
const t = useTranslations("ServerDetailChartClient")
const [memChartData, setMemChartData] = useState([] as memChartData[])
const hasInitialized = useRef(false)
const [historyLoaded, setHistoryLoaded] = useState(false)
useEffect(() => {
if (!hasInitialized.current && history.length > 0) {
const historyData = history
.map((msg) => {
const server = msg.data?.result?.find((item) => item.id === data.id)
if (!server) return null
const { mem, swap } = formatNezhaInfo(server)
return {
timeStamp: msg.timestamp.toString(),
mem: mem,
swap: swap,
}
})
.filter((item): item is memChartData => item !== null)
.reverse()
setMemChartData(historyData)
hasInitialized.current = true
setHistoryLoaded(true)
} else if (history.length === 0) {
setHistoryLoaded(true)
}
}, [])
const { mem, swap } = formatNezhaInfo(data)
useEffect(() => {
if (data) {
if (data && historyLoaded) {
const timestamp = Date.now().toString()
let newData = [] as memChartData[]
if (memChartData.length === 0) {
@ -281,7 +354,7 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
}
setMemChartData(newData)
}
}, [data])
}, [data, historyLoaded])
const chartConfig = {
mem: {
@ -386,15 +459,39 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
)
}
function DiskChart({ data }: { data: NezhaAPISafe }) {
function DiskChart({ data, history }: { data: NezhaAPISafe; history: ServerDataWithTimestamp[] }) {
const t = useTranslations("ServerDetailChartClient")
const [diskChartData, setDiskChartData] = useState([] as diskChartData[])
const hasInitialized = useRef(false)
const [historyLoaded, setHistoryLoaded] = useState(false)
useEffect(() => {
if (!hasInitialized.current && history.length > 0) {
const historyData = history
.map((msg) => {
const server = msg.data?.result?.find((item) => item.id === data.id)
if (!server) return null
const { disk } = formatNezhaInfo(server)
return {
timeStamp: msg.timestamp.toString(),
disk: disk,
}
})
.filter((item): item is diskChartData => item !== null)
.reverse()
setDiskChartData(historyData)
hasInitialized.current = true
setHistoryLoaded(true)
} else if (history.length === 0) {
setHistoryLoaded(true)
}
}, [])
const { disk } = formatNezhaInfo(data)
useEffect(() => {
if (data) {
if (data && historyLoaded) {
const timestamp = Date.now().toString()
let newData = [] as diskChartData[]
if (diskChartData.length === 0) {
@ -410,7 +507,7 @@ function DiskChart({ data }: { data: NezhaAPISafe }) {
}
setDiskChartData(newData)
}
}, [data])
}, [data, historyLoaded])
const chartConfig = {
disk: {
@ -484,15 +581,46 @@ function DiskChart({ data }: { data: NezhaAPISafe }) {
)
}
function NetworkChart({ data }: { data: NezhaAPISafe }) {
function NetworkChart({
data,
history,
}: {
data: NezhaAPISafe
history: ServerDataWithTimestamp[]
}) {
const t = useTranslations("ServerDetailChartClient")
const [networkChartData, setNetworkChartData] = useState([] as networkChartData[])
const hasInitialized = useRef(false)
const [historyLoaded, setHistoryLoaded] = useState(false)
useEffect(() => {
if (!hasInitialized.current && history.length > 0) {
const historyData = history
.map((msg) => {
const server = msg.data?.result?.find((item) => item.id === data.id)
if (!server) return null
const { up, down } = formatNezhaInfo(server)
return {
timeStamp: msg.timestamp.toString(),
upload: up,
download: down,
}
})
.filter((item): item is networkChartData => item !== null)
.reverse()
setNetworkChartData(historyData)
hasInitialized.current = true
setHistoryLoaded(true)
} else if (history.length === 0) {
setHistoryLoaded(true)
}
}, [])
const { up, down } = formatNezhaInfo(data)
useEffect(() => {
if (data) {
if (data && historyLoaded) {
const timestamp = Date.now().toString()
let newData = [] as networkChartData[]
if (networkChartData.length === 0) {
@ -508,7 +636,7 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
}
setNetworkChartData(newData)
}
}, [data])
}, [data, historyLoaded])
let maxDownload = Math.max(...networkChartData.map((item) => item.download))
maxDownload = Math.ceil(maxDownload)
@ -602,13 +730,45 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
)
}
function ConnectChart({ data }: { data: NezhaAPISafe }) {
function ConnectChart({
data,
history,
}: {
data: NezhaAPISafe
history: ServerDataWithTimestamp[]
}) {
const [connectChartData, setConnectChartData] = useState([] as connectChartData[])
const hasInitialized = useRef(false)
const [historyLoaded, setHistoryLoaded] = useState(false)
useEffect(() => {
if (!hasInitialized.current && history.length > 0) {
const historyData = history
.map((msg) => {
const server = msg.data?.result?.find((item) => item.id === data.id)
if (!server) return null
const { tcp, udp } = formatNezhaInfo(server)
return {
timeStamp: msg.timestamp.toString(),
tcp: tcp,
udp: udp,
}
})
.filter((item): item is connectChartData => item !== null)
.reverse()
setConnectChartData(historyData)
hasInitialized.current = true
setHistoryLoaded(true)
} else if (history.length === 0) {
setHistoryLoaded(true)
}
}, [])
const { tcp, udp } = formatNezhaInfo(data)
useEffect(() => {
if (data) {
if (data && historyLoaded) {
const timestamp = Date.now().toString()
let newData = [] as connectChartData[]
if (connectChartData.length === 0) {
@ -624,7 +784,7 @@ function ConnectChart({ data }: { data: NezhaAPISafe }) {
}
setConnectChartData(newData)
}
}, [data])
}, [data, historyLoaded])
const chartConfig = {
tcp: {

View File

@ -1,18 +1,15 @@
"use client"
import { NezhaAPISafe, ServerApi } from "@/app/types/nezha-api"
import { useServerData } from "@/app/lib/server-data-context"
import { BackIcon } from "@/components/Icon"
import ServerFlag from "@/components/ServerFlag"
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
import getEnv from "@/lib/env-entry"
import { cn, formatBytes, nezhaFetcher } from "@/lib/utils"
import { cn, formatBytes } from "@/lib/utils"
import { useTranslations } from "next-intl"
import { notFound, useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import useSWR from "swr"
import useSWRImmutable from "swr/immutable"
export default function ServerDetailClient({ server_id }: { server_id: number }) {
const t = useTranslations("ServerDetailClient")
@ -39,24 +36,13 @@ export default function ServerDetailClient({ server_id }: { server_id: number })
}
}
const { data: allFallbackData, isLoading } = useSWRImmutable<ServerApi>(
"/api/server",
nezhaFetcher,
)
const fallbackData = allFallbackData?.result?.find((item) => item.id === server_id)
const { data: serverList, error, isLoading } = useServerData()
const data = serverList?.result?.find((item) => item.id === server_id)
if (!fallbackData && !isLoading) {
if (!data && !isLoading) {
notFound()
}
const { data, error } = useSWR<NezhaAPISafe>(`/api/detail?server_id=${server_id}`, nezhaFetcher, {
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000,
dedupingInterval: 1000,
fallbackData,
revalidateOnMount: false,
revalidateIfStale: false,
})
if (error) {
return (
<>

View File

@ -1,8 +1,6 @@
"use client"
import { ServerApi } from "@/app/types/nezha-api"
import { nezhaFetcher } from "@/lib/utils"
import useSWRImmutable from "swr/immutable"
import { useServerData } from "@/app/lib/server-data-context"
import GlobalLoading from "../../../../components/loading/GlobalLoading"
import { geoJsonString } from "../../../../lib/geo-json-string"
@ -11,7 +9,7 @@ import GlobalInfo from "./GlobalInfo"
import { InteractiveMap } from "./InteractiveMap"
export default function ServerGlobal() {
const { data: nezhaServerList, error } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
const { data: nezhaServerList, error } = useServerData()
if (error)
return (

View File

@ -1,18 +1,18 @@
"use client"
import { ServerApi } from "@/app/types/nezha-api"
import { useServerData } from "@/app/lib/server-data-context"
import ServerCard from "@/components/ServerCard"
import ServerCardInline from "@/components/ServerCardInline"
import Switch from "@/components/Switch"
import { Loader } from "@/components/loading/Loader"
import getEnv from "@/lib/env-entry"
import { useFilter } from "@/lib/network-filter-context"
import { useStatus } from "@/lib/status-context"
import { cn, nezhaFetcher } from "@/lib/utils"
import { cn } from "@/lib/utils"
import { MapIcon, ViewColumnsIcon } from "@heroicons/react/20/solid"
import { useTranslations } from "next-intl"
import dynamic from "next/dynamic"
import { useEffect, useRef, useState } from "react"
import useSWR from "swr"
import GlobalLoading from "../../../../components/loading/GlobalLoading"
@ -70,10 +70,7 @@ export default function ServerListClient() {
}
}, [])
const { data, error } = useSWR<ServerApi>("/api/server", nezhaFetcher, {
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000,
dedupingInterval: 1000,
})
const { data, error } = useServerData()
if (error)
return (
@ -83,7 +80,15 @@ export default function ServerListClient() {
</div>
)
if (!data?.result) return null
if (!data?.result)
return (
<div className="flex flex-col items-center min-h-96 justify-center ">
<div className="font-semibold flex items-center gap-2 text-sm">
<Loader visible={true} />
{t("connecting")}...
</div>
</div>
)
const { result } = data
const sortedServers = result.sort((a, b) => {

View File

@ -1,31 +1,30 @@
"use client"
import { ServerApi } from "@/app/types/nezha-api"
import { useServerData } from "@/app/lib/server-data-context"
import { Loader } from "@/components/loading/Loader"
import { Card, CardContent } from "@/components/ui/card"
import getEnv from "@/lib/env-entry"
import { useFilter } from "@/lib/network-filter-context"
import { useStatus } from "@/lib/status-context"
import { cn, formatBytes, nezhaFetcher } from "@/lib/utils"
import { cn, formatBytes } from "@/lib/utils"
import blogMan from "@/public/blog-man.webp"
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
import { useTranslations } from "next-intl"
import Image from "next/image"
import useSWRImmutable from "swr/immutable"
export default function ServerOverviewClient() {
const { data, error, isLoading } = useServerData()
const { status, setStatus } = useStatus()
const { filter, setFilter } = useFilter()
const t = useTranslations("ServerOverviewClient")
const { data, error, isLoading } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
const disableCartoon = getEnv("NEXT_PUBLIC_DisableCartoon") === "true"
if (error) {
const errorInfo = error as any
return (
<div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40">
Error status:{error.status} {error.info?.cause ?? error.message}
Error status:{errorInfo?.status} {errorInfo.info?.cause ?? errorInfo?.message}
</p>
<p className="text-sm font-medium opacity-40">{t("error_message")}</p>
</div>

View File

@ -1,5 +1,6 @@
import Footer from "@/app/(main)/footer"
import Header from "@/app/(main)/header"
import { ServerDataProvider } from "@/app/lib/server-data-context"
import { auth } from "@/auth"
import { SignIn } from "@/components/SignIn"
import getEnv from "@/lib/env-entry"
@ -13,7 +14,9 @@ export default function MainLayout({ children }: DashboardProps) {
<div className="flex min-h-screen w-full flex-col">
<main className="flex min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 bg-background p-4 md:p-10 md:pt-8">
<Header />
<AuthProtected>{children}</AuthProtected>
<AuthProtected>
<ServerDataProvider>{children}</ServerDataProvider>
</AuthProtected>
<Footer />
</main>
</div>

View File

@ -1,11 +1,11 @@
import fs from "fs"
import path from "path"
import { auth } from "@/auth"
import getEnv from "@/lib/env-entry"
import { GetServerIP } from "@/lib/serverFetch"
import fs from "fs"
import { AsnResponse, CityResponse, Reader } from "maxmind"
import { redirect } from "next/navigation"
import { NextRequest, NextResponse } from "next/server"
import path from "path"
export const dynamic = "force-dynamic"

View File

@ -0,0 +1,62 @@
"use client"
import { ServerApi } from "@/app/types/nezha-api"
import getEnv from "@/lib/env-entry"
import { nezhaFetcher } from "@/lib/utils"
import { ReactNode, createContext, useContext, useEffect, useState } from "react"
import useSWR from "swr"
export interface ServerDataWithTimestamp {
timestamp: number
data: ServerApi
}
interface ServerDataContextType {
data: ServerApi | undefined
error: Error | undefined
isLoading: boolean
history: ServerDataWithTimestamp[]
}
const ServerDataContext = createContext<ServerDataContextType | undefined>(undefined)
const MAX_HISTORY_LENGTH = 30
export function ServerDataProvider({ children }: { children: ReactNode }) {
const [history, setHistory] = useState<ServerDataWithTimestamp[]>([])
const { data, error, isLoading } = useSWR<ServerApi>("/api/server", nezhaFetcher, {
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000,
dedupingInterval: 1000,
})
useEffect(() => {
if (data) {
setHistory((prev) => {
const newHistory = [
{
timestamp: Date.now(),
data: data,
},
...prev,
].slice(0, MAX_HISTORY_LENGTH)
return newHistory
})
}
}, [data])
return (
<ServerDataContext.Provider value={{ data, error, isLoading, history }}>
{children}
</ServerDataContext.Provider>
)
}
export function useServerData() {
const context = useContext(ServerDataContext)
if (context === undefined) {
throw new Error("useServerData must be used within a ServerDataProvider")
}
return context
}

85
biome.json Normal file
View File

@ -0,0 +1,85 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
"files": { "ignoreUnknown": false, "ignore": [".next", "public"] },
"formatter": {
"enabled": true,
"useEditorconfig": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"attributePosition": "auto",
"bracketSpacing": true
},
"organizeImports": { "enabled": true },
"linter": {
"enabled": true,
"rules": {
"recommended": false,
"complexity": { "noUselessTypeConstraint": "error" },
"correctness": {
"noUnusedVariables": "error",
"useArrayLiterals": "off",
"useExhaustiveDependencies": "off"
},
"style": { "noNamespace": "error", "useAsConstAssertion": "error" },
"suspicious": {
"noExplicitAny": "off",
"noExtraNonNullAssertion": "error",
"noMisleadingInstantiator": "error",
"noUnsafeDeclarationMerging": "error",
"useNamespaceKeyword": "error"
}
}
},
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingCommas": "all",
"semicolons": "asNeeded",
"arrowParentheses": "always",
"bracketSameLine": false,
"quoteStyle": "double",
"attributePosition": "auto",
"bracketSpacing": true
}
},
"overrides": [
{
"include": ["*.ts", "*.tsx", "*.mts", "*.cts"],
"linter": {
"rules": {
"correctness": {
"noConstAssign": "off",
"noGlobalObjectCalls": "off",
"noInvalidBuiltinInstantiation": "off",
"noInvalidConstructorSuper": "off",
"noNewSymbol": "off",
"noSetterReturn": "off",
"noUndeclaredVariables": "off",
"noUnreachable": "off",
"noUnreachableSuper": "off"
},
"style": {
"noArguments": "error",
"noVar": "error",
"useConst": "error"
},
"suspicious": {
"noClassAssign": "off",
"noDuplicateClassMembers": "off",
"noDuplicateObjectKeys": "off",
"noDuplicateParameters": "off",
"noFunctionAssign": "off",
"noImportAssign": "off",
"noRedeclare": "off",
"noUnsafeNegation": "off",
"useGetterReturn": "off"
}
}
}
}
]
}

BIN
bun.lockb

Binary file not shown.

View File

@ -1,2 +0,0 @@
[install]
registry = "https://registry.npmmirror.com/"

View File

@ -9,6 +9,7 @@ import {
} from "@/components/ui/dropdown-menu"
import { localeItems } from "@/i18n-metadata"
import { setUserLocale } from "@/i18n/locale"
import { cn } from "@/lib/utils"
import { CheckCircleIcon } from "@heroicons/react/20/solid"
import { useLocale } from "next-intl"
import * as React from "react"
@ -34,11 +35,20 @@ export function LanguageSwitcher() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
{localeItems.map((item) => (
{localeItems.map((item, index) => (
<DropdownMenuItem
key={item.code}
onSelect={(e) => handleSelect(e, item.code)}
className={locale === item.code ? "bg-muted gap-3" : ""}
className={cn(
{
"bg-muted gap-3": locale === item.code,
},
{
"rounded-t-[5px]": index === localeItems.length - 1,
"rounded-[5px]": index !== 0 && index !== localeItems.length - 1,
"rounded-b-[5px]": index === 0,
},
)}
>
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem>

View File

@ -37,19 +37,19 @@ export function ModeToggle() {
</DropdownMenuTrigger>
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
<DropdownMenuItem
className={cn({ "gap-3 bg-muted": theme === "light" })}
className={cn("rounded-b-[5px]", { "gap-3 bg-muted": theme === "light" })}
onSelect={(e) => handleSelect(e, "light")}
>
{t("Light")} {theme === "light" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem>
<DropdownMenuItem
className={cn({ "gap-3 bg-muted": theme === "dark" })}
className={cn("rounded-[5px]", { "gap-3 bg-muted": theme === "dark" })}
onSelect={(e) => handleSelect(e, "dark")}
>
{t("Dark")} {theme === "dark" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem>
<DropdownMenuItem
className={cn({ "gap-3 bg-muted": theme === "system" })}
className={cn("rounded-t-[5px]", { "gap-3 bg-muted": theme === "system" })}
onSelect={(e) => handleSelect(e, "system")}
>
{t("System")} {theme === "system" && <CheckCircleIcon className="size-4" />}

View File

@ -62,7 +62,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover p-1.5 text-popover-foreground shadow-2xl dark:shadow-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl dark:shadow-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}

View File

@ -6,19 +6,6 @@ export const localeItems = [
{ code: "ja", name: "日本語" },
{ code: "zh-t", name: "中文繁體" },
{ code: "zh", name: "中文简体" },
//{code: 'ar', name: 'العربية'},
//{code: 'de', name: 'Deutsch'},
//{code: 'es', name: 'Español'},
//{code: 'fr', name: 'Français'},
//{code: 'hi', name: 'हिन्दी'},
//{code: 'id', name: 'Bahasa Indonesia'},
//{code: 'it', name: 'Italiano'},
//{code: 'ko', name: '한국어'},
//{code: 'ms', name: 'Bahasa Melayu'},
//{code: 'pt', name: 'Português'},
//{code: 'ru', name: 'Русский'},
//{code: 'th', name: 'ไทย'},
//{code: 'vi', name: 'Tiếng Việt'},
]
export const locales = localeItems.map((item) => item.code)

View File

@ -11,7 +11,8 @@
},
"ServerListClient": {
"error_message": "Please check your environment variables and review the server console",
"defaultTag": "All"
"defaultTag": "All",
"connecting": "Connecting"
},
"ServerCard": {
"System": "System",

View File

@ -11,7 +11,8 @@
},
"ServerListClient": {
"error_message": "環境変数を確認し、サーバーコンソールを確認してください",
"defaultTag": "すべて"
"defaultTag": "すべて",
"connecting": "接続中"
},
"ServerCard": {
"System": "システム",

View File

@ -11,7 +11,8 @@
},
"ServerListClient": {
"error_message": "請檢查您的環境變數並檢查伺服器控制台",
"defaultTag": "全部"
"defaultTag": "全部",
"connecting": "連接中"
},
"ServerCard": {
"System": "系統",

View File

@ -11,7 +11,8 @@
},
"ServerListClient": {
"error_message": "请检查您的环境变量并检查服务器控制台",
"defaultTag": "全部"
"defaultTag": "全部",
"connecting": "连接中"
},
"ServerCard": {
"System": "系统",

View File

@ -1,13 +1,15 @@
{
"name": "nezha-dash",
"version": "1.9.1",
"version": "2.0.0",
"private": true,
"scripts": {
"dev": "next dev -p 3040",
"start": "node .next/standalone/server.js",
"lint": "next lint",
"lint:fix": "next lint --fix",
"format": "prettier --write .",
"lint": "biome lint",
"lint:fix": "biome lint --fix",
"format": "biome format --write .",
"check": "biome check",
"check:fix": "biome check --fix",
"build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
"build-dev": "next build",
"start-dev": "next start"
@ -60,18 +62,16 @@
"typescript-eslint": "^8.18.2"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@next/bundle-analyzer": "^15.1.2",
"@tailwindcss/postcss": "^4.0.0-beta.8",
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"eslint": "^9.17.0",
"eslint-config-next": "^15.1.2",
"eslint-plugin-turbo": "^2.3.3",
"eslint-plugin-unused-imports": "^4.1.4",
"postcss": "^8.4.49",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.9",
"tailwindcss": "^4.0.0-beta.8",
"typescript": "^5.7.2",
"vercel": "^39.2.2"