Compare commits
No commits in common. "main" and "v2.0.0" have entirely different histories.
BIN
.github/1-dark.webp
vendored
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
.github/1.webp
vendored
Normal file
After Width: | Height: | Size: 139 KiB |
BIN
.github/2-dark.webp
vendored
Normal file
After Width: | Height: | Size: 328 KiB |
BIN
.github/2.webp
vendored
Normal file
After Width: | Height: | Size: 226 KiB |
BIN
.github/3-dark.webp
vendored
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
.github/3.webp
vendored
Normal file
After Width: | Height: | Size: 115 KiB |
BIN
.github/4-dark.webp
vendored
Normal file
After Width: | Height: | Size: 203 KiB |
BIN
.github/4.webp
vendored
Normal file
After Width: | Height: | Size: 135 KiB |
BIN
.github/v2-1.webp
vendored
Before Width: | Height: | Size: 185 KiB |
BIN
.github/v2-2.webp
vendored
Before Width: | Height: | Size: 141 KiB |
BIN
.github/v2-3.webp
vendored
Before Width: | Height: | Size: 126 KiB |
BIN
.github/v2-4.webp
vendored
Before Width: | Height: | Size: 142 KiB |
BIN
.github/v2-dark.webp
vendored
Before Width: | Height: | Size: 183 KiB |
2
.github/workflows/Deploy.yml
vendored
@ -1,6 +1,4 @@
|
|||||||
name: Build and push Docker image
|
name: Build and push Docker image
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
name: Auto Fix Lint and Format
|
name: Auto Fix Lint and Format
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request_target:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
15
README.md
@ -20,7 +20,7 @@
|
|||||||
- Cloudflare
|
- Cloudflare
|
||||||
- Docker
|
- Docker
|
||||||
|
|
||||||
[演示站点](https://nezha-vercel.vercel.app)
|
[演示站点](https://nezha-cf.buycoffee.top)
|
||||||
[说明文档](https://nezhadash-docs.vercel.app)
|
[说明文档](https://nezhadash-docs.vercel.app)
|
||||||
|
|
||||||
### 如何更新
|
### 如何更新
|
||||||
@ -31,8 +31,11 @@
|
|||||||
|
|
||||||
[环境变量介绍](https://nezhadash-docs.vercel.app/environment)
|
[环境变量介绍](https://nezhadash-docs.vercel.app/environment)
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import type { NezhaAPIMonitor, ServerMonitorChart } from "@/app/types/nezha-api"
|
import { NezhaAPIMonitor, ServerMonitorChart } from "@/app/types/nezha-api"
|
||||||
import NetworkChartLoading from "@/components/loading/NetworkChartLoading"
|
import NetworkChartLoading from "@/components/loading/NetworkChartLoading"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import {
|
import {
|
||||||
type ChartConfig,
|
ChartConfig,
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
ChartLegend,
|
ChartLegend,
|
||||||
ChartLegendContent,
|
ChartLegendContent,
|
||||||
@ -15,6 +15,7 @@ import { Label } from "@/components/ui/label"
|
|||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import { formatTime, nezhaFetcher } from "@/lib/utils"
|
import { formatTime, nezhaFetcher } from "@/lib/utils"
|
||||||
|
import { formatRelativeTime } from "@/lib/utils"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { useCallback, useMemo } from "react"
|
import { useCallback, useMemo } from "react"
|
||||||
@ -26,13 +27,7 @@ interface ResultItem {
|
|||||||
[key: string]: number
|
[key: string]: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NetworkChartClient({
|
export function NetworkChartClient({ server_id, show }: { server_id: number; show: boolean }) {
|
||||||
server_id,
|
|
||||||
show,
|
|
||||||
}: {
|
|
||||||
server_id: number
|
|
||||||
show: boolean
|
|
||||||
}) {
|
|
||||||
const t = useTranslations("NetworkChartClient")
|
const t = useTranslations("NetworkChartClient")
|
||||||
const { data, error } = useSWR<NezhaAPIMonitor[]>(
|
const { data, error } = useSWR<NezhaAPIMonitor[]>(
|
||||||
`/api/monitor?server_id=${server_id}`,
|
`/api/monitor?server_id=${server_id}`,
|
||||||
@ -47,8 +42,8 @@ export function NetworkChartClient({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<p className="font-medium text-sm opacity-40">{error.message}</p>
|
<p className="text-sm font-medium opacity-40">{error.message}</p>
|
||||||
<p className="font-medium text-sm opacity-40">{t("chart_fetch_error_message")}</p>
|
<p className="text-sm font-medium opacity-40">{t("chart_fetch_error_message")}</p>
|
||||||
</div>
|
</div>
|
||||||
<NetworkChartLoading />
|
<NetworkChartLoading />
|
||||||
</>
|
</>
|
||||||
@ -119,16 +114,13 @@ export const NetworkChart = React.memo(function NetworkChart({
|
|||||||
() =>
|
() =>
|
||||||
chartDataKey.map((key) => (
|
chartDataKey.map((key) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
|
||||||
key={key}
|
key={key}
|
||||||
data-active={activeChart === key}
|
data-active={activeChart === key}
|
||||||
className={
|
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`}
|
||||||
"relative z-30 flex grow basis-0 cursor-pointer flex-col justify-center gap-1 border-neutral-200 border-b px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-t-0 sm:border-l sm:px-6 dark:border-neutral-800"
|
|
||||||
}
|
|
||||||
onClick={() => handleButtonClick(key)}
|
onClick={() => handleButtonClick(key)}
|
||||||
>
|
>
|
||||||
<span className="whitespace-nowrap text-muted-foreground text-xs">{key}</span>
|
<span className="whitespace-nowrap text-xs text-muted-foreground">{key}</span>
|
||||||
<span className="font-bold text-md leading-none sm:text-lg">
|
<span className="text-md font-bold leading-none sm:text-lg">
|
||||||
{chartData[key][chartData[key].length - 1].avg_delay.toFixed(2)}ms
|
{chartData[key][chartData[key].length - 1].avg_delay.toFixed(2)}ms
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -218,7 +210,7 @@ export const NetworkChart = React.memo(function NetworkChart({
|
|||||||
const smoothed = { ...point } as ResultItem
|
const smoothed = { ...point } as ResultItem
|
||||||
|
|
||||||
if (activeChart === defaultChart) {
|
if (activeChart === defaultChart) {
|
||||||
for (const key of chartDataKey) {
|
chartDataKey.forEach((key) => {
|
||||||
const values = window
|
const values = window
|
||||||
.map((w) => w[key])
|
.map((w) => w[key])
|
||||||
.filter((v) => v !== undefined && v !== null) as number[]
|
.filter((v) => v !== undefined && v !== null) as number[]
|
||||||
@ -235,7 +227,7 @@ export const NetworkChart = React.memo(function NetworkChart({
|
|||||||
smoothed[key] = ewmaHistory[key]
|
smoothed[key] = ewmaHistory[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
} else {
|
} else {
|
||||||
const values = window
|
const values = window
|
||||||
.map((w) => w.avg_delay)
|
.map((w) => w.avg_delay)
|
||||||
@ -245,12 +237,12 @@ export const NetworkChart = React.memo(function NetworkChart({
|
|||||||
const processed = processValues(values)
|
const processed = processValues(values)
|
||||||
if (processed !== null) {
|
if (processed !== null) {
|
||||||
// 应用EWMA平滑
|
// 应用EWMA平滑
|
||||||
if (ewmaHistory.current === undefined) {
|
if (ewmaHistory["current"] === undefined) {
|
||||||
ewmaHistory.current = processed
|
ewmaHistory["current"] = processed
|
||||||
} else {
|
} else {
|
||||||
ewmaHistory.current = alpha * processed + (1 - alpha) * ewmaHistory.current
|
ewmaHistory["current"] = alpha * processed + (1 - alpha) * ewmaHistory["current"]
|
||||||
}
|
}
|
||||||
smoothed.avg_delay = ewmaHistory.current
|
smoothed.avg_delay = ewmaHistory["current"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -269,55 +261,27 @@ export const NetworkChart = React.memo(function NetworkChart({
|
|||||||
<CardDescription className="text-xs">
|
<CardDescription className="text-xs">
|
||||||
{chartDataKey.length} {t("ServerMonitorCount")}
|
{chartDataKey.length} {t("ServerMonitorCount")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
<div className="mt-0.5 flex items-center 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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-wrap">{chartButtons}</div>
|
<div className="flex flex-wrap w-full">{chartButtons}</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="py-4 pr-2 pl-0 sm:pt-6 sm:pr-6 sm:pb-6 sm:pl-2">
|
<CardContent className="pr-2 pl-0 py-4 sm:pt-6 sm:pb-6 sm:pr-6 sm:pl-2">
|
||||||
<ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full">
|
<ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full">
|
||||||
<LineChart accessibilityLayer data={processedData} margin={{ left: 12, right: 12 }}>
|
<LineChart accessibilityLayer data={processedData} margin={{ left: 12, right: 12 }}>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="created_at"
|
dataKey="created_at"
|
||||||
tickLine={true}
|
tickLine={false}
|
||||||
tickSize={3}
|
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickMargin={8}
|
tickMargin={8}
|
||||||
minTickGap={80}
|
minTickGap={32}
|
||||||
ticks={processedData
|
interval={"preserveStartEnd"}
|
||||||
.filter((item, index, array) => {
|
tickFormatter={(value) => formatRelativeTime(value)}
|
||||||
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
|
<YAxis
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
@ -350,7 +314,7 @@ export const NetworkChart = React.memo(function NetworkChart({
|
|||||||
const transformData = (data: NezhaAPIMonitor[]) => {
|
const transformData = (data: NezhaAPIMonitor[]) => {
|
||||||
const monitorData: ServerMonitorChart = {}
|
const monitorData: ServerMonitorChart = {}
|
||||||
|
|
||||||
for (const item of data) {
|
data.forEach((item) => {
|
||||||
const monitorName = item.monitor_name
|
const monitorName = item.monitor_name
|
||||||
|
|
||||||
if (!monitorData[monitorName]) {
|
if (!monitorData[monitorName]) {
|
||||||
@ -363,7 +327,7 @@ const transformData = (data: NezhaAPIMonitor[]) => {
|
|||||||
avg_delay: item.avg_delay[i],
|
avg_delay: item.avg_delay[i],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
return monitorData
|
return monitorData
|
||||||
}
|
}
|
||||||
@ -372,18 +336,16 @@ const formatData = (rawData: NezhaAPIMonitor[]) => {
|
|||||||
const result: { [time: number]: ResultItem } = {}
|
const result: { [time: number]: ResultItem } = {}
|
||||||
|
|
||||||
const allTimes = new Set<number>()
|
const allTimes = new Set<number>()
|
||||||
for (const item of rawData) {
|
rawData.forEach((item) => {
|
||||||
for (const time of item.created_at) {
|
item.created_at.forEach((time) => allTimes.add(time))
|
||||||
allTimes.add(time)
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const allTimeArray = Array.from(allTimes).sort((a, b) => a - b)
|
const allTimeArray = Array.from(allTimes).sort((a, b) => a - b)
|
||||||
|
|
||||||
for (const item of rawData) {
|
rawData.forEach((item) => {
|
||||||
const { monitor_name, created_at, avg_delay } = item
|
const { monitor_name, created_at, avg_delay } = item
|
||||||
|
|
||||||
for (const time of allTimeArray) {
|
allTimeArray.forEach((time) => {
|
||||||
if (!result[time]) {
|
if (!result[time]) {
|
||||||
result[time] = { created_at: time }
|
result[time] = { created_at: time }
|
||||||
}
|
}
|
||||||
@ -391,8 +353,8 @@ const formatData = (rawData: NezhaAPIMonitor[]) => {
|
|||||||
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
|
||||||
}
|
})
|
||||||
}
|
})
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import {
|
import { ServerDataWithTimestamp, useServerData } from "@/app/lib/server-data-context"
|
||||||
MAX_HISTORY_LENGTH,
|
import { NezhaAPISafe } from "@/app/types/nezha-api"
|
||||||
type ServerDataWithTimestamp,
|
|
||||||
useServerData,
|
|
||||||
} from "@/app/context/server-data-context"
|
|
||||||
import type { NezhaAPISafe } from "@/app/types/nezha-api"
|
|
||||||
import { ServerDetailChartLoading } from "@/components/loading/ServerDetailLoading"
|
import { ServerDetailChartLoading } from "@/components/loading/ServerDetailLoading"
|
||||||
import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar"
|
import AnimatedCircularProgressBar from "@/components/ui/animated-circular-progress-bar"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
|
import { ChartConfig, ChartContainer } from "@/components/ui/chart"
|
||||||
import { formatBytes, formatNezhaInfo, formatRelativeTime } from "@/lib/utils"
|
import { formatBytes, formatNezhaInfo, formatRelativeTime } from "@/lib/utils"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
@ -64,8 +60,8 @@ export default function ServerDetailChartClient({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<p className="font-medium text-sm opacity-40">{error.message}</p>
|
<p className="text-sm font-medium opacity-40">{error.message}</p>
|
||||||
<p className="font-medium text-sm opacity-40">{t("chart_fetch_error_message")}</p>
|
<p className="text-sm font-medium opacity-40">{t("chart_fetch_error_message")}</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@ -73,7 +69,7 @@ export default function ServerDetailChartClient({
|
|||||||
if (!data) return <ServerDetailChartLoading />
|
if (!data) return <ServerDetailChartLoading />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
|
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
|
||||||
<CpuChart data={data} history={history} />
|
<CpuChart data={data} history={history} />
|
||||||
<ProcessChart data={data} history={history} />
|
<ProcessChart data={data} history={history} />
|
||||||
<DiskChart data={data} history={history} />
|
<DiskChart data={data} history={history} />
|
||||||
@ -84,13 +80,7 @@ export default function ServerDetailChartClient({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CpuChart({
|
function CpuChart({ history, data }: { history: ServerDataWithTimestamp[]; data: NezhaAPISafe }) {
|
||||||
history,
|
|
||||||
data,
|
|
||||||
}: {
|
|
||||||
history: ServerDataWithTimestamp[]
|
|
||||||
data: NezhaAPISafe
|
|
||||||
}) {
|
|
||||||
const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[])
|
const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[])
|
||||||
const hasInitialized = useRef(false)
|
const hasInitialized = useRef(false)
|
||||||
const [historyLoaded, setHistoryLoaded] = useState(false)
|
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||||
@ -113,8 +103,6 @@ function CpuChart({
|
|||||||
setCpuChartData(historyData)
|
setCpuChartData(historyData)
|
||||||
hasInitialized.current = true
|
hasInitialized.current = true
|
||||||
setHistoryLoaded(true)
|
setHistoryLoaded(true)
|
||||||
} else if (history.length === 0) {
|
|
||||||
setHistoryLoaded(true)
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -132,7 +120,7 @@ function CpuChart({
|
|||||||
} else {
|
} else {
|
||||||
newData = [...cpuChartData, { timeStamp: timestamp, cpu: cpu }]
|
newData = [...cpuChartData, { timeStamp: timestamp, cpu: cpu }]
|
||||||
}
|
}
|
||||||
if (newData.length > MAX_HISTORY_LENGTH) {
|
if (newData.length > 30) {
|
||||||
newData.shift()
|
newData.shift()
|
||||||
}
|
}
|
||||||
setCpuChartData(newData)
|
setCpuChartData(newData)
|
||||||
@ -150,9 +138,9 @@ function CpuChart({
|
|||||||
<CardContent className="px-6 py-3">
|
<CardContent className="px-6 py-3">
|
||||||
<section className="flex flex-col gap-1">
|
<section className="flex flex-col gap-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="font-medium text-md">CPU</p>
|
<p className="text-md font-medium">CPU</p>
|
||||||
<section className="flex items-center gap-2">
|
<section className="flex items-center gap-2">
|
||||||
<p className="w-10 text-end font-medium text-xs">{cpu.toFixed(0)}%</p>
|
<p className="text-xs text-end w-10 font-medium">{cpu.toFixed(0)}%</p>
|
||||||
<AnimatedCircularProgressBar
|
<AnimatedCircularProgressBar
|
||||||
className="size-3 text-[0px]"
|
className="size-3 text-[0px]"
|
||||||
max={100}
|
max={100}
|
||||||
@ -236,8 +224,6 @@ function ProcessChart({
|
|||||||
setProcessChartData(historyData)
|
setProcessChartData(historyData)
|
||||||
hasInitialized.current = true
|
hasInitialized.current = true
|
||||||
setHistoryLoaded(true)
|
setHistoryLoaded(true)
|
||||||
} else if (history.length === 0) {
|
|
||||||
setHistoryLoaded(true)
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -255,7 +241,7 @@ function ProcessChart({
|
|||||||
} else {
|
} else {
|
||||||
newData = [...processChartData, { timeStamp: timestamp, process: process }]
|
newData = [...processChartData, { timeStamp: timestamp, process: process }]
|
||||||
}
|
}
|
||||||
if (newData.length > MAX_HISTORY_LENGTH) {
|
if (newData.length > 30) {
|
||||||
newData.shift()
|
newData.shift()
|
||||||
}
|
}
|
||||||
setProcessChartData(newData)
|
setProcessChartData(newData)
|
||||||
@ -273,9 +259,9 @@ function ProcessChart({
|
|||||||
<CardContent className="px-6 py-3">
|
<CardContent className="px-6 py-3">
|
||||||
<section className="flex flex-col gap-1">
|
<section className="flex flex-col gap-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="font-medium text-md">{t("Process")}</p>
|
<p className="text-md font-medium">{t("Process")}</p>
|
||||||
<section className="flex items-center gap-2">
|
<section className="flex items-center gap-2">
|
||||||
<p className="w-10 text-end font-medium text-xs">{process}</p>
|
<p className="text-xs text-end w-10 font-medium">{process}</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
|
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
|
||||||
@ -315,13 +301,7 @@ function ProcessChart({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function MemChart({
|
function MemChart({ data, history }: { data: NezhaAPISafe; history: ServerDataWithTimestamp[] }) {
|
||||||
data,
|
|
||||||
history,
|
|
||||||
}: {
|
|
||||||
data: NezhaAPISafe
|
|
||||||
history: ServerDataWithTimestamp[]
|
|
||||||
}) {
|
|
||||||
const t = useTranslations("ServerDetailChartClient")
|
const t = useTranslations("ServerDetailChartClient")
|
||||||
const [memChartData, setMemChartData] = useState([] as memChartData[])
|
const [memChartData, setMemChartData] = useState([] as memChartData[])
|
||||||
const hasInitialized = useRef(false)
|
const hasInitialized = useRef(false)
|
||||||
@ -346,8 +326,6 @@ function MemChart({
|
|||||||
setMemChartData(historyData)
|
setMemChartData(historyData)
|
||||||
hasInitialized.current = true
|
hasInitialized.current = true
|
||||||
setHistoryLoaded(true)
|
setHistoryLoaded(true)
|
||||||
} else if (history.length === 0) {
|
|
||||||
setHistoryLoaded(true)
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -365,7 +343,7 @@ function MemChart({
|
|||||||
} else {
|
} else {
|
||||||
newData = [...memChartData, { timeStamp: timestamp, mem: mem, swap: swap }]
|
newData = [...memChartData, { timeStamp: timestamp, mem: mem, swap: swap }]
|
||||||
}
|
}
|
||||||
if (newData.length > MAX_HISTORY_LENGTH) {
|
if (newData.length > 30) {
|
||||||
newData.shift()
|
newData.shift()
|
||||||
}
|
}
|
||||||
setMemChartData(newData)
|
setMemChartData(newData)
|
||||||
@ -388,7 +366,7 @@ function MemChart({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<section className="flex items-center gap-4">
|
<section className="flex items-center gap-4">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className=" text-muted-foreground text-xs">{t("Mem")}</p>
|
<p className=" text-xs text-muted-foreground">{t("Mem")}</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AnimatedCircularProgressBar
|
<AnimatedCircularProgressBar
|
||||||
className="size-3 text-[0px]"
|
className="size-3 text-[0px]"
|
||||||
@ -397,11 +375,11 @@ function MemChart({
|
|||||||
value={mem}
|
value={mem}
|
||||||
primaryColor="hsl(var(--chart-8))"
|
primaryColor="hsl(var(--chart-8))"
|
||||||
/>
|
/>
|
||||||
<p className="font-medium text-xs">{mem.toFixed(0)}%</p>
|
<p className="text-xs font-medium">{mem.toFixed(0)}%</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className=" text-muted-foreground text-xs">{t("Swap")}</p>
|
<p className=" text-xs text-muted-foreground">{t("Swap")}</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AnimatedCircularProgressBar
|
<AnimatedCircularProgressBar
|
||||||
className="size-3 text-[0px]"
|
className="size-3 text-[0px]"
|
||||||
@ -410,15 +388,15 @@ function MemChart({
|
|||||||
value={swap}
|
value={swap}
|
||||||
primaryColor="hsl(var(--chart-10))"
|
primaryColor="hsl(var(--chart-10))"
|
||||||
/>
|
/>
|
||||||
<p className="font-medium text-xs">{swap.toFixed(0)}%</p>
|
<p className="text-xs font-medium">{swap.toFixed(0)}%</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col items-end gap-0.5">
|
<section className="flex flex-col items-end gap-0.5">
|
||||||
<div className="flex items-center gap-2 font-medium text-[11px]">
|
<div className="flex text-[11px] font-medium items-center gap-2">
|
||||||
{formatBytes(data.status.MemUsed)} / {formatBytes(data.host.MemTotal)}
|
{formatBytes(data.status.MemUsed)} / {formatBytes(data.host.MemTotal)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 font-medium text-[11px]">
|
<div className="flex text-[11px] font-medium items-center gap-2">
|
||||||
swap: {formatBytes(data.status.SwapUsed)}
|
swap: {formatBytes(data.status.SwapUsed)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -475,13 +453,7 @@ function MemChart({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DiskChart({
|
function DiskChart({ data, history }: { data: NezhaAPISafe; history: ServerDataWithTimestamp[] }) {
|
||||||
data,
|
|
||||||
history,
|
|
||||||
}: {
|
|
||||||
data: NezhaAPISafe
|
|
||||||
history: ServerDataWithTimestamp[]
|
|
||||||
}) {
|
|
||||||
const t = useTranslations("ServerDetailChartClient")
|
const t = useTranslations("ServerDetailChartClient")
|
||||||
const [diskChartData, setDiskChartData] = useState([] as diskChartData[])
|
const [diskChartData, setDiskChartData] = useState([] as diskChartData[])
|
||||||
const hasInitialized = useRef(false)
|
const hasInitialized = useRef(false)
|
||||||
@ -505,8 +477,6 @@ function DiskChart({
|
|||||||
setDiskChartData(historyData)
|
setDiskChartData(historyData)
|
||||||
hasInitialized.current = true
|
hasInitialized.current = true
|
||||||
setHistoryLoaded(true)
|
setHistoryLoaded(true)
|
||||||
} else if (history.length === 0) {
|
|
||||||
setHistoryLoaded(true)
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -524,7 +494,7 @@ function DiskChart({
|
|||||||
} else {
|
} else {
|
||||||
newData = [...diskChartData, { timeStamp: timestamp, disk: disk }]
|
newData = [...diskChartData, { timeStamp: timestamp, disk: disk }]
|
||||||
}
|
}
|
||||||
if (newData.length > MAX_HISTORY_LENGTH) {
|
if (newData.length > 30) {
|
||||||
newData.shift()
|
newData.shift()
|
||||||
}
|
}
|
||||||
setDiskChartData(newData)
|
setDiskChartData(newData)
|
||||||
@ -542,10 +512,10 @@ function DiskChart({
|
|||||||
<CardContent className="px-6 py-3">
|
<CardContent className="px-6 py-3">
|
||||||
<section className="flex flex-col gap-1">
|
<section className="flex flex-col gap-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="font-medium text-md">{t("Disk")}</p>
|
<p className="text-md font-medium">{t("Disk")}</p>
|
||||||
<section className="flex flex-col items-end gap-0.5">
|
<section className="flex flex-col items-end gap-0.5">
|
||||||
<section className="flex items-center gap-2">
|
<section className="flex items-center gap-2">
|
||||||
<p className="w-10 text-end font-medium text-xs">{disk.toFixed(0)}%</p>
|
<p className="text-xs text-end w-10 font-medium">{disk.toFixed(0)}%</p>
|
||||||
<AnimatedCircularProgressBar
|
<AnimatedCircularProgressBar
|
||||||
className="size-3 text-[0px]"
|
className="size-3 text-[0px]"
|
||||||
max={100}
|
max={100}
|
||||||
@ -554,7 +524,7 @@ function DiskChart({
|
|||||||
primaryColor="hsl(var(--chart-5))"
|
primaryColor="hsl(var(--chart-5))"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<div className="flex items-center gap-2 font-medium text-[11px]">
|
<div className="flex text-[11px] font-medium items-center gap-2">
|
||||||
{formatBytes(data.status.DiskUsed)} / {formatBytes(data.host.DiskTotal)}
|
{formatBytes(data.status.DiskUsed)} / {formatBytes(data.host.DiskTotal)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -634,8 +604,6 @@ function NetworkChart({
|
|||||||
setNetworkChartData(historyData)
|
setNetworkChartData(historyData)
|
||||||
hasInitialized.current = true
|
hasInitialized.current = true
|
||||||
setHistoryLoaded(true)
|
setHistoryLoaded(true)
|
||||||
} else if (history.length === 0) {
|
|
||||||
setHistoryLoaded(true)
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -653,7 +621,7 @@ function NetworkChart({
|
|||||||
} else {
|
} else {
|
||||||
newData = [...networkChartData, { timeStamp: timestamp, upload: up, download: down }]
|
newData = [...networkChartData, { timeStamp: timestamp, upload: up, download: down }]
|
||||||
}
|
}
|
||||||
if (newData.length > MAX_HISTORY_LENGTH) {
|
if (newData.length > 30) {
|
||||||
newData.shift()
|
newData.shift()
|
||||||
}
|
}
|
||||||
setNetworkChartData(newData)
|
setNetworkChartData(newData)
|
||||||
@ -681,18 +649,18 @@ function NetworkChart({
|
|||||||
<section className="flex flex-col gap-1">
|
<section className="flex flex-col gap-1">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<section className="flex items-center gap-4">
|
<section className="flex items-center gap-4">
|
||||||
<div className="flex w-20 flex-col">
|
<div className="flex flex-col w-20">
|
||||||
<p className="text-muted-foreground text-xs">{t("Upload")}</p>
|
<p className="text-xs text-muted-foreground">{t("Upload")}</p>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]" />
|
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]"></span>
|
||||||
<p className="font-medium text-xs">{up.toFixed(2)} M/s</p>
|
<p className="text-xs font-medium">{up.toFixed(2)} M/s</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-20 flex-col">
|
<div className="flex flex-col w-20">
|
||||||
<p className=" text-muted-foreground text-xs">{t("Download")}</p>
|
<p className=" text-xs text-muted-foreground">{t("Download")}</p>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]" />
|
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]"></span>
|
||||||
<p className="font-medium text-xs">{down.toFixed(2)} M/s</p>
|
<p className="text-xs font-medium">{down.toFixed(2)} M/s</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -782,8 +750,6 @@ function ConnectChart({
|
|||||||
setConnectChartData(historyData)
|
setConnectChartData(historyData)
|
||||||
hasInitialized.current = true
|
hasInitialized.current = true
|
||||||
setHistoryLoaded(true)
|
setHistoryLoaded(true)
|
||||||
} else if (history.length === 0) {
|
|
||||||
setHistoryLoaded(true)
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -801,7 +767,7 @@ function ConnectChart({
|
|||||||
} else {
|
} else {
|
||||||
newData = [...connectChartData, { timeStamp: timestamp, tcp: tcp, udp: udp }]
|
newData = [...connectChartData, { timeStamp: timestamp, tcp: tcp, udp: udp }]
|
||||||
}
|
}
|
||||||
if (newData.length > MAX_HISTORY_LENGTH) {
|
if (newData.length > 30) {
|
||||||
newData.shift()
|
newData.shift()
|
||||||
}
|
}
|
||||||
setConnectChartData(newData)
|
setConnectChartData(newData)
|
||||||
@ -823,18 +789,18 @@ function ConnectChart({
|
|||||||
<section className="flex flex-col gap-1">
|
<section className="flex flex-col gap-1">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<section className="flex items-center gap-4">
|
<section className="flex items-center gap-4">
|
||||||
<div className="flex w-12 flex-col">
|
<div className="flex flex-col w-12">
|
||||||
<p className="text-muted-foreground text-xs">TCP</p>
|
<p className="text-xs text-muted-foreground">TCP</p>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]" />
|
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]"></span>
|
||||||
<p className="font-medium text-xs">{tcp}</p>
|
<p className="text-xs font-medium">{tcp}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-12 flex-col">
|
<div className="flex flex-col w-12">
|
||||||
<p className=" text-muted-foreground text-xs">UDP</p>
|
<p className=" text-xs text-muted-foreground">UDP</p>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]" />
|
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]"></span>
|
||||||
<p className="font-medium text-xs">{udp}</p>
|
<p className="text-xs font-medium">{udp}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -1,25 +1,17 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useServerData } from "@/app/context/server-data-context"
|
import { useServerData } from "@/app/lib/server-data-context"
|
||||||
import { BackIcon } from "@/components/Icon"
|
import { BackIcon } from "@/components/Icon"
|
||||||
import ServerFlag from "@/components/ServerFlag"
|
import ServerFlag from "@/components/ServerFlag"
|
||||||
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"
|
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { cn, formatBytes, formatNezhaInfo } from "@/lib/utils"
|
import { cn, formatBytes } from "@/lib/utils"
|
||||||
import countries from "i18n-iso-countries"
|
|
||||||
import enLocale from "i18n-iso-countries/langs/en.json"
|
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { notFound, useRouter } from "next/navigation"
|
import { notFound, useRouter } from "next/navigation"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
countries.registerLocale(enLocale)
|
export default function ServerDetailClient({ server_id }: { server_id: number }) {
|
||||||
|
|
||||||
export default function ServerDetailClient({
|
|
||||||
server_id,
|
|
||||||
}: {
|
|
||||||
server_id: number
|
|
||||||
}) {
|
|
||||||
const t = useTranslations("ServerDetailClient")
|
const t = useTranslations("ServerDetailClient")
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@ -40,14 +32,14 @@ export default function ServerDetailClient({
|
|||||||
if (hasHistory) {
|
if (hasHistory) {
|
||||||
router.back()
|
router.back()
|
||||||
} else {
|
} else {
|
||||||
router.push("/")
|
router.push(`/`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: serverList, error, isLoading } = useServerData()
|
const { data: serverList, error, isLoading } = useServerData()
|
||||||
const serverData = serverList?.result?.find((item) => item.id === server_id)
|
const data = serverList?.result?.find((item) => item.id === server_id)
|
||||||
|
|
||||||
if (!serverData && !isLoading) {
|
if (!data && !isLoading) {
|
||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,195 +47,175 @@ export default function ServerDetailClient({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<p className="font-medium text-sm opacity-40">{error.message}</p>
|
<p className="text-sm font-medium opacity-40">{error.message}</p>
|
||||||
<p className="font-medium text-sm opacity-40">{t("detail_fetch_error_message")}</p>
|
<p className="text-sm font-medium opacity-40">{t("detail_fetch_error_message")}</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!serverData) return <ServerDetailLoading />
|
if (!data) return <ServerDetailLoading />
|
||||||
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
online,
|
|
||||||
uptime,
|
|
||||||
version,
|
|
||||||
arch,
|
|
||||||
mem_total,
|
|
||||||
disk_total,
|
|
||||||
country_code,
|
|
||||||
platform,
|
|
||||||
platform_version,
|
|
||||||
cpu_info,
|
|
||||||
gpu_info,
|
|
||||||
load_1,
|
|
||||||
load_5,
|
|
||||||
load_15,
|
|
||||||
net_out_transfer,
|
|
||||||
net_in_transfer,
|
|
||||||
last_active_time_string,
|
|
||||||
boot_time_string,
|
|
||||||
} = formatNezhaInfo(serverData)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
onClick={linkClick}
|
onClick={linkClick}
|
||||||
className="flex flex-none cursor-pointer items-center gap-0.5 break-all font-semibold text-xl leading-none tracking-tight transition-opacity duration-300 hover:opacity-50"
|
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
|
||||||
>
|
>
|
||||||
<BackIcon />
|
<BackIcon />
|
||||||
{name}
|
{data?.name}
|
||||||
</div>
|
</div>
|
||||||
<section className="mt-3 flex flex-wrap gap-2">
|
<section className="flex flex-wrap gap-2 mt-3">
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("status")}</p>
|
<p className="text-xs text-muted-foreground">{t("status")}</p>
|
||||||
<Badge
|
<Badge
|
||||||
className={cn(
|
className={cn(
|
||||||
"-mt-[0.3px] w-fit rounded-[6px] px-1 py-0 text-[9px] dark:text-white",
|
"text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white",
|
||||||
{
|
{
|
||||||
" bg-green-800": online,
|
" bg-green-800": data?.online_status,
|
||||||
" bg-red-600": !online,
|
" bg-red-600": !data?.online_status,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{online ? t("Online") : t("Offline")}
|
{data?.online_status ? t("Online") : t("Offline")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("Uptime")}</p>
|
<p className="text-xs text-muted-foreground">{t("Uptime")}</p>
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
{" "}
|
{" "}
|
||||||
{uptime / 86400 >= 1
|
{data?.status.Uptime / 86400 >= 1
|
||||||
? `${Math.floor(uptime / 86400)} ${t("Days")} ${Math.floor((uptime % 86400) / 3600)} ${t("Hours")}`
|
? (data?.status.Uptime / 86400).toFixed(0) + " " + t("Days")
|
||||||
: `${Math.floor(uptime / 3600)} ${t("Hours")}`}
|
: (data?.status.Uptime / 3600).toFixed(0) + " " + t("Hours")}{" "}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
{version && (
|
{data?.host.Version && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("Version")}</p>
|
<p className="text-xs text-muted-foreground">{t("Version")}</p>
|
||||||
<div className="text-xs">{version} </div>
|
<div className="text-xs">{data?.host.Version} </div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{arch && (
|
{data?.host.Arch && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("Arch")}</p>
|
<p className="text-xs text-muted-foreground">{t("Arch")}</p>
|
||||||
<div className="text-xs">{arch} </div>
|
<div className="text-xs">{data?.host.Arch} </div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("Mem")}</p>
|
<p className="text-xs text-muted-foreground">{t("Mem")}</p>
|
||||||
<div className="text-xs">{formatBytes(mem_total)}</div>
|
<div className="text-xs">{formatBytes(data?.host.MemTotal)}</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("Disk")}</p>
|
<p className="text-xs text-muted-foreground">{t("Disk")}</p>
|
||||||
<div className="text-xs">{formatBytes(disk_total)}</div>
|
<div className="text-xs">{formatBytes(data?.host.DiskTotal)}</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
{country_code && (
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("Region")}</p>
|
<p className="text-xs text-muted-foreground">{t("Region")}</p>
|
||||||
<section className="flex items-start gap-1">
|
<section className="flex items-start gap-1">
|
||||||
<div className="text-start text-xs">{countries.getName(country_code, "en")}</div>
|
<div className="text-xs text-start">{data?.host.CountryCode.toUpperCase()}</div>
|
||||||
<ServerFlag className="-mt-[1px] text-[11px]" country_code={country_code} />
|
<ServerFlag
|
||||||
|
className="text-[11px] -mt-[1px]"
|
||||||
|
country_code={data?.host.CountryCode}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
<section className="mt-1 flex flex-wrap gap-2">
|
<section className="flex flex-wrap gap-2 mt-1">
|
||||||
{platform && (
|
{data?.host.Platform && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("System")}</p>
|
<p className="text-xs text-muted-foreground">{t("System")}</p>
|
||||||
|
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
{" "}
|
{" "}
|
||||||
{platform} - {platform_version}{" "}
|
{data?.host.Platform} - {data?.host.PlatformVersion}{" "}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{cpu_info && cpu_info.length > 0 && (
|
{data?.host.CPU && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("CPU")}</p>
|
<p className="text-xs text-muted-foreground">{t("CPU")}</p>
|
||||||
|
|
||||||
<div className="text-xs"> {cpu_info.join(", ")}</div>
|
<div className="text-xs"> {data?.host.CPU.join(", ")}</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{gpu_info && gpu_info.length > 0 && (
|
{data?.host.GPU && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{"GPU"}</p>
|
<p className="text-xs text-muted-foreground">{"GPU"}</p>
|
||||||
<div className="text-xs"> {gpu_info.join(", ")}</div>
|
<div className="text-xs"> {data?.host.GPU.join(", ")}</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
<section className="mt-1 flex flex-wrap gap-2">
|
<section className="flex flex-wrap gap-2 mt-1">
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("Load")}</p>
|
<p className="text-xs text-muted-foreground">{t("Load")}</p>
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
{load_1 || "0.00"} / {load_5 || "0.00"} / {load_15 || "0.00"}
|
{data.status.Load1.toFixed(2) || "0.00"} / {data.status.Load5.toFixed(2) || "0.00"}{" "}
|
||||||
|
/ {data.status.Load15.toFixed(2) || "0.00"}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("Upload")}</p>
|
<p className="text-xs text-muted-foreground">{t("Upload")}</p>
|
||||||
{net_out_transfer ? (
|
{data.status.NetOutTransfer ? (
|
||||||
<div className="text-xs"> {formatBytes(net_out_transfer)} </div>
|
<div className="text-xs"> {formatBytes(data.status.NetOutTransfer)} </div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs">Unknown</div>
|
<div className="text-xs">Unknown</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("Download")}</p>
|
<p className="text-xs text-muted-foreground">{t("Download")}</p>
|
||||||
{net_in_transfer ? (
|
{data.status.NetInTransfer ? (
|
||||||
<div className="text-xs"> {formatBytes(net_in_transfer)} </div>
|
<div className="text-xs"> {formatBytes(data.status.NetInTransfer)} </div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs">Unknown</div>
|
<div className="text-xs">Unknown</div>
|
||||||
)}
|
)}
|
||||||
@ -251,26 +223,6 @@ export default function ServerDetailClient({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
<section className="mt-1 flex flex-wrap gap-2">
|
|
||||||
<Card className="rounded-[10px] border-none bg-transparent shadow-none">
|
|
||||||
<CardContent className="px-1.5 py-1">
|
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
|
||||||
<p className="text-muted-foreground text-xs">{t("BootTime")}</p>
|
|
||||||
<div className="text-xs">{boot_time_string ? boot_time_string : "N/A"}</div>
|
|
||||||
</section>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="rounded-[10px] border-none bg-transparent shadow-none">
|
|
||||||
<CardContent className="px-1.5 py-1">
|
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
|
||||||
<p className="text-muted-foreground text-xs">{t("LastActive")}</p>
|
|
||||||
<div className="text-xs">
|
|
||||||
{last_active_time_string ? last_active_time_string : "N/A"}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import type { IPInfo } from "@/app/api/server-ip/route"
|
import { IPInfo } from "@/app/api/server-ip/route"
|
||||||
import { Loader } from "@/components/loading/Loader"
|
import { Loader } from "@/components/loading/Loader"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { nezhaFetcher } from "@/lib/utils"
|
import { nezhaFetcher } from "@/lib/utils"
|
||||||
@ -22,92 +22,92 @@ export default function ServerIPInfo({ server_id }: { server_id: number }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="mb-4 flex flex-wrap gap-2">
|
<section className="flex flex-wrap gap-2 mb-4">
|
||||||
{data.asn?.autonomous_system_organization && (
|
{data.asn?.autonomous_system_organization && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{"ASN"}</p>
|
<p className="text-xs text-muted-foreground">{"ASN"}</p>
|
||||||
<div className="text-xs">{data.asn.autonomous_system_organization}</div>
|
<div className="text-xs">{data.asn.autonomous_system_organization}</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{data.asn?.autonomous_system_number && (
|
{data.asn?.autonomous_system_number && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("asn_number")}</p>
|
<p className="text-xs text-muted-foreground">{t("asn_number")}</p>
|
||||||
<div className="text-xs">AS{data.asn.autonomous_system_number}</div>
|
<div className="text-xs">AS{data.asn.autonomous_system_number}</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{data.city?.registered_country?.names.en && (
|
{data.city?.registered_country?.names.en && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("registered_country")}</p>
|
<p className="text-xs text-muted-foreground">{t("registered_country")}</p>
|
||||||
<div className="text-xs">{data.city.registered_country?.names.en}</div>
|
<div className="text-xs">{data.city.registered_country?.names.en}</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{data.city?.country?.iso_code && (
|
{data.city?.country?.iso_code && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{"ISO"}</p>
|
<p className="text-xs text-muted-foreground">{"ISO"}</p>
|
||||||
<div className="text-xs">{data.city.country?.iso_code}</div>
|
<div className="text-xs">{data.city.country?.iso_code}</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{data.city?.city?.names.en && (
|
{data.city?.city?.names.en && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("city")}</p>
|
<p className="text-xs text-muted-foreground">{t("city")}</p>
|
||||||
<div className="text-xs">{data.city.city?.names.en}</div>
|
<div className="text-xs">{data.city.city?.names.en}</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{data.city?.location?.longitude && (
|
{data.city?.location?.longitude && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("longitude")}</p>
|
<p className="text-xs text-muted-foreground">{t("longitude")}</p>
|
||||||
<div className="text-xs">{data.city.location?.longitude}</div>
|
<div className="text-xs">{data.city.location?.longitude}</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{data.city?.location?.latitude && (
|
{data.city?.location?.latitude && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("latitude")}</p>
|
<p className="text-xs text-muted-foreground">{t("latitude")}</p>
|
||||||
<div className="text-xs">{data.city.location?.latitude}</div>
|
<div className="text-xs">{data.city.location?.latitude}</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{data.city?.location?.time_zone && (
|
{data.city?.location?.time_zone && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("time_zone")}</p>
|
<p className="text-xs text-muted-foreground">{t("time_zone")}</p>
|
||||||
<div className="text-xs">{data.city.location?.time_zone}</div>
|
<div className="text-xs">{data.city.location?.time_zone}</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
{data.city?.postal && (
|
{data.city?.postal && (
|
||||||
<Card className="rounded-[10px] border-none bg-transparent 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-muted-foreground text-xs">{t("postal_code")}</p>
|
<p className="text-xs text-muted-foreground">{t("postal_code")}</p>
|
||||||
<div className="text-xs">{data.city.postal?.code}</div>
|
<div className="text-xs">{data.city.postal?.code}</div>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { type ReactNode, createContext, useContext, useState } from "react"
|
import { ReactNode, createContext, useContext, useState } 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: string
|
|
||||||
name: string
|
name: string
|
||||||
status: boolean
|
status: boolean
|
||||||
}>
|
}>
|
@ -1,11 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import GlobalInfo from "@/app/(main)/ClientComponents/main/GlobalInfo"
|
import { useServerData } from "@/app/lib/server-data-context"
|
||||||
import { InteractiveMap } from "@/app/(main)/ClientComponents/main/InteractiveMap"
|
|
||||||
import { useServerData } from "@/app/context/server-data-context"
|
import GlobalLoading from "../../../../components/loading/GlobalLoading"
|
||||||
import { TooltipProvider } from "@/app/context/tooltip-context"
|
import { geoJsonString } from "../../../../lib/geo-json-string"
|
||||||
import GlobalLoading from "@/components/loading/GlobalLoading"
|
import { TooltipProvider } from "../detail/TooltipContext"
|
||||||
import { geoJsonString } from "@/lib/geo/geo-json-string"
|
import GlobalInfo from "./GlobalInfo"
|
||||||
|
import { InteractiveMap } from "./InteractiveMap"
|
||||||
|
|
||||||
export default function ServerGlobal() {
|
export default function ServerGlobal() {
|
||||||
const { data: nezhaServerList, error } = useServerData()
|
const { data: nezhaServerList, error } = useServerData()
|
||||||
@ -13,7 +14,7 @@ export default function ServerGlobal() {
|
|||||||
if (error)
|
if (error)
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<p className="font-medium text-sm opacity-40">{error.message}</p>
|
<p className="text-sm font-medium opacity-40">{error.message}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ export default function ServerGlobal() {
|
|||||||
const countryList: string[] = []
|
const countryList: string[] = []
|
||||||
const serverCounts: { [key: string]: number } = {}
|
const serverCounts: { [key: string]: number } = {}
|
||||||
|
|
||||||
for (const server of nezhaServerList.result) {
|
nezhaServerList.result.forEach((server) => {
|
||||||
if (server.host.CountryCode) {
|
if (server.host.CountryCode) {
|
||||||
const countryCode = server.host.CountryCode.toUpperCase()
|
const countryCode = server.host.CountryCode.toUpperCase()
|
||||||
if (!countryList.includes(countryCode)) {
|
if (!countryList.includes(countryCode)) {
|
||||||
@ -32,7 +33,7 @@ export default function ServerGlobal() {
|
|||||||
}
|
}
|
||||||
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1
|
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
const width = 900
|
const width = 900
|
||||||
const height = 500
|
const height = 500
|
||||||
@ -43,7 +44,7 @@ export default function ServerGlobal() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="mt-[3.2px] flex flex-col gap-4">
|
<section className="flex flex-col gap-4 mt-[3.2px]">
|
||||||
<GlobalInfo countries={countryList} />
|
<GlobalInfo countries={countryList} />
|
||||||
<div className="w-full overflow-x-auto">
|
<div className="w-full overflow-x-auto">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
|
@ -10,7 +10,7 @@ export default function GlobalInfo({ countries }: GlobalInfoProps) {
|
|||||||
const t = useTranslations("Global")
|
const t = useTranslations("Global")
|
||||||
return (
|
return (
|
||||||
<section className="flex items-center justify-between">
|
<section className="flex items-center justify-between">
|
||||||
<p className="font-medium text-sm opacity-40">
|
<p className="text-sm font-medium opacity-40">
|
||||||
{t("Distributions")} {countries.length} {t("Regions")}
|
{t("Distributions")} {countries.length} {t("Regions")}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import MapTooltip from "@/app/(main)/ClientComponents/main/MapTooltip"
|
import { countryCoordinates } from "@/lib/geo-limit"
|
||||||
import { useTooltip } from "@/app/context/tooltip-context"
|
|
||||||
import { countryCoordinates } from "@/lib/geo/geo-limit"
|
|
||||||
import { geoEquirectangular, geoPath } from "d3-geo"
|
import { geoEquirectangular, geoPath } from "d3-geo"
|
||||||
|
|
||||||
|
import { useTooltip } from "../detail/TooltipContext"
|
||||||
|
import MapTooltip from "./MapTooltip"
|
||||||
|
|
||||||
interface InteractiveMapProps {
|
interface InteractiveMapProps {
|
||||||
countries: string[]
|
countries: string[]
|
||||||
serverCounts: { [key: string]: number }
|
serverCounts: { [key: string]: number }
|
||||||
@ -32,15 +33,14 @@ export function InteractiveMap({
|
|||||||
const path = geoPath().projection(projection)
|
const path = geoPath().projection(projection)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative aspect-[2/1] w-full" onMouseLeave={() => setTooltipData(null)}>
|
<div className="relative w-full aspect-[2/1]" onMouseLeave={() => setTooltipData(null)}>
|
||||||
<svg
|
<svg
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
viewBox={`0 0 ${width} ${height}`}
|
viewBox={`0 0 ${width} ${height}`}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className="h-auto w-full"
|
className="w-full h-auto"
|
||||||
>
|
>
|
||||||
<title>Interactive Map</title>
|
|
||||||
<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" />
|
||||||
@ -63,12 +63,12 @@ export function InteractiveMap({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<path
|
<path
|
||||||
key={feature.properties.iso_a2_eh + String(index)}
|
key={index}
|
||||||
d={path(feature) || ""}
|
d={path(feature) || ""}
|
||||||
className={
|
className={
|
||||||
isHighlighted
|
isHighlighted
|
||||||
? "cursor-pointer fill-green-700 transition-all hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700"
|
? "fill-green-700 hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700 transition-all cursor-pointer"
|
||||||
: "fill-neutral-200/50 stroke-[0.5] stroke-neutral-300/40 dark:fill-neutral-800 dark:stroke-neutral-700"
|
: "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]"
|
||||||
}
|
}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
if (!isHighlighted) {
|
if (!isHighlighted) {
|
||||||
@ -82,7 +82,6 @@ export function InteractiveMap({
|
|||||||
(server: any) => server.host.CountryCode?.toUpperCase() === countryCode,
|
(server: any) => server.host.CountryCode?.toUpperCase() === countryCode,
|
||||||
)
|
)
|
||||||
.map((server: any) => ({
|
.map((server: any) => ({
|
||||||
id: server.id,
|
|
||||||
name: server.name,
|
name: server.name,
|
||||||
status: server.online_status,
|
status: server.online_status,
|
||||||
}))
|
}))
|
||||||
@ -123,7 +122,6 @@ export function InteractiveMap({
|
|||||||
const countryServers = nezhaServerList.result
|
const countryServers = nezhaServerList.result
|
||||||
.filter((server: any) => server.host.CountryCode?.toUpperCase() === countryCode)
|
.filter((server: any) => server.host.CountryCode?.toUpperCase() === countryCode)
|
||||||
.map((server: any) => ({
|
.map((server: any) => ({
|
||||||
id: server.id,
|
|
||||||
name: server.name,
|
name: server.name,
|
||||||
status: server.online_status,
|
status: server.online_status,
|
||||||
}))
|
}))
|
||||||
@ -140,7 +138,7 @@ export function InteractiveMap({
|
|||||||
cx={x}
|
cx={x}
|
||||||
cy={y}
|
cy={y}
|
||||||
r={4}
|
r={4}
|
||||||
className="fill-sky-700 stroke-white transition-all hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700"
|
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useTooltip } from "@/app/context/tooltip-context"
|
import { AnimatePresence, m } from "framer-motion"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import Link from "next/link"
|
|
||||||
import { memo } from "react"
|
import { memo } from "react"
|
||||||
|
|
||||||
|
import { useTooltip } from "../detail/TooltipContext"
|
||||||
|
|
||||||
const MapTooltip = memo(function MapTooltip() {
|
const MapTooltip = memo(function MapTooltip() {
|
||||||
const { tooltipData } = useTooltip()
|
const { tooltipData } = useTooltip()
|
||||||
const t = useTranslations("Global")
|
const t = useTranslations("Global")
|
||||||
@ -15,13 +16,13 @@ const MapTooltip = memo(function MapTooltip() {
|
|||||||
return a.status === b.status ? 0 : a.status ? 1 : -1
|
return a.status === b.status ? 0 : a.status ? 1 : -1
|
||||||
})
|
})
|
||||||
|
|
||||||
const saveSession = () => {
|
|
||||||
sessionStorage.setItem("fromMainPage", "true")
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<AnimatePresence mode="wait">
|
||||||
className="tooltip-animate absolute z-50 hidden rounded bg-white px-2 py-1 text-sm shadow-lg lg:block dark:border dark:border-neutral-700 dark:bg-neutral-800"
|
<m.div
|
||||||
|
initial={{ opacity: 0, filter: "blur(10px)" }}
|
||||||
|
animate={{ opacity: 1, filter: "blur(0px)" }}
|
||||||
|
exit={{ opacity: 0, filter: "blur(10px)" }}
|
||||||
|
className="absolute hidden lg:block bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700 z-50"
|
||||||
key={tooltipData.country}
|
key={tooltipData.country}
|
||||||
style={{
|
style={{
|
||||||
left: tooltipData.centroid[0],
|
left: tooltipData.centroid[0],
|
||||||
@ -36,34 +37,30 @@ const MapTooltip = memo(function MapTooltip() {
|
|||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{tooltipData.country === "China" ? "Mainland China" : tooltipData.country}
|
{tooltipData.country === "China" ? "Mainland China" : tooltipData.country}
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-1 font-light text-neutral-600 text-xs dark:text-neutral-400">
|
<p className="text-neutral-600 dark:text-neutral-400 mb-1">
|
||||||
{tooltipData.count} {t("Servers")}
|
{tooltipData.count} {t("Servers")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="border-t pt-1 dark:border-neutral-700"
|
className="border-t dark:border-neutral-700 pt-1"
|
||||||
style={{
|
style={{
|
||||||
maxHeight: "200px",
|
maxHeight: "200px",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{sortedServers.map((server) => (
|
{sortedServers.map((server, index) => (
|
||||||
<Link
|
<div key={index} className="flex items-center gap-1.5 py-0.5">
|
||||||
onClick={saveSession}
|
|
||||||
href={`/server/${server.id}`}
|
|
||||||
key={server.name}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
className={`h-1.5 w-1.5 shrink-0 rounded-full ${
|
className={`w-1.5 h-1.5 shrink-0 rounded-full ${
|
||||||
server.status ? "bg-green-500" : "bg-red-500"
|
server.status ? "bg-green-500" : "bg-red-500"
|
||||||
}`}
|
}`}
|
||||||
/>
|
></span>
|
||||||
<span className="text-xs">{server.name}</span>
|
<span className="text-xs">{server.name}</span>
|
||||||
</Link>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</m.div>
|
||||||
|
</AnimatePresence>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,103 +1,26 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useFilter } from "@/app/context/network-filter-context"
|
import { useServerData } from "@/app/lib/server-data-context"
|
||||||
import { useServerData } from "@/app/context/server-data-context"
|
|
||||||
import { useStatus } from "@/app/context/status-context"
|
|
||||||
import ServerCard from "@/components/ServerCard"
|
import ServerCard from "@/components/ServerCard"
|
||||||
import ServerCardInline from "@/components/ServerCardInline"
|
import ServerCardInline from "@/components/ServerCardInline"
|
||||||
import Switch from "@/components/Switch"
|
import Switch from "@/components/Switch"
|
||||||
import GlobalLoading from "@/components/loading/GlobalLoading"
|
|
||||||
import { Loader } from "@/components/loading/Loader"
|
import { Loader } from "@/components/loading/Loader"
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
|
import { useFilter } from "@/lib/network-filter-context"
|
||||||
|
import { useStatus } from "@/lib/status-context"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { MapIcon, ViewColumnsIcon } from "@heroicons/react/20/solid"
|
import { MapIcon, ViewColumnsIcon } from "@heroicons/react/20/solid"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import dynamic from "next/dynamic"
|
import dynamic from "next/dynamic"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
|
import GlobalLoading from "../../../../components/loading/GlobalLoading"
|
||||||
|
|
||||||
const ServerGlobal = dynamic(() => import("./Global"), {
|
const ServerGlobal = dynamic(() => import("./Global"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => <GlobalLoading />,
|
loading: () => <GlobalLoading />,
|
||||||
})
|
})
|
||||||
|
|
||||||
const sortServersByDisplayIndex = (servers: any[]) => {
|
|
||||||
return servers.sort((a, b) => {
|
|
||||||
const displayIndexDiff = (b.display_index || 0) - (a.display_index || 0)
|
|
||||||
return displayIndexDiff !== 0 ? displayIndexDiff : a.id - b.id
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterServersByStatus = (servers: any[], status: string) => {
|
|
||||||
return status === "all"
|
|
||||||
? servers
|
|
||||||
: servers.filter((server) => [status].includes(server.online_status ? "online" : "offline"))
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterServersByTag = (servers: any[], tag: string, defaultTag: string) => {
|
|
||||||
return tag === defaultTag ? servers : servers.filter((server) => server.tag === tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortServersByNetwork = (servers: any[]) => {
|
|
||||||
return [...servers].sort((a, b) => {
|
|
||||||
if (!a.online_status && b.online_status) return 1
|
|
||||||
if (a.online_status && !b.online_status) return -1
|
|
||||||
if (!a.online_status && !b.online_status) return 0
|
|
||||||
return b.status.NetInSpeed + b.status.NetOutSpeed - (a.status.NetInSpeed + a.status.NetOutSpeed)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTagCounts = (servers: any[]) => {
|
|
||||||
return servers.reduce((acc: Record<string, number>, server) => {
|
|
||||||
if (server.tag) {
|
|
||||||
acc[server.tag] = (acc[server.tag] || 0) + 1
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
const LoadingState = ({ t }: { t: any }) => (
|
|
||||||
<div className="flex min-h-96 flex-col items-center justify-center ">
|
|
||||||
<div className="flex items-center gap-2 font-semibold text-sm">
|
|
||||||
<Loader visible={true} />
|
|
||||||
{t("connecting")}...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const ErrorState = ({ error, t }: { error: Error; t: any }) => (
|
|
||||||
<div className="flex flex-col items-center justify-center">
|
|
||||||
<p className="font-medium text-sm opacity-40">{error.message}</p>
|
|
||||||
<p className="font-medium text-sm opacity-40">{t("error_message")}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const ServerList = ({
|
|
||||||
servers,
|
|
||||||
inline,
|
|
||||||
containerRef,
|
|
||||||
}: { servers: any[]; inline: string; containerRef: any }) => {
|
|
||||||
if (inline === "1") {
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
ref={containerRef}
|
|
||||||
className="scrollbar-hidden flex flex-col gap-2 overflow-x-scroll"
|
|
||||||
>
|
|
||||||
{servers.map((serverInfo) => (
|
|
||||||
<ServerCardInline key={serverInfo.id} serverInfo={serverInfo} />
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section ref={containerRef} className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
|
||||||
{servers.map((serverInfo) => (
|
|
||||||
<ServerCard key={serverInfo.id} serverInfo={serverInfo} />
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ServerListClient() {
|
export default function ServerListClient() {
|
||||||
const { status } = useStatus()
|
const { status } = useStatus()
|
||||||
const { filter } = useFilter()
|
const { filter } = useFilter()
|
||||||
@ -114,14 +37,12 @@ export default function ServerListClient() {
|
|||||||
if (inlineState !== null) {
|
if (inlineState !== null) {
|
||||||
setInline(inlineState)
|
setInline(inlineState)
|
||||||
}
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const showMapState = localStorage.getItem("showMap")
|
useEffect(() => {
|
||||||
if (showMapState !== null) {
|
|
||||||
setShowMap(showMapState === "true")
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedTag = sessionStorage.getItem("selectedTag") || defaultTag
|
const savedTag = sessionStorage.getItem("selectedTag") || defaultTag
|
||||||
setTag(savedTag)
|
setTag(savedTag)
|
||||||
|
|
||||||
restoreScrollPosition()
|
restoreScrollPosition()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -151,56 +72,90 @@ export default function ServerListClient() {
|
|||||||
|
|
||||||
const { data, error } = useServerData()
|
const { data, error } = useServerData()
|
||||||
|
|
||||||
if (error) return <ErrorState error={error} t={t} />
|
if (error)
|
||||||
if (!data?.result) return <LoadingState t={t} />
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<p className="text-sm font-medium opacity-40">{error.message}</p>
|
||||||
|
<p className="text-sm font-medium opacity-40">{t("error_message")}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
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 { result } = data
|
||||||
const sortedServers = sortServersByDisplayIndex(result)
|
const sortedServers = result.sort((a, b) => {
|
||||||
const filteredServersByStatus = filterServersByStatus(sortedServers, status)
|
const displayIndexDiff = (b.display_index || 0) - (a.display_index || 0)
|
||||||
|
if (displayIndexDiff !== 0) return displayIndexDiff
|
||||||
|
return a.id - b.id
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredServersByStatus =
|
||||||
|
status === "all"
|
||||||
|
? sortedServers
|
||||||
|
: sortedServers.filter((server) =>
|
||||||
|
[status].includes(server.online_status ? "online" : "offline"),
|
||||||
|
)
|
||||||
|
|
||||||
const allTag = filteredServersByStatus.map((server) => server.tag).filter(Boolean)
|
const allTag = filteredServersByStatus.map((server) => server.tag).filter(Boolean)
|
||||||
const uniqueTags = [...new Set(allTag)]
|
const uniqueTags = [...new Set(allTag)]
|
||||||
uniqueTags.unshift(defaultTag)
|
uniqueTags.unshift(defaultTag)
|
||||||
|
|
||||||
let filteredServers = filterServersByTag(filteredServersByStatus, tag, defaultTag)
|
const filteredServers =
|
||||||
|
tag === defaultTag
|
||||||
|
? filteredServersByStatus
|
||||||
|
: filteredServersByStatus.filter((server) => server.tag === tag)
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
filteredServers = sortServersByNetwork(filteredServers)
|
filteredServers.sort((a, b) => {
|
||||||
|
if (!a.online_status && b.online_status) return 1
|
||||||
|
if (a.online_status && !b.online_status) return -1
|
||||||
|
if (!a.online_status && !b.online_status) return 0
|
||||||
|
return (
|
||||||
|
b.status.NetInSpeed + b.status.NetOutSpeed - (a.status.NetInSpeed + a.status.NetOutSpeed)
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagCountMap = getTagCounts(filteredServersByStatus)
|
const tagCountMap: Record<string, number> = {}
|
||||||
|
filteredServersByStatus.forEach((server) => {
|
||||||
|
if (server.tag) {
|
||||||
|
tagCountMap[server.tag] = (tagCountMap[server.tag] || 0) + 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="flex w-full items-center gap-2 overflow-hidden">
|
<section className="flex items-center gap-2 w-full overflow-hidden">
|
||||||
<button
|
<button
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newShowMap = !showMap
|
setShowMap(!showMap)
|
||||||
setShowMap(newShowMap)
|
|
||||||
localStorage.setItem("showMap", String(newShowMap))
|
|
||||||
}}
|
}}
|
||||||
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-[10px] text-blue-600 transition-all dark:bg-blue-900 dark:text-blue-100 ",
|
"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)]",
|
||||||
{
|
{
|
||||||
"inset-shadow-black/20 bg-blue-600 text-white dark:bg-blue-100 dark:text-blue-600":
|
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": showMap,
|
||||||
showMap,
|
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MapIcon className="size-[13px]" />
|
<MapIcon className="size-[13px]" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newInline = inline === "0" ? "1" : "0"
|
setInline(inline === "0" ? "1" : "0")
|
||||||
setInline(newInline)
|
localStorage.setItem("inline", inline === "0" ? "1" : "0")
|
||||||
localStorage.setItem("inline", newInline)
|
|
||||||
}}
|
}}
|
||||||
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-[10px] text-blue-600 transition-all dark:bg-blue-900 dark:text-blue-100 ",
|
"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)] ",
|
||||||
{
|
{
|
||||||
"inset-shadow-black/20 bg-blue-600 text-white dark:bg-blue-100 dark:text-blue-600":
|
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": inline === "1",
|
||||||
inline === "1",
|
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -216,7 +171,24 @@ export default function ServerListClient() {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
{showMap && <ServerGlobal />}
|
{showMap && <ServerGlobal />}
|
||||||
<ServerList servers={filteredServers} inline={inline} containerRef={containerRef} />
|
{inline === "1" && (
|
||||||
|
<section
|
||||||
|
ref={containerRef}
|
||||||
|
className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden"
|
||||||
|
>
|
||||||
|
{filteredServers.map((serverInfo) => (
|
||||||
|
<ServerCardInline key={serverInfo.id} serverInfo={serverInfo} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{inline === "0" && (
|
||||||
|
<section ref={containerRef} className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||||
|
{filteredServers.map((serverInfo) => (
|
||||||
|
<ServerCard key={serverInfo.id} serverInfo={serverInfo} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useFilter } from "@/app/context/network-filter-context"
|
import { useServerData } from "@/app/lib/server-data-context"
|
||||||
import { useServerData } from "@/app/context/server-data-context"
|
|
||||||
import { useStatus } from "@/app/context/status-context"
|
|
||||||
import AnimateCountClient from "@/components/AnimatedCount"
|
|
||||||
import { Loader } from "@/components/loading/Loader"
|
import { Loader } from "@/components/loading/Loader"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
|
import { useFilter } from "@/lib/network-filter-context"
|
||||||
|
import { useStatus } from "@/lib/status-context"
|
||||||
import { cn, formatBytes } from "@/lib/utils"
|
import { cn, formatBytes } from "@/lib/utils"
|
||||||
import blogMan from "@/public/blog-man.webp"
|
import blogMan from "@/public/blog-man.webp"
|
||||||
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
|
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
|
||||||
@ -24,10 +23,10 @@ export default function ServerOverviewClient() {
|
|||||||
const errorInfo = error as any
|
const errorInfo = error as any
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<p className="font-medium text-sm opacity-40">
|
<p className="text-sm font-medium opacity-40">
|
||||||
Error status:{errorInfo?.status} {errorInfo.info?.cause ?? errorInfo?.message}
|
Error status:{errorInfo?.status} {errorInfo.info?.cause ?? errorInfo?.message}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-medium text-sm opacity-40">{t("error_message")}</p>
|
<p className="text-sm font-medium opacity-40">{t("error_message")}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -40,19 +39,17 @@ export default function ServerOverviewClient() {
|
|||||||
setFilter(false)
|
setFilter(false)
|
||||||
setStatus("all")
|
setStatus("all")
|
||||||
}}
|
}}
|
||||||
className={cn("group cursor-pointer transition-all hover:border-blue-500")}
|
className={cn("cursor-pointer hover:border-blue-500 transition-all group")}
|
||||||
>
|
>
|
||||||
<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="font-medium text-sm md:text-base">{t("p_816-881_Totalservers")}</p>
|
<p className="text-sm font-medium md:text-base">{t("p_816-881_Totalservers")}</p>
|
||||||
<div className="flex min-h-[28px] 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 className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
|
||||||
</span>
|
</span>
|
||||||
{data?.result ? (
|
{data?.result ? (
|
||||||
<div className="font-semibold text-lg">
|
<div className="text-lg font-semibold">{data?.result.length}</div>
|
||||||
<AnimateCountClient count={data?.result.length} />
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-7 items-center">
|
<div className="flex h-7 items-center">
|
||||||
<Loader visible={true} />
|
<Loader visible={true} />
|
||||||
@ -68,24 +65,22 @@ export default function ServerOverviewClient() {
|
|||||||
setStatus("online")
|
setStatus("online")
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer ring-1 ring-transparent transition-all hover:ring-green-500",
|
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all",
|
||||||
{
|
{
|
||||||
"border-transparent ring-2 ring-green-500": status === "online",
|
"ring-green-500 ring-2 border-transparent": status === "online",
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CardContent className="flex h-full items-center px-6 py-3">
|
<CardContent className="flex h-full items-center px-6 py-3">
|
||||||
<section className="flex flex-col gap-1">
|
<section className="flex flex-col gap-1">
|
||||||
<p className="font-medium text-sm md:text-base">{t("p_1610-1676_Onlineservers")}</p>
|
<p className="text-sm font-medium md:text-base">{t("p_1610-1676_Onlineservers")}</p>
|
||||||
<div className="flex min-h-[28px] 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 className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
|
||||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500" />
|
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
|
||||||
</span>
|
</span>
|
||||||
{data?.result ? (
|
{data?.result ? (
|
||||||
<div className="font-semibold text-lg">
|
<div className="text-lg font-semibold">{data?.live_servers}</div>
|
||||||
<AnimateCountClient count={data?.live_servers} />
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-7 items-center">
|
<div className="flex h-7 items-center">
|
||||||
<Loader visible={true} />
|
<Loader visible={true} />
|
||||||
@ -101,24 +96,22 @@ export default function ServerOverviewClient() {
|
|||||||
setStatus("offline")
|
setStatus("offline")
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer ring-1 ring-transparent transition-all hover:ring-red-500",
|
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all",
|
||||||
{
|
{
|
||||||
"border-transparent ring-2 ring-red-500": status === "offline",
|
"ring-red-500 ring-2 border-transparent": status === "offline",
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CardContent className="flex h-full items-center px-6 py-3">
|
<CardContent className="flex h-full items-center px-6 py-3">
|
||||||
<section className="flex flex-col gap-1">
|
<section className="flex flex-col gap-1">
|
||||||
<p className="font-medium text-sm md:text-base">{t("p_2532-2599_Offlineservers")}</p>
|
<p className="text-sm font-medium md:text-base">{t("p_2532-2599_Offlineservers")}</p>
|
||||||
<div className="flex min-h-[28px] 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 className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span>
|
||||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500" />
|
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
|
||||||
</span>
|
</span>
|
||||||
{data?.result ? (
|
{data?.result ? (
|
||||||
<div className="font-semibold text-lg">
|
<div className="text-lg font-semibold">{data?.offline_servers}</div>
|
||||||
<AnimateCountClient count={data?.offline_servers} />
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-7 items-center">
|
<div className="flex h-7 items-center">
|
||||||
<Loader visible={true} />
|
<Loader visible={true} />
|
||||||
@ -134,34 +127,34 @@ export default function ServerOverviewClient() {
|
|||||||
setFilter(true)
|
setFilter(true)
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group cursor-pointer ring-1 ring-transparent transition-all hover:ring-purple-500",
|
"cursor-pointer hover:ring-purple-500 ring-1 ring-transparent transition-all group",
|
||||||
{
|
{
|
||||||
"border-transparent ring-2 ring-purple-500": filter === true,
|
"ring-purple-500 ring-2 border-transparent": filter === true,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CardContent className="relative flex h-full items-center px-6 py-3">
|
<CardContent className="flex h-full items-center relative px-6 py-3">
|
||||||
<section className="flex w-full flex-col gap-1">
|
<section className="flex flex-col gap-1 w-full">
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex items-center w-full justify-between">
|
||||||
<p className="font-medium text-sm md:text-base">{t("network")}</p>
|
<p className="text-sm font-medium md:text-base">{t("network")}</p>
|
||||||
</div>
|
</div>
|
||||||
{data?.result ? (
|
{data?.result ? (
|
||||||
<>
|
<>
|
||||||
<section className="flex flex-row flex-wrap items-start gap-1 pr-0">
|
<section className="flex flex-col sm:flex-row items-start pr-0 gap-1">
|
||||||
<p className="text-nowrap font-medium text-[12px] text-blue-800 dark:text-blue-400">
|
<p className="text-[12px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">
|
||||||
↑{formatBytes(data?.total_out_bandwidth)}
|
↑{formatBytes(data?.total_out_bandwidth)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-nowrap font-medium text-[12px] text-purple-800 dark:text-purple-400">
|
<p className="text-[12px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">
|
||||||
↓{formatBytes(data?.total_in_bandwidth)}
|
↓{formatBytes(data?.total_in_bandwidth)}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section className="-mr-1 flex flex-row flex-wrap items-start gap-1 sm:items-center">
|
<section className="flex flex-col sm:flex-row -mr-1 sm:items-center items-start gap-1">
|
||||||
<p className="flex items-center text-nowrap font-semibold text-[11px]">
|
<p className="text-[11px] flex items-center text-nowrap font-semibold">
|
||||||
<ArrowUpCircleIcon className="mr-0.5 size-3 sm:mb-[1px]" />
|
<ArrowUpCircleIcon className="size-3 mr-0.5 sm:mb-[1px]" />
|
||||||
{formatBytes(data?.total_out_speed)}/s
|
{formatBytes(data?.total_out_speed)}/s
|
||||||
</p>
|
</p>
|
||||||
<p className="flex items-center text-nowrap font-semibold text-[11px]">
|
<p className="text-[11px] flex items-center text-nowrap font-semibold">
|
||||||
<ArrowDownCircleIcon className="mr-0.5 size-3" />
|
<ArrowDownCircleIcon className="size-3 mr-0.5" />
|
||||||
{formatBytes(data?.total_in_speed)}/s
|
{formatBytes(data?.total_in_speed)}/s
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
@ -174,7 +167,7 @@ export default function ServerOverviewClient() {
|
|||||||
</section>
|
</section>
|
||||||
{!disableCartoon && (
|
{!disableCartoon && (
|
||||||
<Image
|
<Image
|
||||||
className="absolute top-[-85px] right-3 z-50 w-20 scale-90 transition-all group-hover:opacity-50 md:scale-100"
|
className="absolute right-3 top-[-85px] z-50 w-20 scale-90 group-hover:opacity-50 md:scale-100 transition-all"
|
||||||
alt={"Hamster1963"}
|
alt={"Hamster1963"}
|
||||||
src={blogMan}
|
src={blogMan}
|
||||||
priority
|
priority
|
||||||
@ -186,7 +179,7 @@ export default function ServerOverviewClient() {
|
|||||||
</section>
|
</section>
|
||||||
{data?.result === undefined && !isLoading && (
|
{data?.result === undefined && !isLoading && (
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<p className="font-medium text-sm opacity-40">{t("error_message")}</p>
|
<p className="text-sm font-medium opacity-40">{t("error_message")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -1,58 +1,35 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import pack from "@/package.json"
|
import pack from "@/package.json"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
const GITHUB_URL = "https://github.com/hamster1963/nezha-dash"
|
|
||||||
const PERSONAL_URL = "https://buycoffee.top"
|
|
||||||
|
|
||||||
type LinkProps = {
|
|
||||||
href: string
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
const FooterLink = ({ href, children }: LinkProps) => (
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
target="_blank"
|
|
||||||
className="cursor-pointer font-normal underline decoration-2 decoration-yellow-500 underline-offset-2 transition-colors hover:decoration-yellow-600 dark:decoration-yellow-500/60 dark:hover:decoration-yellow-500/80"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
|
|
||||||
const baseTextStyles =
|
|
||||||
"text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50"
|
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const t = useTranslations("Footer")
|
const t = useTranslations("Footer")
|
||||||
const version = pack.version
|
const version = pack.version
|
||||||
const currentYear = new Date().getFullYear()
|
|
||||||
const [isMac, setIsMac] = useState(true)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsMac(/macintosh|mac os x/i.test(navigator.userAgent))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="mx-auto flex w-full max-w-5xl items-center justify-between">
|
<footer className="mx-auto w-full max-w-5xl">
|
||||||
<section className="flex flex-col">
|
<section className="flex flex-col">
|
||||||
<p className={`mt-3 flex gap-1 ${baseTextStyles}`}>
|
<p className="mt-3 flex gap-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
|
||||||
{t("p_146-598_Findthecodeon")}{" "}
|
{t("p_146-598_Findthecodeon")}{" "}
|
||||||
<FooterLink href={GITHUB_URL}>{t("a_303-585_GitHub")}</FooterLink>
|
<a
|
||||||
<FooterLink href={`${GITHUB_URL}/releases/tag/v${version}`}>v{version}</FooterLink>
|
href="https://github.com/hamster1963/nezha-dash"
|
||||||
|
target="_blank"
|
||||||
|
className="cursor-pointer font-normal underline decoration-yellow-500 hover:decoration-yellow-600 transition-colors decoration-2 underline-offset-2 dark:decoration-yellow-500/60 dark:hover:decoration-yellow-500/80"
|
||||||
|
>
|
||||||
|
{t("a_303-585_GitHub")}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={`https://github.com/hamster1963/nezha-dash/releases/tag/v${version}`}
|
||||||
|
target="_blank"
|
||||||
|
className="cursor-pointer font-normal underline decoration-yellow-500 hover:decoration-yellow-600 transition-colors decoration-2 underline-offset-2 dark:decoration-yellow-500/60 dark:hover:decoration-yellow-500/80"
|
||||||
|
>
|
||||||
|
v{version}
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<section className={`mt-1 flex items-center gap-2 ${baseTextStyles}`}>
|
<section className="mt-1 flex items-center gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
|
||||||
{t("section_607-869_2020")}
|
{t("section_607-869_2020")}
|
||||||
{currentYear} <FooterLink href={PERSONAL_URL}>{t("a_800-850_Hamster1963")}</FooterLink>
|
{new Date().getFullYear()}{" "}
|
||||||
|
<a href={"https://buycoffee.top"}>{t("a_800-850_Hamster1963")}</a>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
<p className={`mt-1 ${baseTextStyles}`}>
|
|
||||||
<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-medium font-mono text-[10px] text-muted-foreground opacity-100">
|
|
||||||
{isMac ? <span className="text-xs">⌘</span> : "Ctrl "}K
|
|
||||||
</kbd>
|
|
||||||
</p>
|
|
||||||
</footer>
|
</footer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,105 +1,14 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import AnimateCountClient from "@/components/AnimatedCount"
|
|
||||||
import { LanguageSwitcher } from "@/components/LanguageSwitcher"
|
import { LanguageSwitcher } from "@/components/LanguageSwitcher"
|
||||||
import { ModeToggle } from "@/components/ThemeSwitcher"
|
import { ModeToggle } from "@/components/ThemeSwitcher"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { memo, useCallback, useEffect, useState } from "react"
|
import React, { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
interface TimeState {
|
|
||||||
hh: number
|
|
||||||
mm: number
|
|
||||||
ss: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CustomLink {
|
|
||||||
link: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const useCurrentTime = () => {
|
|
||||||
const [time, setTime] = useState<TimeState>({
|
|
||||||
hh: DateTime.now().setLocale("en-US").hour,
|
|
||||||
mm: DateTime.now().setLocale("en-US").minute,
|
|
||||||
ss: DateTime.now().setLocale("en-US").second,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
const now = DateTime.now().setLocale("en-US")
|
|
||||||
setTime({
|
|
||||||
hh: now.hour,
|
|
||||||
mm: now.minute,
|
|
||||||
ss: now.second,
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
return () => clearInterval(intervalId)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return time
|
|
||||||
}
|
|
||||||
|
|
||||||
const Links = memo(function Links() {
|
|
||||||
const linksEnv = getEnv("NEXT_PUBLIC_Links")
|
|
||||||
const links: CustomLink[] | null = linksEnv ? JSON.parse(linksEnv) : null
|
|
||||||
|
|
||||||
if (!links) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{links.map((link) => (
|
|
||||||
<a
|
|
||||||
key={link.link}
|
|
||||||
href={link.link}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1 font-medium text-sm opacity-50 transition-opacity hover:opacity-100"
|
|
||||||
>
|
|
||||||
{link.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const Overview = memo(function Overview() {
|
|
||||||
const t = useTranslations("Overview")
|
|
||||||
const time = useCurrentTime()
|
|
||||||
const [mounted, setMounted] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className={"mt-10 flex flex-col md:mt-16"}>
|
|
||||||
<p className="font-semibold text-base">{t("p_2277-2331_Overview")}</p>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<p className="font-medium text-sm opacity-50">{t("p_2390-2457_wherethetimeis")}</p>
|
|
||||||
{mounted ? (
|
|
||||||
<div className="flex items-center font-medium text-sm">
|
|
||||||
<AnimateCountClient count={time.hh} minDigits={2} />
|
|
||||||
<span className="mb-[1px] font-medium text-sm opacity-50">:</span>
|
|
||||||
<AnimateCountClient count={time.mm} minDigits={2} />
|
|
||||||
<span className="mb-[1px] font-medium text-sm opacity-50">:</span>
|
|
||||||
<span className="font-medium text-sm">
|
|
||||||
<AnimateCountClient count={time.ss} minDigits={2} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Skeleton className="h-[21px] w-16 animate-none rounded-[5px] bg-muted-foreground/10" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
const t = useTranslations("Header")
|
const t = useTranslations("Header")
|
||||||
@ -109,17 +18,15 @@ function Header() {
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const handleLogoClick = useCallback(() => {
|
|
||||||
sessionStorage.removeItem("selectedTag")
|
|
||||||
router.push("/")
|
|
||||||
}, [router])
|
|
||||||
|
|
||||||
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">
|
<section className="flex items-center justify-between">
|
||||||
<section
|
<section
|
||||||
onClick={handleLogoClick}
|
onClick={() => {
|
||||||
className="flex cursor-pointer items-center font-medium text-base transition-opacity duration-300 hover:opacity-50"
|
sessionStorage.removeItem("selectedTag")
|
||||||
|
router.push(`/`)
|
||||||
|
}}
|
||||||
|
className="flex cursor-pointer items-center text-base font-medium"
|
||||||
>
|
>
|
||||||
<div className="mr-1 flex flex-row items-center justify-start">
|
<div className="mr-1 flex flex-row items-center justify-start">
|
||||||
<img
|
<img
|
||||||
@ -127,19 +34,19 @@ function Header() {
|
|||||||
height={40}
|
height={40}
|
||||||
alt="apple-touch-icon"
|
alt="apple-touch-icon"
|
||||||
src={customLogo ? customLogo : "/apple-touch-icon.png"}
|
src={customLogo ? customLogo : "/apple-touch-icon.png"}
|
||||||
className="relative m-0! h-6 w-6 border-2 border-transparent object-cover object-top p-0! dark:hidden"
|
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0! dark:hidden"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
alt="apple-touch-icon"
|
alt="apple-touch-icon"
|
||||||
src={customLogo ? customLogo : "/apple-touch-icon-dark.png"}
|
src={customLogo ? customLogo : "/apple-touch-icon-dark.png"}
|
||||||
className="relative m-0! hidden h-6 w-6 border-2 border-transparent object-cover object-top p-0! dark:block"
|
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0! hidden dark:block"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{customTitle ? customTitle : "NezhaDash"}
|
{customTitle ? customTitle : "NezhaDash"}
|
||||||
<Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" />
|
<Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" />
|
||||||
<p className="hidden font-medium text-sm opacity-40 md:block">
|
<p className="hidden text-sm font-medium opacity-40 md:block">
|
||||||
{customDescription ? customDescription : t("p_1079-1199_Simpleandbeautifuldashbo")}
|
{customDescription ? customDescription : t("p_1079-1199_Simpleandbeautifuldashbo")}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
@ -151,7 +58,7 @@ function Header() {
|
|||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
<div className="mt-1 flex w-full justify-end sm:hidden">
|
<div className="w-full flex justify-end sm:hidden mt-1">
|
||||||
<Links />
|
<Links />
|
||||||
</div>
|
</div>
|
||||||
<Overview />
|
<Overview />
|
||||||
@ -159,4 +66,77 @@ function Header() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type links = {
|
||||||
|
link: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function Links() {
|
||||||
|
const linksEnv = getEnv("NEXT_PUBLIC_Links")
|
||||||
|
|
||||||
|
const links: links[] | null = linksEnv ? JSON.parse(linksEnv) : null
|
||||||
|
|
||||||
|
if (!links) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{links.map((link, index) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={index}
|
||||||
|
href={link.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
|
||||||
|
>
|
||||||
|
{link.name}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/streamich/react-use/blob/master/src/useInterval.ts
|
||||||
|
const useInterval = (callback: () => void, delay: number | null) => {
|
||||||
|
const savedCallback = useRef<() => void>(() => {})
|
||||||
|
useEffect(() => {
|
||||||
|
savedCallback.current = callback
|
||||||
|
})
|
||||||
|
useEffect(() => {
|
||||||
|
if (delay !== null) {
|
||||||
|
const interval = setInterval(() => savedCallback.current(), delay || 0)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}, [delay])
|
||||||
|
}
|
||||||
|
function Overview() {
|
||||||
|
const t = useTranslations("Overview")
|
||||||
|
const [mouted, setMounted] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
const timeOption = DateTime.TIME_SIMPLE
|
||||||
|
timeOption.hour12 = true
|
||||||
|
const [timeString, setTimeString] = useState(
|
||||||
|
DateTime.now().setLocale("en-US").toLocaleString(timeOption),
|
||||||
|
)
|
||||||
|
useInterval(() => {
|
||||||
|
setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption))
|
||||||
|
}, 1000)
|
||||||
|
return (
|
||||||
|
<section className={"mt-10 flex flex-col md:mt-16"}>
|
||||||
|
<p className="text-base font-semibold">{t("p_2277-2331_Overview")}</p>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<p className="text-sm font-medium opacity-50">{t("p_2390-2457_wherethetimeis")}</p>
|
||||||
|
{mouted ? (
|
||||||
|
<p className="text-sm font-medium">{timeString}</p>
|
||||||
|
) : (
|
||||||
|
<Skeleton className="h-[20px] w-[50px] rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
export default Header
|
export default Header
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import Footer from "@/app/(main)/footer"
|
import Footer from "@/app/(main)/footer"
|
||||||
import Header from "@/app/(main)/header"
|
import Header from "@/app/(main)/header"
|
||||||
import { ServerDataProvider } from "@/app/context/server-data-context"
|
import { ServerDataProvider } from "@/app/lib/server-data-context"
|
||||||
import { auth } from "@/auth"
|
import { auth } from "@/auth"
|
||||||
import { DashCommand } from "@/components/DashCommand"
|
|
||||||
import { SignIn } from "@/components/SignIn"
|
import { SignIn } from "@/components/SignIn"
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import type React from "react"
|
import React from "react"
|
||||||
|
|
||||||
type DashboardProps = {
|
type DashboardProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
@ -16,10 +15,7 @@ export default function MainLayout({ children }: DashboardProps) {
|
|||||||
<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">
|
<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 />
|
<Header />
|
||||||
<AuthProtected>
|
<AuthProtected>
|
||||||
<ServerDataProvider>
|
<ServerDataProvider>{children}</ServerDataProvider>
|
||||||
{children}
|
|
||||||
<DashCommand />
|
|
||||||
</ServerDataProvider>
|
|
||||||
</AuthProtected>
|
</AuthProtected>
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import ServerListClient from "@/app/(main)/ClientComponents/main/ServerListClient"
|
import ServerListClient from "./ClientComponents/main/ServerListClient"
|
||||||
import ServerOverviewClient from "@/app/(main)/ClientComponents/main/ServerOverviewClient"
|
import ServerOverviewClient from "./ClientComponents/main/ServerOverviewClient"
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
return (
|
return (
|
||||||
|
@ -3,51 +3,34 @@
|
|||||||
import { NetworkChartClient } from "@/app/(main)/ClientComponents/detail/NetworkChart"
|
import { NetworkChartClient } from "@/app/(main)/ClientComponents/detail/NetworkChart"
|
||||||
import ServerDetailChartClient from "@/app/(main)/ClientComponents/detail/ServerDetailChartClient"
|
import ServerDetailChartClient from "@/app/(main)/ClientComponents/detail/ServerDetailChartClient"
|
||||||
import ServerDetailClient from "@/app/(main)/ClientComponents/detail/ServerDetailClient"
|
import ServerDetailClient from "@/app/(main)/ClientComponents/detail/ServerDetailClient"
|
||||||
import ServerIPInfo from "@/app/(main)/ClientComponents/detail/ServerIPInfo"
|
|
||||||
import TabSwitch from "@/components/TabSwitch"
|
import TabSwitch from "@/components/TabSwitch"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import { use, useState } from "react"
|
import { use, useState } from "react"
|
||||||
|
|
||||||
type PageProps = {
|
import ServerIPInfo from "../../ClientComponents/detail/ServerIPInfo"
|
||||||
params: Promise<{ id: string }>
|
|
||||||
}
|
|
||||||
|
|
||||||
type TabType = "Detail" | "Network"
|
|
||||||
|
|
||||||
export default function Page({ params }: PageProps) {
|
|
||||||
const { id } = use(params)
|
|
||||||
const serverId = Number(id)
|
|
||||||
const tabs: TabType[] = ["Detail", "Network"]
|
|
||||||
const [currentTab, setCurrentTab] = useState<TabType>(tabs[0])
|
|
||||||
|
|
||||||
const tabContent = {
|
|
||||||
Detail: <ServerDetailChartClient server_id={serverId} show={currentTab === "Detail"} />,
|
|
||||||
Network: (
|
|
||||||
<>
|
|
||||||
{getEnv("NEXT_PUBLIC_ShowIpInfo") && <ServerIPInfo server_id={serverId} />}
|
|
||||||
<NetworkChartClient server_id={serverId} show={currentTab === "Network"} />
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export default function Page(props: { params: Promise<{ id: string }> }) {
|
||||||
|
const params = use(props.params)
|
||||||
|
const tabs = ["Detail", "Network"]
|
||||||
|
const [currentTab, setCurrentTab] = useState(tabs[0])
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto grid w-full max-w-5xl gap-2">
|
<div className="mx-auto grid w-full max-w-5xl gap-2">
|
||||||
<ServerDetailClient server_id={serverId} />
|
<ServerDetailClient server_id={Number(params.id)} />
|
||||||
|
<section className="flex items-center my-2 w-full">
|
||||||
<nav className="my-2 flex w-full items-center">
|
|
||||||
<Separator className="flex-1" />
|
<Separator className="flex-1" />
|
||||||
<div className="flex w-full max-w-[200px] justify-center">
|
<div className="flex justify-center w-full max-w-[200px]">
|
||||||
<TabSwitch
|
<TabSwitch tabs={tabs} currentTab={currentTab} setCurrentTab={setCurrentTab} />
|
||||||
tabs={tabs}
|
|
||||||
currentTab={currentTab}
|
|
||||||
setCurrentTab={(tab: string) => setCurrentTab(tab as TabType)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Separator className="flex-1" />
|
<Separator className="flex-1" />
|
||||||
</nav>
|
</section>
|
||||||
|
<div style={{ display: currentTab === tabs[0] ? "block" : "none" }}>
|
||||||
{tabContent[currentTab]}
|
<ServerDetailChartClient server_id={Number(params.id)} show={currentTab === tabs[0]} />
|
||||||
</main>
|
</div>
|
||||||
|
<div style={{ display: currentTab === tabs[1] ? "block" : "none" }}>
|
||||||
|
{getEnv("NEXT_PUBLIC_ShowIpInfo") && <ServerIPInfo server_id={Number(params.id)} />}
|
||||||
|
<NetworkChartClient server_id={Number(params.id)} show={currentTab === tabs[1]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { auth } from "@/auth"
|
|||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import { GetServerDetail } from "@/lib/serverFetch"
|
import { GetServerDetail } from "@/lib/serverFetch"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
import { type NextRequest, NextResponse } from "next/server"
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
export const dynamic = "force-dynamic"
|
||||||
|
|
||||||
@ -27,8 +27,8 @@ export async function GET(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const serverIdNum = Number.parseInt(server_id, 10)
|
const serverIdNum = parseInt(server_id, 10)
|
||||||
if (Number.isNaN(serverIdNum)) {
|
if (isNaN(serverIdNum)) {
|
||||||
return NextResponse.json({ error: "server_id must be a valid number" }, { status: 400 })
|
return NextResponse.json({ error: "server_id must be a valid number" }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { auth } from "@/auth"
|
|||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import { GetServerMonitor } from "@/lib/serverFetch"
|
import { GetServerMonitor } from "@/lib/serverFetch"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
import { type NextRequest, NextResponse } from "next/server"
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
export const dynamic = "force-dynamic"
|
||||||
|
|
||||||
@ -27,8 +27,8 @@ export async function GET(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const serverIdNum = Number.parseInt(server_id, 10)
|
const serverIdNum = parseInt(server_id, 10)
|
||||||
if (Number.isNaN(serverIdNum)) {
|
if (isNaN(serverIdNum)) {
|
||||||
return NextResponse.json({ error: "server_id must be a number" }, { status: 400 })
|
return NextResponse.json({ error: "server_id must be a number" }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import fs from "node:fs"
|
import fs from "fs"
|
||||||
import path from "node:path"
|
import path from "path"
|
||||||
import { auth } from "@/auth"
|
import { auth } from "@/auth"
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import { GetServerIP } from "@/lib/serverFetch"
|
import { GetServerIP } from "@/lib/serverFetch"
|
||||||
import { type AsnResponse, type CityResponse, Reader } from "maxmind"
|
import { AsnResponse, CityResponse, Reader } from "maxmind"
|
||||||
import { redirect } from "next/navigation"
|
import { redirect } from "next/navigation"
|
||||||
import { type NextRequest, NextResponse } from "next/server"
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
|
||||||
export const dynamic = "force-dynamic"
|
export const dynamic = "force-dynamic"
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ export async function GET(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!getEnv("NEXT_PUBLIC_ShowIpInfo")) {
|
if (!getEnv("NEXT_PUBLIC_ShowIpInfo")) {
|
||||||
return NextResponse.json({ error: "ip info is disabled" }, { status: 400 })
|
return NextResponse.json({ error: "NEXT_PUBLIC_ShowIpInfo is disable" }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const { searchParams } = new URL(req.url)
|
const { searchParams } = new URL(req.url)
|
||||||
@ -41,9 +41,9 @@ export async function GET(req: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const ip = await GetServerIP({ server_id: Number(server_id) })
|
const ip = await GetServerIP({ server_id: Number(server_id) })
|
||||||
|
|
||||||
const cityDbPath = path.join(process.cwd(), "lib", "maxmind-db", "GeoLite2-City.mmdb")
|
const cityDbPath = path.join(process.cwd(), "lib", "GeoLite2-City.mmdb")
|
||||||
|
|
||||||
const asnDbPath = path.join(process.cwd(), "lib", "maxmind-db", "GeoLite2-ASN.mmdb")
|
const asnDbPath = path.join(process.cwd(), "lib", "GeoLite2-ASN.mmdb")
|
||||||
|
|
||||||
const cityDbBuffer = fs.readFileSync(cityDbPath)
|
const cityDbBuffer = fs.readFileSync(cityDbPath)
|
||||||
const asnDbBuffer = fs.readFileSync(asnDbPath)
|
const asnDbBuffer = fs.readFileSync(asnDbPath)
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import { FilterProvider } from "@/app/context/network-filter-context"
|
// @auto-i18n-check. Please do not delete the line.
|
||||||
import { StatusProvider } from "@/app/context/status-context"
|
|
||||||
import { ThemeColorManager } from "@/components/ThemeColorManager"
|
import { ThemeColorManager } from "@/components/ThemeColorManager"
|
||||||
|
import { MotionProvider } from "@/components/motion/motion-provider"
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
|
import { FilterProvider } from "@/lib/network-filter-context"
|
||||||
|
import { StatusProvider } from "@/lib/status-context"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import "@/styles/globals.css"
|
import "@/styles/globals.css"
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
import type { Viewport } from "next"
|
import { Viewport } from "next"
|
||||||
import { NextIntlClientProvider } from "next-intl"
|
import { NextIntlClientProvider } from "next-intl"
|
||||||
import { getLocale, getMessages } from "next-intl/server"
|
import { getLocale, getMessages } from "next-intl/server"
|
||||||
import { PublicEnvScript } from "next-runtime-env"
|
import { PublicEnvScript } from "next-runtime-env"
|
||||||
import { ThemeProvider } from "next-themes"
|
import { ThemeProvider } from "next-themes"
|
||||||
import { Inter as FontSans } from "next/font/google"
|
import { Inter as FontSans } from "next/font/google"
|
||||||
import type React from "react"
|
import React from "react"
|
||||||
|
|
||||||
const fontSans = FontSans({
|
const fontSans = FontSans({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@ -32,8 +34,8 @@ export const metadata: Metadata = {
|
|||||||
statusBarStyle: "default",
|
statusBarStyle: "default",
|
||||||
},
|
},
|
||||||
robots: {
|
robots: {
|
||||||
index: !disableIndex,
|
index: disableIndex ? false : true,
|
||||||
follow: !disableIndex,
|
follow: disableIndex ? false : true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,11 +46,7 @@ export const viewport: Viewport = {
|
|||||||
userScalable: false,
|
userScalable: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function LocaleLayout({
|
export default async function LocaleLayout({ children }: { children: React.ReactNode }) {
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
const locale = await getLocale()
|
const locale = await getLocale()
|
||||||
const messages = await getMessages()
|
const messages = await getMessages()
|
||||||
|
|
||||||
@ -66,6 +64,7 @@ export default async function LocaleLayout({
|
|||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body className={cn("min-h-screen bg-background font-sans antialiased", fontSans.variable)}>
|
<body className={cn("min-h-screen bg-background font-sans antialiased", fontSans.variable)}>
|
||||||
|
<MotionProvider>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
@ -81,6 +80,7 @@ export default async function LocaleLayout({
|
|||||||
</FilterProvider>
|
</FilterProvider>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</MotionProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import type { ServerApi } from "@/app/types/nezha-api"
|
import { ServerApi } from "@/app/types/nezha-api"
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import { nezhaFetcher } from "@/lib/utils"
|
import { nezhaFetcher } from "@/lib/utils"
|
||||||
import { type ReactNode, createContext, useContext, useEffect, useState } from "react"
|
import { ReactNode, createContext, useContext, useEffect, useState } from "react"
|
||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
|
|
||||||
export interface ServerDataWithTimestamp {
|
export interface ServerDataWithTimestamp {
|
||||||
@ -20,7 +20,7 @@ interface ServerDataContextType {
|
|||||||
|
|
||||||
const ServerDataContext = createContext<ServerDataContextType | undefined>(undefined)
|
const ServerDataContext = createContext<ServerDataContextType | undefined>(undefined)
|
||||||
|
|
||||||
export const MAX_HISTORY_LENGTH = 30
|
const MAX_HISTORY_LENGTH = 30
|
||||||
|
|
||||||
export function ServerDataProvider({ children }: { children: ReactNode }) {
|
export function ServerDataProvider({ children }: { children: ReactNode }) {
|
||||||
const [history, setHistory] = useState<ServerDataWithTimestamp[]>([])
|
const [history, setHistory] = useState<ServerDataWithTimestamp[]>([])
|
@ -1,18 +1,19 @@
|
|||||||
import Footer from "@/app/(main)/footer"
|
|
||||||
import Header from "@/app/(main)/header"
|
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
|
||||||
|
import Footer from "./(main)/footer"
|
||||||
|
import Header from "./(main)/header"
|
||||||
|
|
||||||
export default function NotFoundPage() {
|
export default function NotFoundPage() {
|
||||||
const t = useTranslations("NotFoundPage")
|
const t = useTranslations("NotFoundPage")
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen w-full flex-col">
|
<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">
|
<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 />
|
<Header />
|
||||||
<section className="flex flex-1 flex-col items-center justify-center gap-2">
|
<section className="flex flex-col items-center flex-1 justify-center gap-2">
|
||||||
<p className="font-semibold text-sm">{t("h1_490-590_404NotFound")}</p>
|
<p className="text-sm font-semibold">{t("h1_490-590_404NotFound")}</p>
|
||||||
<Link href="/" className="flex items-center gap-1">
|
<Link href="/" className="flex items-center gap-1">
|
||||||
<p className="font-medium text-sm opacity-40">{t("h1_490-590_404NotFoundBack")}</p>
|
<p className="text-sm font-medium opacity-40">{t("h1_490-590_404NotFoundBack")}</p>
|
||||||
</Link>
|
</Link>
|
||||||
</section>
|
</section>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
8
auth.ts
@ -1,12 +1,10 @@
|
|||||||
import getEnv from "@/lib/env-entry"
|
|
||||||
import CryptoJS from "crypto-js"
|
|
||||||
import NextAuth from "next-auth"
|
import NextAuth from "next-auth"
|
||||||
import CredentialsProvider from "next-auth/providers/credentials"
|
import CredentialsProvider from "next-auth/providers/credentials"
|
||||||
|
|
||||||
|
import getEnv from "./lib/env-entry"
|
||||||
|
|
||||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
secret:
|
secret: process.env.AUTH_SECRET ?? "this_is_nezha_dash_web_secret",
|
||||||
process.env.AUTH_SECRET ??
|
|
||||||
CryptoJS.MD5(`this_is_nezha_dash_web_secret_${getEnv("SitePassword")}`).toString(),
|
|
||||||
trustHost: (process.env.AUTH_TRUST_HOST as boolean | undefined) ?? true,
|
trustHost: (process.env.AUTH_TRUST_HOST as boolean | undefined) ?? true,
|
||||||
providers: [
|
providers: [
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
|
25
biome.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||||
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
|
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
|
||||||
"files": { "ignoreUnknown": false, "ignore": [".next", "public", "styles/globals.css"] },
|
"files": { "ignoreUnknown": false, "ignore": [".next", "public"] },
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"useEditorconfig": true,
|
"useEditorconfig": true,
|
||||||
@ -16,17 +16,7 @@
|
|||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true,
|
"recommended": false,
|
||||||
"nursery": {
|
|
||||||
"useSortedClasses": "error"
|
|
||||||
},
|
|
||||||
"a11y": {
|
|
||||||
"useKeyWithClickEvents": "off",
|
|
||||||
"noLabelWithoutControl": "off"
|
|
||||||
},
|
|
||||||
"security": {
|
|
||||||
"noDangerouslySetInnerHtml": "off"
|
|
||||||
},
|
|
||||||
"complexity": { "noUselessTypeConstraint": "error" },
|
"complexity": { "noUselessTypeConstraint": "error" },
|
||||||
"correctness": {
|
"correctness": {
|
||||||
"noUnusedVariables": "error",
|
"noUnusedVariables": "error",
|
||||||
@ -62,7 +52,15 @@
|
|||||||
"linter": {
|
"linter": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"correctness": {
|
"correctness": {
|
||||||
"noUnusedImports": "error"
|
"noConstAssign": "off",
|
||||||
|
"noGlobalObjectCalls": "off",
|
||||||
|
"noInvalidBuiltinInstantiation": "off",
|
||||||
|
"noInvalidConstructorSuper": "off",
|
||||||
|
"noNewSymbol": "off",
|
||||||
|
"noSetterReturn": "off",
|
||||||
|
"noUndeclaredVariables": "off",
|
||||||
|
"noUnreachable": "off",
|
||||||
|
"noUnreachableSuper": "off"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"noArguments": "error",
|
"noArguments": "error",
|
||||||
@ -75,6 +73,7 @@
|
|||||||
"noDuplicateObjectKeys": "off",
|
"noDuplicateObjectKeys": "off",
|
||||||
"noDuplicateParameters": "off",
|
"noDuplicateParameters": "off",
|
||||||
"noFunctionAssign": "off",
|
"noFunctionAssign": "off",
|
||||||
|
"noImportAssign": "off",
|
||||||
"noRedeclare": "off",
|
"noRedeclare": "off",
|
||||||
"noUnsafeNegation": "off",
|
"noUnsafeNegation": "off",
|
||||||
"useGetterReturn": "off"
|
"useGetterReturn": "off"
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
|
|
||||||
export default function AnimateCountClient({
|
|
||||||
count,
|
|
||||||
className,
|
|
||||||
minDigits,
|
|
||||||
}: {
|
|
||||||
count: number
|
|
||||||
className?: string
|
|
||||||
minDigits?: number
|
|
||||||
}) {
|
|
||||||
const [previousCount, setPreviousCount] = useState(count)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (count !== previousCount) {
|
|
||||||
setTimeout(() => {
|
|
||||||
setPreviousCount(count)
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
}, [count])
|
|
||||||
return (
|
|
||||||
<AnimateCount
|
|
||||||
key={count}
|
|
||||||
preCount={previousCount}
|
|
||||||
className={cn("inline-flex items-center leading-none", className)}
|
|
||||||
minDigits={minDigits}
|
|
||||||
data-issues-count-animation
|
|
||||||
>
|
|
||||||
{count}
|
|
||||||
</AnimateCount>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AnimateCount({
|
|
||||||
children: count,
|
|
||||||
className,
|
|
||||||
preCount,
|
|
||||||
minDigits = 1,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
children: number
|
|
||||||
className?: string
|
|
||||||
preCount?: number
|
|
||||||
minDigits?: number
|
|
||||||
}) {
|
|
||||||
const currentDigits = count.toString().split("")
|
|
||||||
const previousDigits = (
|
|
||||||
preCount !== undefined ? preCount.toString() : count - 1 >= 0 ? (count - 1).toString() : "0"
|
|
||||||
).split("")
|
|
||||||
|
|
||||||
// Ensure both numbers meet the minimum length requirement and maintain the same length for animation
|
|
||||||
const maxLength = Math.max(previousDigits.length, currentDigits.length, minDigits)
|
|
||||||
while (previousDigits.length < maxLength) {
|
|
||||||
previousDigits.unshift("0")
|
|
||||||
}
|
|
||||||
while (currentDigits.length < maxLength) {
|
|
||||||
currentDigits.unshift("0")
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...props} className={cn("flex h-[1em] items-center", className)}>
|
|
||||||
{currentDigits.map((digit, index) => {
|
|
||||||
const hasChanged = digit !== previousDigits[index]
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`${index}-${digit}`}
|
|
||||||
className={cn("relative flex h-full min-w-[0.6em] items-center text-center", {
|
|
||||||
"min-w-[0.2em]": digit === ".",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-hidden
|
|
||||||
data-issues-count-exit
|
|
||||||
className={cn(
|
|
||||||
"absolute inset-0 flex items-center justify-center",
|
|
||||||
hasChanged ? "animate" : "opacity-0",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{previousDigits[index]}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
data-issues-count-enter
|
|
||||||
className={cn(
|
|
||||||
"absolute inset-0 flex items-center justify-center",
|
|
||||||
hasChanged && "animate",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{digit}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { Home, Languages, Moon, Sun, SunMoon } from "lucide-react"
|
|
||||||
|
|
||||||
import { useServerData } from "@/app/context/server-data-context"
|
|
||||||
import {
|
|
||||||
CommandDialog,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
CommandSeparator,
|
|
||||||
} from "@/components/ui/command"
|
|
||||||
import { localeItems } from "@/i18n-metadata"
|
|
||||||
import { setUserLocale } from "@/i18n/locale"
|
|
||||||
import { useTranslations } from "next-intl"
|
|
||||||
import { useTheme } from "next-themes"
|
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
|
|
||||||
export function DashCommand() {
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const [search, setSearch] = useState("")
|
|
||||||
const { data } = useServerData()
|
|
||||||
const router = useRouter()
|
|
||||||
const { setTheme } = useTheme()
|
|
||||||
const t = useTranslations("DashCommand")
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const down = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
||||||
e.preventDefault()
|
|
||||||
setOpen((open) => !open)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("keydown", down)
|
|
||||||
return () => document.removeEventListener("keydown", down)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (!data?.result) return null
|
|
||||||
|
|
||||||
const sortedServers = data.result.sort((a, b) => {
|
|
||||||
const displayIndexDiff = (b.display_index || 0) - (a.display_index || 0)
|
|
||||||
if (displayIndexDiff !== 0) return displayIndexDiff
|
|
||||||
return a.id - b.id
|
|
||||||
})
|
|
||||||
|
|
||||||
const languageShortcuts = localeItems.map((item) => ({
|
|
||||||
keywords: ["language", "locale", item.code.toLowerCase()],
|
|
||||||
icon: <Languages />,
|
|
||||||
label: item.name,
|
|
||||||
action: () => setUserLocale(item.code),
|
|
||||||
value: `language ${item.name.toLowerCase()} ${item.code}`,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const shortcuts = [
|
|
||||||
{
|
|
||||||
keywords: ["home", "homepage"],
|
|
||||||
icon: <Home />,
|
|
||||||
label: t("Home"),
|
|
||||||
action: () => router.push("/"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keywords: ["light", "theme", "lightmode"],
|
|
||||||
icon: <Sun />,
|
|
||||||
label: t("ToggleLightMode"),
|
|
||||||
action: () => setTheme("light"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keywords: ["dark", "theme", "darkmode"],
|
|
||||||
icon: <Moon />,
|
|
||||||
label: t("ToggleDarkMode"),
|
|
||||||
action: () => setTheme("dark"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keywords: ["system", "theme", "systemmode"],
|
|
||||||
icon: <SunMoon />,
|
|
||||||
label: t("ToggleSystemMode"),
|
|
||||||
action: () => setTheme("system"),
|
|
||||||
},
|
|
||||||
...languageShortcuts,
|
|
||||||
].map((item) => ({
|
|
||||||
...item,
|
|
||||||
value: `${item.keywords.join(" ")} ${item.label}`,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
||||||
<CommandInput placeholder={t("TypeCommand")} value={search} onValueChange={setSearch} />
|
|
||||||
<CommandList className="border-t">
|
|
||||||
<CommandEmpty>{t("NoResults")}</CommandEmpty>
|
|
||||||
<CommandGroup heading={t("Servers")}>
|
|
||||||
{sortedServers.map((server) => (
|
|
||||||
<CommandItem
|
|
||||||
key={server.id}
|
|
||||||
value={server.name}
|
|
||||||
onSelect={() => {
|
|
||||||
router.push(`/server/${server.id}`)
|
|
||||||
setOpen(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{server.online_status ? (
|
|
||||||
<span className="h-2 w-2 shrink-0 self-center rounded-full bg-green-500" />
|
|
||||||
) : (
|
|
||||||
<span className="h-2 w-2 shrink-0 self-center rounded-full bg-red-500" />
|
|
||||||
)}
|
|
||||||
<span>{server.name}</span>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
<CommandSeparator />
|
|
||||||
|
|
||||||
<CommandGroup heading={t("Shortcuts")}>
|
|
||||||
{shortcuts.map((item) => (
|
|
||||||
<CommandItem
|
|
||||||
key={item.label}
|
|
||||||
value={item.value}
|
|
||||||
onSelect={() => {
|
|
||||||
item.action()
|
|
||||||
setOpen(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.icon}
|
|
||||||
<span>{item.label}</span>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</CommandDialog>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
31
components/GlobalBackButton.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useFilter } from "@/lib/network-filter-context"
|
||||||
|
import { useStatus } from "@/lib/status-context"
|
||||||
|
import { ServerStackIcon } from "@heroicons/react/20/solid"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
|
export default function GlobalBackButton() {
|
||||||
|
const router = useRouter()
|
||||||
|
const { setStatus } = useStatus()
|
||||||
|
const { setFilter } = useFilter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setStatus("all")
|
||||||
|
setFilter(false)
|
||||||
|
sessionStorage.removeItem("selectedTag")
|
||||||
|
router.prefetch(`/`)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
router.push(`/`)
|
||||||
|
}}
|
||||||
|
className="rounded-[50px] mt-[1px] w-fit text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-green-600 hover:bg-green-500 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] hover:shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] "
|
||||||
|
>
|
||||||
|
<ServerStackIcon className="size-[13px]" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
@ -2,7 +2,7 @@ import Image from "next/image"
|
|||||||
|
|
||||||
export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||||
return (
|
return (
|
||||||
<svg role="img" aria-label="github-icon" 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>
|
||||||
)
|
)
|
||||||
|
@ -10,8 +10,9 @@ import {
|
|||||||
import { localeItems } from "@/i18n-metadata"
|
import { localeItems } from "@/i18n-metadata"
|
||||||
import { setUserLocale } from "@/i18n/locale"
|
import { setUserLocale } from "@/i18n/locale"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { CheckCircleIcon, LanguageIcon } from "@heroicons/react/20/solid"
|
import { CheckCircleIcon } from "@heroicons/react/20/solid"
|
||||||
import { useLocale } from "next-intl"
|
import { useLocale } from "next-intl"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
export function LanguageSwitcher() {
|
export function LanguageSwitcher() {
|
||||||
const locale = useLocale()
|
const locale = useLocale()
|
||||||
@ -27,9 +28,9 @@ export function LanguageSwitcher() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="cursor-pointer rounded-full bg-white px-[9px] hover:bg-accent/50 dark:bg-black dark:hover:bg-accent/50"
|
className="rounded-full px-[9px] bg-white dark:bg-black cursor-pointer hover:bg-accent/50 dark:hover:bg-accent/50"
|
||||||
>
|
>
|
||||||
<LanguageIcon className="size-4" />
|
{localeItems.find((item) => item.code === locale)?.name}
|
||||||
<span className="sr-only">Change language</span>
|
<span className="sr-only">Change language</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@ -40,7 +41,7 @@ export function LanguageSwitcher() {
|
|||||||
onSelect={(e) => handleSelect(e, item.code)}
|
onSelect={(e) => handleSelect(e, item.code)}
|
||||||
className={cn(
|
className={cn(
|
||||||
{
|
{
|
||||||
"gap-3 bg-muted font-semibold": locale === item.code,
|
"bg-muted gap-3": locale === item.code,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rounded-t-[5px]": index === localeItems.length - 1,
|
"rounded-t-[5px]": index === localeItems.length - 1,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { NezhaAPISafe } from "@/app/types/nezha-api"
|
import { NezhaAPISafe } from "@/app/types/nezha-api"
|
||||||
import ServerFlag from "@/components/ServerFlag"
|
import ServerFlag from "@/components/ServerFlag"
|
||||||
import ServerUsageBar from "@/components/ServerUsageBar"
|
import ServerUsageBar from "@/components/ServerUsageBar"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
@ -9,11 +9,7 @@ import { cn, formatBytes, formatNezhaInfo } from "@/lib/utils"
|
|||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
|
||||||
export default function ServerCard({
|
export default function ServerCard({ serverInfo }: { serverInfo: NezhaAPISafe }) {
|
||||||
serverInfo,
|
|
||||||
}: {
|
|
||||||
serverInfo: NezhaAPISafe
|
|
||||||
}) {
|
|
||||||
const t = useTranslations("ServerCard")
|
const t = useTranslations("ServerCard")
|
||||||
const { id, name, country_code, online, cpu, up, down, mem, stg, host } =
|
const { id, name, country_code, online, cpu, up, down, mem, stg, host } =
|
||||||
formatNezhaInfo(serverInfo)
|
formatNezhaInfo(serverInfo)
|
||||||
@ -30,7 +26,7 @@ export default function ServerCard({
|
|||||||
<Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
|
<Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-pointer flex-col items-center justify-start gap-3 p-3 hover:border-stone-300 hover:shadow-md md:px-5 dark:hover:border-stone-700",
|
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
|
||||||
{
|
{
|
||||||
"flex-col": fixedTopServerName,
|
"flex-col": fixedTopServerName,
|
||||||
"lg:flex-row": !fixedTopServerName,
|
"lg:flex-row": !fixedTopServerName,
|
||||||
@ -43,7 +39,7 @@ export default function ServerCard({
|
|||||||
})}
|
})}
|
||||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||||
>
|
>
|
||||||
<span className="h-2 w-2 shrink-0 self-center rounded-full bg-green-500" />
|
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-center",
|
"flex items-center justify-center",
|
||||||
@ -70,8 +66,8 @@ export default function ServerCard({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{fixedTopServerName && (
|
{fixedTopServerName && (
|
||||||
<div className={"col-span-1 hidden items-center gap-2 lg:flex lg:flex-row"}>
|
<div className={"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"}>
|
||||||
<div className="font-semibold text-xs">
|
<div className="text-xs font-semibold">
|
||||||
{host.Platform.includes("Windows") ? (
|
{host.Platform.includes("Windows") ? (
|
||||||
<MageMicrosoftWindows className="size-[10px]" />
|
<MageMicrosoftWindows className="size-[10px]" />
|
||||||
) : (
|
) : (
|
||||||
@ -79,37 +75,37 @@ export default function ServerCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-14 flex-col"}>
|
<div className={"flex w-14 flex-col"}>
|
||||||
<p className="text-muted-foreground text-xs">{t("System")}</p>
|
<p className="text-xs text-muted-foreground">{t("System")}</p>
|
||||||
<div className="flex items-center font-semibold text-[10.5px]">
|
<div className="flex items-center text-[10.5px] font-semibold">
|
||||||
{host.Platform.includes("Windows") ? "Windows" : GetOsName(host.Platform)}
|
{host.Platform.includes("Windows") ? "Windows" : GetOsName(host.Platform)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={"flex w-14 flex-col"}>
|
<div className={"flex w-14 flex-col"}>
|
||||||
<p className="text-muted-foreground text-xs">{t("CPU")}</p>
|
<p className="text-xs text-muted-foreground">{t("CPU")}</p>
|
||||||
<div className="flex items-center font-semibold text-xs">{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-muted-foreground text-xs">{t("Mem")}</p>
|
<p className="text-xs text-muted-foreground">{t("Mem")}</p>
|
||||||
<div className="flex items-center font-semibold text-xs">{mem.toFixed(2)}%</div>
|
<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-muted-foreground text-xs">{t("STG")}</p>
|
<p className="text-xs text-muted-foreground">{t("STG")}</p>
|
||||||
<div className="flex items-center font-semibold text-xs">{stg.toFixed(2)}%</div>
|
<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-muted-foreground text-xs">{t("Upload")}</p>
|
<p className="text-xs text-muted-foreground">{t("Upload")}</p>
|
||||||
<div className="flex items-center font-semibold text-xs">
|
<div className="flex items-center text-xs font-semibold">
|
||||||
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`}
|
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-14 flex-col"}>
|
<div className={"flex w-14 flex-col"}>
|
||||||
<p className="text-muted-foreground text-xs">{t("Download")}</p>
|
<p className="text-xs text-muted-foreground">{t("Download")}</p>
|
||||||
<div className="flex items-center font-semibold text-xs">
|
<div className="flex items-center text-xs font-semibold">
|
||||||
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`}
|
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -118,13 +114,13 @@ export default function ServerCard({
|
|||||||
<section className={"flex items-center justify-between gap-1"}>
|
<section className={"flex items-center justify-between gap-1"}>
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="flex-1 items-center justify-center text-nowrap rounded-[8px] border-muted-50 text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none"
|
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] border-muted-50 shadow-md shadow-neutral-200/30 dark:shadow-none"
|
||||||
>
|
>
|
||||||
{t("Upload")}:{formatBytes(serverInfo.status.NetOutTransfer)}
|
{t("Upload")}:{formatBytes(serverInfo.status.NetOutTransfer)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex-1 items-center justify-center text-nowrap rounded-[8px] text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none"
|
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none"
|
||||||
>
|
>
|
||||||
{t("Download")}:{formatBytes(serverInfo.status.NetInTransfer)}
|
{t("Download")}:{formatBytes(serverInfo.status.NetInTransfer)}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -137,8 +133,8 @@ export default function ServerCard({
|
|||||||
<Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
|
<Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-pointer flex-col items-center justify-start gap-3 p-3 hover:border-stone-300 hover:shadow-md md:px-5 dark:hover:border-stone-700",
|
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
|
||||||
showNetTransfer ? "min-h-[123px] lg:min-h-[91px]" : "min-h-[93px] lg:min-h-[61px]",
|
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,
|
||||||
@ -151,7 +147,7 @@ export default function ServerCard({
|
|||||||
})}
|
})}
|
||||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||||
>
|
>
|
||||||
<span className="h-2 w-2 shrink-0 self-center rounded-full bg-red-500" />
|
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-center",
|
"flex items-center justify-center",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { NezhaAPISafe } from "@/app/types/nezha-api"
|
import { NezhaAPISafe } from "@/app/types/nezha-api"
|
||||||
import ServerFlag from "@/components/ServerFlag"
|
import ServerFlag from "@/components/ServerFlag"
|
||||||
import ServerUsageBar from "@/components/ServerUsageBar"
|
import ServerUsageBar from "@/components/ServerUsageBar"
|
||||||
import { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
@ -10,11 +10,7 @@ import Link from "next/link"
|
|||||||
|
|
||||||
import { Separator } from "./ui/separator"
|
import { Separator } from "./ui/separator"
|
||||||
|
|
||||||
export default function ServerCardInline({
|
export default function ServerCardInline({ serverInfo }: { serverInfo: NezhaAPISafe }) {
|
||||||
serverInfo,
|
|
||||||
}: {
|
|
||||||
serverInfo: NezhaAPISafe
|
|
||||||
}) {
|
|
||||||
const t = useTranslations("ServerCard")
|
const t = useTranslations("ServerCard")
|
||||||
const { id, name, country_code, online, cpu, up, down, mem, stg, host } =
|
const { id, name, country_code, online, cpu, up, down, mem, stg, host } =
|
||||||
formatNezhaInfo(serverInfo)
|
formatNezhaInfo(serverInfo)
|
||||||
@ -29,14 +25,14 @@ export default function ServerCardInline({
|
|||||||
<Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
|
<Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full min-w-[900px] cursor-pointer items-center justify-start gap-3 p-3 hover:border-stone-300 hover:shadow-md md:px-5 lg:flex-row dark:hover:border-stone-700",
|
"flex items-center lg:flex-row justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors min-w-[900px] w-full",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<section
|
<section
|
||||||
className={cn("grid items-center gap-2 lg:w-36")}
|
className={cn("grid items-center gap-2 lg:w-36")}
|
||||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||||
>
|
>
|
||||||
<span className="h-2 w-2 shrink-0 self-center rounded-full bg-green-500" />
|
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-center",
|
"flex items-center justify-center",
|
||||||
@ -56,11 +52,11 @@ export default function ServerCardInline({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<Separator orientation="vertical" className="mx-0 ml-2 h-8" />
|
<Separator orientation="vertical" className="h-8 mx-0 ml-2" />
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<section className={cn("grid flex-1 grid-cols-9 items-center gap-3")}>
|
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
|
||||||
<div className={"flex flex-row items-center gap-2 whitespace-nowrap"}>
|
<div className={"items-center flex flex-row gap-2 whitespace-nowrap"}>
|
||||||
<div className="font-semibold text-xs">
|
<div className="text-xs font-semibold">
|
||||||
{host.Platform.includes("Windows") ? (
|
{host.Platform.includes("Windows") ? (
|
||||||
<MageMicrosoftWindows className="size-[10px]" />
|
<MageMicrosoftWindows className="size-[10px]" />
|
||||||
) : (
|
) : (
|
||||||
@ -68,54 +64,54 @@ export default function ServerCardInline({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-14 flex-col"}>
|
<div className={"flex w-14 flex-col"}>
|
||||||
<p className="text-muted-foreground text-xs">{t("System")}</p>
|
<p className="text-xs text-muted-foreground">{t("System")}</p>
|
||||||
<div className="flex items-center font-semibold text-[10.5px]">
|
<div className="flex items-center text-[10.5px] font-semibold">
|
||||||
{host.Platform.includes("Windows") ? "Windows" : GetOsName(host.Platform)}
|
{host.Platform.includes("Windows") ? "Windows" : GetOsName(host.Platform)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-20 flex-col"}>
|
<div className={"flex w-20 flex-col"}>
|
||||||
<p className="text-muted-foreground text-xs">{t("Uptime")}</p>
|
<p className="text-xs text-muted-foreground">{t("Uptime")}</p>
|
||||||
<div className="flex items-center font-semibold text-xs">
|
<div className="flex items-center text-xs font-semibold">
|
||||||
{(serverInfo?.status.Uptime / 86400).toFixed(0)} {"Days"}
|
{(serverInfo?.status.Uptime / 86400).toFixed(0)} {"Days"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-14 flex-col"}>
|
<div className={"flex w-14 flex-col"}>
|
||||||
<p className="text-muted-foreground text-xs">{t("CPU")}</p>
|
<p className="text-xs text-muted-foreground">{t("CPU")}</p>
|
||||||
<div className="flex items-center font-semibold text-xs">{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-muted-foreground text-xs">{t("Mem")}</p>
|
<p className="text-xs text-muted-foreground">{t("Mem")}</p>
|
||||||
<div className="flex items-center font-semibold text-xs">{mem.toFixed(2)}%</div>
|
<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-muted-foreground text-xs">{t("STG")}</p>
|
<p className="text-xs text-muted-foreground">{t("STG")}</p>
|
||||||
<div className="flex items-center font-semibold text-xs">{stg.toFixed(2)}%</div>
|
<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-muted-foreground text-xs">{t("Upload")}</p>
|
<p className="text-xs text-muted-foreground">{t("Upload")}</p>
|
||||||
<div className="flex items-center font-semibold text-xs">
|
<div className="flex items-center text-xs font-semibold">
|
||||||
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`}
|
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-16 flex-col"}>
|
<div className={"flex w-16 flex-col"}>
|
||||||
<p className="text-muted-foreground text-xs">{t("Download")}</p>
|
<p className="text-xs text-muted-foreground">{t("Download")}</p>
|
||||||
<div className="flex items-center font-semibold text-xs">
|
<div className="flex items-center text-xs font-semibold">
|
||||||
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`}
|
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-20 flex-col"}>
|
<div className={"flex w-20 flex-col"}>
|
||||||
<p className="text-muted-foreground text-xs">{t("TotalUpload")}</p>
|
<p className="text-xs text-muted-foreground">{t("TotalUpload")}</p>
|
||||||
<div className="flex items-center font-semibold text-xs">
|
<div className="flex items-center text-xs font-semibold">
|
||||||
{formatBytes(serverInfo.status.NetOutTransfer)}
|
{formatBytes(serverInfo.status.NetOutTransfer)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-20 flex-col"}>
|
<div className={"flex w-20 flex-col"}>
|
||||||
<p className="text-muted-foreground text-xs">{t("TotalDownload")}</p>
|
<p className="text-xs text-muted-foreground">{t("TotalDownload")}</p>
|
||||||
<div className="flex items-center font-semibold text-xs">
|
<div className="flex items-center text-xs font-semibold">
|
||||||
{formatBytes(serverInfo.status.NetInTransfer)}
|
{formatBytes(serverInfo.status.NetInTransfer)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -124,34 +120,27 @@ export default function ServerCardInline({
|
|||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
|
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-[61px] min-w-[900px] flex-row items-center justify-start gap-3 p-3 hover:border-stone-300 hover:shadow-md md:px-5 dark:hover:border-stone-700",
|
"flex items-center justify-start gap-3 p-3 md:px-5 min-h-[61px] min-w-[900px] flex-row",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<section
|
<section
|
||||||
className={cn("grid items-center gap-2 lg:w-40")}
|
className={cn("grid items-center gap-2 lg:w-40")}
|
||||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||||
>
|
>
|
||||||
<span className="h-2 w-2 shrink-0 self-center rounded-full bg-red-500" />
|
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}
|
||||||
"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">
|
<div className="relative w-28">
|
||||||
<p
|
<p className={cn("break-all font-bold tracking-tight", showFlag ? "text-xs" : "text-sm")}>
|
||||||
className={cn("break-all font-bold tracking-tight", showFlag ? "text-xs" : "text-sm")}
|
|
||||||
>
|
|
||||||
{name}
|
{name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
69
components/ServerCardPopover.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { NezhaAPISafe } from "@/app/types/nezha-api"
|
||||||
|
import { cn, formatBytes } from "@/lib/utils"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
|
|
||||||
|
export function ServerCardPopoverCard({
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
className?: string
|
||||||
|
title: string
|
||||||
|
content?: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={cn("mb-[6px] flex w-full flex-col", className)}>
|
||||||
|
<div className="text-sm font-semibold">{title}</div>
|
||||||
|
{children ? children : <div className="break-all text-xs font-medium">{content}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ServerCardPopover({
|
||||||
|
host,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
host: NezhaAPISafe["host"]
|
||||||
|
status: NezhaAPISafe["status"]
|
||||||
|
}) {
|
||||||
|
const t = useTranslations("ServerCardPopover")
|
||||||
|
return (
|
||||||
|
<section className="max-w-[300px]">
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title={t("System")}
|
||||||
|
content={`${host.Platform}-${host.PlatformVersion} [${host.Virtualization}: ${host.Arch}]`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title={t("CPU")}
|
||||||
|
content={`${host.CPU.map((item) => item).join(", ")}`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title={t("Mem")}
|
||||||
|
content={`${formatBytes(status.MemUsed)} / ${formatBytes(host.MemTotal)}`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title={t("STG")}
|
||||||
|
content={`${formatBytes(status.DiskUsed)} / ${formatBytes(host.DiskTotal)}`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title={t("Swap")}
|
||||||
|
content={`${formatBytes(status.SwapUsed)} / ${formatBytes(host.SwapTotal)}`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title={t("Network")}
|
||||||
|
content={`${formatBytes(status.NetOutTransfer)} / ${formatBytes(status.NetInTransfer)}`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
title={t("Load")}
|
||||||
|
content={`${status.Load1.toFixed(2)} / ${status.Load5.toFixed(2)} / ${status.Load15.toFixed(2)}`}
|
||||||
|
/>
|
||||||
|
<ServerCardPopoverCard
|
||||||
|
className="mb-0"
|
||||||
|
title={t("Online")}
|
||||||
|
content={`${(status.Uptime / 86400).toFixed(0)} Days`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { Progress } from "@/components/ui/progress"
|
import { Progress } from "@/components/ui/progress"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
type ServerUsageBarProps = {
|
type ServerUsageBarProps = {
|
||||||
value: number
|
value: number
|
||||||
|
@ -48,27 +48,23 @@ export function SignIn() {
|
|||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<form
|
<form className="flex flex-col items-center justify-start gap-4 p-4 " onSubmit={handleSubmit}>
|
||||||
className="flex flex-1 flex-col items-center justify-center gap-4 p-4 "
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
<input type="hidden" name="csrfToken" value={csrfToken} />
|
<input type="hidden" name="csrfToken" value={csrfToken} />
|
||||||
<section className="flex flex-col items-start gap-2">
|
<section className="flex flex-col items-start gap-2">
|
||||||
<label className="flex flex-col items-start gap-1 ">
|
<label className="flex flex-col items-start gap-1 ">
|
||||||
{errorState && <p className="font-semibold text-red-500 text-sm">{t("ErrorMessage")}</p>}
|
{errorState && <p className="text-red-500 text-sm font-semibold">{t("ErrorMessage")}</p>}
|
||||||
{successState && (
|
{successState && (
|
||||||
<p className="font-semibold text-green-500 text-sm">{t("SuccessMessage")}</p>
|
<p className="text-green-500 text-sm font-semibold">{t("SuccessMessage")}</p>
|
||||||
)}
|
)}
|
||||||
<p className="font-semibold text-base">{t("SignInMessage")}</p>
|
<p className="text-base font-semibold">{t("SignInMessage")}</p>
|
||||||
<input
|
<input
|
||||||
className="rounded-[5px] border-[1px] border-stone-300 px-1 dark:border-stone-800"
|
className="px-1 border-[1px] rounded-[5px] border-stone-300 dark:border-stone-800"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
className="px-1.5 py-0.5 w-fit flex items-center gap-1 text-sm font-semibold border-stone-300 dark:border-stone-800 rounded-[8px] border bg-card hover:brightness-95 transition-all text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none"
|
||||||
className="flex w-fit items-center gap-1 rounded-[8px] border border-stone-300 bg-card px-1.5 py-0.5 font-semibold text-card-foreground text-sm shadow-lg shadow-neutral-200/40 transition-all hover:brightness-95 dark:border-stone-800 dark:shadow-none"
|
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{t("Submit")}
|
{t("Submit")}
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useLocale, useTranslations } from "next-intl"
|
import { m } from "framer-motion"
|
||||||
import { createRef, useEffect, useRef, useState } from "react"
|
import { useTranslations } from "next-intl"
|
||||||
|
import React, { createRef, useEffect, useRef } from "react"
|
||||||
|
|
||||||
export default function Switch({
|
export default function Switch({
|
||||||
allTag,
|
allTag,
|
||||||
@ -19,9 +20,6 @@ export default function Switch({
|
|||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
const tagRefs = useRef(allTag.map(() => createRef<HTMLDivElement>()))
|
const tagRefs = useRef(allTag.map(() => createRef<HTMLDivElement>()))
|
||||||
const t = useTranslations("ServerListClient")
|
const t = useTranslations("ServerListClient")
|
||||||
const locale = useLocale()
|
|
||||||
const [indicator, setIndicator] = useState<{ x: number; w: number } | null>(null)
|
|
||||||
const [isFirstRender, setIsFirstRender] = useState(true)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedTag = sessionStorage.getItem("selectedTag")
|
const savedTag = sessionStorage.getItem("selectedTag")
|
||||||
@ -50,76 +48,47 @@ export default function Switch({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentTagElement = tagRefs.current[allTag.indexOf(nowTag)]?.current
|
const currentTagRef = tagRefs.current[allTag.indexOf(nowTag)]
|
||||||
if (currentTagElement) {
|
if (currentTagRef && currentTagRef.current) {
|
||||||
setIndicator({
|
currentTagRef.current.scrollIntoView({
|
||||||
x: currentTagElement.offsetLeft,
|
|
||||||
w: currentTagElement.offsetWidth,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFirstRender) {
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsFirstRender(false)
|
|
||||||
}, 50)
|
|
||||||
}
|
|
||||||
}, [nowTag, locale, allTag, isFirstRender])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const currentTagElement = tagRefs.current[allTag.indexOf(nowTag)]?.current
|
|
||||||
const container = scrollRef.current
|
|
||||||
|
|
||||||
if (currentTagElement && container) {
|
|
||||||
const containerRect = container.getBoundingClientRect()
|
|
||||||
const tagRect = currentTagElement.getBoundingClientRect()
|
|
||||||
|
|
||||||
const scrollLeft = currentTagElement.offsetLeft - (containerRect.width - tagRect.width) / 2
|
|
||||||
|
|
||||||
container.scrollTo({
|
|
||||||
left: Math.max(0, scrollLeft),
|
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
|
block: "nearest",
|
||||||
|
inline: "center",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [nowTag, locale])
|
}, [nowTag])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]"
|
className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]"
|
||||||
>
|
>
|
||||||
<div className="relative flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800">
|
<div className="flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800">
|
||||||
{indicator && (
|
|
||||||
<div
|
|
||||||
className="absolute top-[3px] left-0 z-10 h-[35px] bg-white shadow-black/5 shadow-lg dark:bg-stone-700 dark:shadow-white/5"
|
|
||||||
style={{
|
|
||||||
borderRadius: 24,
|
|
||||||
width: `${indicator.w}px`,
|
|
||||||
transform: `translateX(${indicator.x}px)`,
|
|
||||||
transition: isFirstRender ? "none" : "all 0.5s cubic-bezier(0.4, 0, 0.2, 1)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{allTag.map((tag, index) => (
|
{allTag.map((tag, index) => (
|
||||||
<div
|
<div
|
||||||
key={tag}
|
key={tag}
|
||||||
ref={tagRefs.current[index]}
|
ref={tagRefs.current[index]}
|
||||||
onClick={() => {
|
onClick={() => onTagChange(tag)}
|
||||||
onTagChange(tag)
|
|
||||||
sessionStorage.setItem("selectedTag", tag)
|
|
||||||
}}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] font-[600] text-[13px]",
|
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500",
|
||||||
"text-stone-400 transition-all duration-500 ease-in-out hover:text-stone-950 dark:text-stone-500 hover:dark:text-stone-50",
|
nowTag === tag ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500",
|
||||||
{
|
|
||||||
"text-stone-950 dark:text-stone-50": nowTag === tag,
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{nowTag === tag && (
|
||||||
|
<m.div
|
||||||
|
layoutId="nav-item"
|
||||||
|
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
|
||||||
|
style={{
|
||||||
|
originY: "0px",
|
||||||
|
borderRadius: 46,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="relative z-20 flex items-center gap-1">
|
<div className="relative z-20 flex items-center gap-1">
|
||||||
<div className="flex items-center gap-2 whitespace-nowrap">
|
<div className="whitespace-nowrap flex items-center gap-2">
|
||||||
{tag === "defaultTag" ? t("defaultTag") : tag}{" "}
|
{tag === "defaultTag" ? t("defaultTag") : tag}{" "}
|
||||||
{getEnv("NEXT_PUBLIC_ShowTagCount") === "true" && tag !== "defaultTag" && (
|
{getEnv("NEXT_PUBLIC_ShowTagCount") === "true" && tag !== "defaultTag" && (
|
||||||
<div className="w-fit rounded-full bg-muted px-1.5">{tagCountMap[tag]}</div>
|
<div className="w-fit px-1.5 rounded-full bg-muted">{tagCountMap[tag]}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useLocale, useTranslations } from "next-intl"
|
import { m } from "framer-motion"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useTranslations } from "next-intl"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
export default function TabSwitch({
|
export default function TabSwitch({
|
||||||
tabs,
|
tabs,
|
||||||
@ -14,56 +15,30 @@ export default function TabSwitch({
|
|||||||
setCurrentTab: (tab: string) => void
|
setCurrentTab: (tab: string) => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations("TabSwitch")
|
const t = useTranslations("TabSwitch")
|
||||||
const [indicator, setIndicator] = useState<{ x: number; w: number }>({
|
|
||||||
x: 0,
|
|
||||||
w: 0,
|
|
||||||
})
|
|
||||||
const tabRefs = useRef<(HTMLDivElement | null)[]>([])
|
|
||||||
const locale = useLocale()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const currentTabElement = tabRefs.current[tabs.indexOf(currentTab)]
|
|
||||||
if (currentTabElement) {
|
|
||||||
const parentPadding = 1
|
|
||||||
setIndicator({
|
|
||||||
x:
|
|
||||||
tabs.indexOf(currentTab) !== 0
|
|
||||||
? currentTabElement.offsetLeft - parentPadding
|
|
||||||
: currentTabElement.offsetLeft,
|
|
||||||
w: currentTabElement.offsetWidth,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [currentTab, tabs, locale])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="z-50 flex flex-col items-start rounded-[50px]">
|
<div className="z-50 flex flex-col items-start rounded-[50px]">
|
||||||
<div className="relative flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800">
|
<div className="flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800">
|
||||||
{indicator.w > 0 && (
|
{tabs.map((tab: string) => (
|
||||||
<div
|
<div
|
||||||
className="absolute top-[3px] left-0 z-10 h-[35px] bg-white shadow-black/5 shadow-lg dark:bg-stone-700 dark:shadow-white/5"
|
key={tab}
|
||||||
|
onClick={() => setCurrentTab(tab)}
|
||||||
|
className={cn(
|
||||||
|
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500",
|
||||||
|
currentTab === tab
|
||||||
|
? "text-black dark:text-white"
|
||||||
|
: "text-stone-400 dark:text-stone-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{currentTab === tab && (
|
||||||
|
<m.div
|
||||||
|
layoutId="tab-switch"
|
||||||
|
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 24,
|
originY: "0px",
|
||||||
width: `${indicator.w}px`,
|
borderRadius: 46,
|
||||||
transform: `translateX(${indicator.x}px)`,
|
|
||||||
transition: "all 0.5s cubic-bezier(0.4, 0, 0.2, 1)",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tabs.map((tab: string, index) => (
|
|
||||||
<div
|
|
||||||
key={tab}
|
|
||||||
ref={(el) => {
|
|
||||||
tabRefs.current[index] = el
|
|
||||||
}}
|
|
||||||
onClick={() => setCurrentTab(tab)}
|
|
||||||
className={cn(
|
|
||||||
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] font-[600] text-[13px]",
|
|
||||||
"text-stone-400 transition-all duration-500 ease-in-out hover:text-stone-950 dark:text-stone-500 hover:dark:text-stone-50",
|
|
||||||
{
|
|
||||||
"text-stone-950 dark:text-stone-50": currentTab === tab,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<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(tab)}</p>
|
<p className="whitespace-nowrap">{t(tab)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,28 +4,23 @@ import { Button } from "@/components/ui/button"
|
|||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { CheckIcon, MinusIcon, Moon, Sun } from "lucide-react"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { CheckCircleIcon } from "@heroicons/react/20/solid"
|
||||||
|
import { Moon, Sun } from "lucide-react"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import { useId } from "react"
|
|
||||||
import { RadioGroup, RadioGroupItem } from "./ui/radio-group"
|
|
||||||
|
|
||||||
const items = [
|
|
||||||
{ value: "light", label: "Light", image: "/ui-light.png" },
|
|
||||||
{ value: "dark", label: "Dark", image: "/ui-dark.png" },
|
|
||||||
{ value: "system", label: "System", image: "/ui-system.png" },
|
|
||||||
]
|
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const { setTheme, theme } = useTheme()
|
const { setTheme, theme } = useTheme()
|
||||||
const t = useTranslations("ThemeSwitcher")
|
const t = useTranslations("ThemeSwitcher")
|
||||||
|
|
||||||
const handleSelect = (newTheme: string) => {
|
const handleSelect = (e: Event, newTheme: string) => {
|
||||||
|
e.preventDefault()
|
||||||
setTheme(newTheme)
|
setTheme(newTheme)
|
||||||
}
|
}
|
||||||
const id = useId()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@ -33,47 +28,32 @@ export function ModeToggle() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="cursor-pointer rounded-full bg-white px-[9px] hover:bg-accent/50 dark:bg-black dark:hover:bg-accent/50"
|
className="rounded-full px-[9px] bg-white dark:bg-black cursor-pointer hover:bg-accent/50 dark:hover:bg-accent/50"
|
||||||
>
|
>
|
||||||
<Sun className="dark:-rotate-90 h-4 w-4 rotate-0 scale-100 transition-all dark:scale-0" />
|
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="px-2 pt-2 pb-1.5" align="end">
|
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
||||||
<fieldset className="space-y-4">
|
<DropdownMenuItem
|
||||||
<RadioGroup className="flex gap-2" defaultValue={theme} onValueChange={handleSelect}>
|
className={cn("rounded-b-[5px]", { "gap-3 bg-muted": theme === "light" })}
|
||||||
{items.map((item) => (
|
onSelect={(e) => handleSelect(e, "light")}
|
||||||
<label key={`${id}-${item.value}`}>
|
>
|
||||||
<RadioGroupItem
|
{t("Light")} {theme === "light" && <CheckCircleIcon className="size-4" />}
|
||||||
id={`${id}-${item.value}`}
|
</DropdownMenuItem>
|
||||||
value={item.value}
|
<DropdownMenuItem
|
||||||
className="peer sr-only after:absolute after:inset-0"
|
className={cn("rounded-[5px]", { "gap-3 bg-muted": theme === "dark" })}
|
||||||
/>
|
onSelect={(e) => handleSelect(e, "dark")}
|
||||||
<img
|
>
|
||||||
src={item.image}
|
{t("Dark")} {theme === "dark" && <CheckCircleIcon className="size-4" />}
|
||||||
alt={item.label}
|
</DropdownMenuItem>
|
||||||
width={88}
|
<DropdownMenuItem
|
||||||
height={70}
|
className={cn("rounded-t-[5px]", { "gap-3 bg-muted": theme === "system" })}
|
||||||
className="relative cursor-pointer overflow-hidden rounded-[8px] border border-neutral-300 shadow-xs outline-none transition-[color,box-shadow] peer-focus-visible:ring-[3px] peer-focus-visible:ring-ring/50 peer-data-disabled:cursor-not-allowed peer-data-[state=checked]:bg-accent peer-data-disabled:opacity-50 dark:border-neutral-700"
|
onSelect={(e) => handleSelect(e, "system")}
|
||||||
/>
|
>
|
||||||
<span className="group mt-2 flex items-center gap-1 peer-data-[state=unchecked]:text-muted-foreground/70">
|
{t("System")} {theme === "system" && <CheckCircleIcon className="size-4" />}
|
||||||
<CheckIcon
|
</DropdownMenuItem>
|
||||||
size={16}
|
|
||||||
className="group-peer-data-[state=unchecked]:hidden"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<MinusIcon
|
|
||||||
size={16}
|
|
||||||
className="group-peer-data-[state=checked]:hidden"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="font-medium text-xs">{t(item.label)}</span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</fieldset>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)
|
)
|
||||||
|
@ -6,7 +6,7 @@ import { useTranslations } from "next-intl"
|
|||||||
export default function GlobalLoading() {
|
export default function GlobalLoading() {
|
||||||
const t = useTranslations("Global")
|
const t = useTranslations("Global")
|
||||||
return (
|
return (
|
||||||
<section className="mt-[3.2px] flex flex-col gap-4">
|
<section className="flex flex-col gap-4 mt-[3.2px]">
|
||||||
<div className="flex min-h-40 flex-col items-center justify-center font-medium text-sm">
|
<div className="flex min-h-40 flex-col items-center justify-center font-medium text-sm">
|
||||||
{t("Loading")}
|
{t("Loading")}
|
||||||
<Loader visible={true} />
|
<Loader visible={true} />
|
||||||
|
@ -5,7 +5,7 @@ export const Loader = ({ visible }: { visible: boolean }) => {
|
|||||||
<div className="hamster-loading-wrapper" data-visible={visible}>
|
<div className="hamster-loading-wrapper" data-visible={visible}>
|
||||||
<div className="hamster-spinner">
|
<div className="hamster-spinner">
|
||||||
{bars.map((_, i) => (
|
{bars.map((_, i) => (
|
||||||
<div className="hamster-loading-bar" key={`hamster-bar-${i + 1}`} />
|
<div className="hamster-loading-bar" key={`hamster-bar-${i}`} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,16 +7,16 @@ export default function NetworkChartLoading() {
|
|||||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
||||||
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5">
|
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5">
|
||||||
<CardTitle className="flex items-center gap-0.5 text-xl">
|
<CardTitle className="flex items-center gap-0.5 text-xl">
|
||||||
<div className="aspect-auto h-[20px] w-24 bg-muted" />
|
<div className="aspect-auto h-[20px] w-24 bg-muted"></div>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted" />
|
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden pt-4 pr-4 sm:block">
|
<div className="hidden pr-4 pt-4 sm:block">
|
||||||
<Loader visible={true} />
|
<Loader visible={true} />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-2 sm:p-6">
|
<CardContent className="px-2 sm:p-6">
|
||||||
<div className="aspect-auto h-[250px] w-full" />
|
<div className="aspect-auto h-[250px] w-full"></div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
@ -5,13 +5,13 @@ import { useRouter } from "next/navigation"
|
|||||||
export function ServerDetailChartLoading() {
|
export function ServerDetailChartLoading() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<section className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
|
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
|
||||||
<Skeleton className="h-[182px] w-full animate-none rounded-[5px] bg-muted-foreground/10" />
|
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
<Skeleton className="h-[182px] w-full animate-none rounded-[5px] bg-muted-foreground/10" />
|
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
<Skeleton className="h-[182px] w-full animate-none rounded-[5px] bg-muted-foreground/10" />
|
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
<Skeleton className="h-[182px] w-full animate-none rounded-[5px] bg-muted-foreground/10" />
|
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
<Skeleton className="h-[182px] w-full animate-none rounded-[5px] bg-muted-foreground/10" />
|
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
<Skeleton className="h-[182px] w-full animate-none rounded-[5px] bg-muted-foreground/10" />
|
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -24,14 +24,14 @@ export function ServerDetailLoading() {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push("/")
|
router.push(`/`)
|
||||||
}}
|
}}
|
||||||
className="flex flex-none cursor-pointer items-center gap-0.5 break-all font-semibold text-xl leading-none tracking-tight"
|
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
|
||||||
>
|
>
|
||||||
<BackIcon />
|
<BackIcon />
|
||||||
<Skeleton className="h-[20px] w-24 animate-none rounded-[5px] bg-muted-foreground/10" />
|
<Skeleton className="h-[20px] w-24 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
</div>
|
</div>
|
||||||
<Skeleton className="mt-3 flex h-[81px] w-1/2 animate-none flex-wrap gap-2 rounded-[5px] bg-muted-foreground/10" />
|
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
1
components/motion/framer-lazy-feature.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { domMax as default } from "framer-motion"
|
13
components/motion/motion-provider.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { LazyMotion } from "framer-motion"
|
||||||
|
|
||||||
|
const loadFeatures = () => import("./framer-lazy-feature").then((res) => res.default)
|
||||||
|
|
||||||
|
export const MotionProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<LazyMotion features={loadFeatures} strict key="framer">
|
||||||
|
{children}
|
||||||
|
</LazyMotion>
|
||||||
|
)
|
||||||
|
}
|
@ -21,7 +21,7 @@ export default function AnimatedCircularProgressBar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("relative size-40 font-semibold text-2xl", className)}
|
className={cn("relative size-40 text-2xl font-semibold", className)}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--circle-size": "100px",
|
"--circle-size": "100px",
|
||||||
@ -38,7 +38,6 @@ export default function AnimatedCircularProgressBar({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<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">
|
||||||
<title>Circular Progress Bar</title>
|
|
||||||
{currentPercent <= 90 && currentPercent >= 0 && (
|
{currentPercent <= 90 && currentPercent >= 0 && (
|
||||||
<circle
|
<circle
|
||||||
cx="50"
|
cx="50"
|
||||||
@ -48,7 +47,7 @@ export default function AnimatedCircularProgressBar({
|
|||||||
strokeDashoffset="0"
|
strokeDashoffset="0"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
className="stroke-muted opacity-100"
|
className="opacity-100 stroke-muted"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--stroke-percent": 90 - currentPercent,
|
"--stroke-percent": 90 - currentPercent,
|
||||||
@ -71,7 +70,7 @@ export default function AnimatedCircularProgressBar({
|
|||||||
strokeDashoffset="0"
|
strokeDashoffset="0"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
className={cn("stroke-current opacity-100", {
|
className={cn("opacity-100 stroke-current", {
|
||||||
"stroke-[var(--stroke-primary-color)]": primaryColor,
|
"stroke-[var(--stroke-primary-color)]": primaryColor,
|
||||||
})}
|
})}
|
||||||
style={
|
style={
|
||||||
@ -92,7 +91,7 @@ export default function AnimatedCircularProgressBar({
|
|||||||
</svg>
|
</svg>
|
||||||
<span
|
<span
|
||||||
data-current-value={currentPercent}
|
data-current-value={currentPercent}
|
||||||
className="fade-in absolute inset-0 m-auto size-fit animate-in delay-[var(--delay)] duration-[var(--transition-length)] ease-linear"
|
className="duration-[var(--transition-length)] delay-[var(--delay)] absolute inset-0 m-auto size-fit ease-linear animate-in fade-in"
|
||||||
>
|
>
|
||||||
{currentPercent}
|
{currentPercent}
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
export const AnimatedTooltip = ({
|
export const AnimatedTooltip = ({
|
||||||
items,
|
items,
|
||||||
@ -14,7 +15,7 @@ export const AnimatedTooltip = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div className="group -mr-4 relative" key={item.name}>
|
<div className="group relative -mr-4" key={item.name}>
|
||||||
<Link href="https://buycoffee.top" target="_blank">
|
<Link href="https://buycoffee.top" target="_blank">
|
||||||
<Image
|
<Image
|
||||||
width={40}
|
width={40}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { type VariantProps, cva } from "class-variance-authority"
|
import { type VariantProps, cva } from "class-variance-authority"
|
||||||
import type * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center text-nowarp rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors pointer-events-none focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
"inline-flex items-center text-nowarp rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors pointer-events-none focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
@ -26,7 +26,7 @@ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HT
|
|||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<h3
|
<h3
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("font-semibold text-2xl leading-none tracking-tight", className)}
|
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -37,7 +37,7 @@ const CardDescription = React.forwardRef<
|
|||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<p ref={ref} className={cn("text-muted-foreground text-sm", className)} {...props} />
|
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||||
))
|
))
|
||||||
CardDescription.displayName = "CardDescription"
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
@ -164,19 +164,12 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid min-w-[8rem] items-start gap-1.5 overflow-hidden rounded-sm border border-border/50 bg-stone-100 text-xs dark:bg-stone-900",
|
"grid min-w-[8rem] items-start gap-1.5 rounded-sm border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!nestLabel && (
|
{!nestLabel ? tooltipLabel : null}
|
||||||
<div className="-mb-1 mx-auto px-2.5 pt-1">{!nestLabel ? tooltipLabel : null}</div>
|
<div className="grid gap-1.5">
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={cn("grid gap-1.5 bg-white px-2.5 py-1.5 dark:bg-black", {
|
|
||||||
"border-t-[1px]": !nestLabel,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{payload.map((item, index) => {
|
{payload.map((item, index) => {
|
||||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
@ -233,14 +226,13 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
{item.value && (
|
{item.value && (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-2 font-medium text-foreground tabular-nums",
|
"ml-2 font-mono font-medium tabular-nums text-foreground",
|
||||||
payload.length === 1 && "-ml-9",
|
payload.length === 1 && "-ml-9",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{typeof item.value === "number"
|
{typeof item.value === "number"
|
||||||
? item.value.toFixed(2).toLocaleString()
|
? item.value.toFixed(3).toLocaleString()
|
||||||
: item.value}{" "}
|
: item.value}
|
||||||
ms
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,144 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog"
|
|
||||||
import { Command as CommandPrimitive } from "cmdk"
|
|
||||||
import { Search } from "lucide-react"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Command = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Command.displayName = CommandPrimitive.displayName
|
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
|
||||||
return (
|
|
||||||
<Dialog {...props}>
|
|
||||||
<DialogTitle />
|
|
||||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
|
||||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4">
|
|
||||||
{children}
|
|
||||||
</Command>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CommandInput = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className="flex items-center bg-stone-100 px-3 dark:bg-stone-900" cmdk-input-wrapper="">
|
|
||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
<CommandPrimitive.Input
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
|
||||||
|
|
||||||
const CommandList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn("mb-1 max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandList.displayName = CommandPrimitive.List.displayName
|
|
||||||
|
|
||||||
const CommandEmpty = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
|
||||||
>((props, ref) => (
|
|
||||||
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
|
||||||
|
|
||||||
const CommandGroup = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Group
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:text-xs",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
|
||||||
|
|
||||||
const CommandSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<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}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
const CommandItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<CommandPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center gap-2 rounded-[8px] px-2 py-1.5 text-xs outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-stone-100 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 dark:data-[selected='true']:bg-stone-900 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn("ml-auto text-muted-foreground text-xs tracking-widest", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
CommandShortcut.displayName = "CommandShortcut"
|
|
||||||
|
|
||||||
export {
|
|
||||||
Command,
|
|
||||||
CommandDialog,
|
|
||||||
CommandInput,
|
|
||||||
CommandList,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandItem,
|
|
||||||
CommandShortcut,
|
|
||||||
CommandSeparator,
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
||||||
import { X } from "lucide-react"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root
|
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger
|
|
||||||
|
|
||||||
const DialogPortal = DialogPrimitive.Portal
|
|
||||||
|
|
||||||
const DialogClose = DialogPrimitive.Close
|
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Overlay
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<DialogPortal>
|
|
||||||
<DialogOverlay />
|
|
||||||
<DialogPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:rounded-lg",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<DialogPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
</DialogPrimitive.Content>
|
|
||||||
</DialogPortal>
|
|
||||||
))
|
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const DialogHeader = ({ className, ...props }: 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>) => (
|
|
||||||
<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>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
ref={ref}
|
|
||||||
className={cn("font-semibold text-lg leading-none tracking-tight", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<DialogPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
Dialog,
|
|
||||||
DialogPortal,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogClose,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
}
|
|
@ -45,7 +45,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in",
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -62,7 +62,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"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 data-[state=closed]:animate-out data-[state=open]:animate-in dark:shadow-none",
|
"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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -80,7 +80,7 @@ const DropdownMenuItem = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-[10px] px-2 py-1.5 font-normal text-xs outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
"relative flex cursor-default select-none items-center rounded-[10px] px-2 py-1.5 text-xs font-medium outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||||
inset && "pl-8",
|
inset && "pl-8",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@ -96,7 +96,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
@ -119,7 +119,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -142,7 +142,7 @@ const DropdownMenuLabel = React.forwardRef<
|
|||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("px-2 py-1.5 font-semibold text-sm", inset && "pl-8", className)}
|
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@ -62,7 +62,7 @@ const NavigationMenuContent = React.forwardRef<
|
|||||||
<NavigationMenuPrimitive.Content
|
<NavigationMenuPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out md:absolute md:w-auto",
|
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -76,10 +76,10 @@ const NavigationMenuViewport = React.forwardRef<
|
|||||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div className={cn("absolute top-full left-0 flex justify-center")}>
|
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||||
<NavigationMenuPrimitive.Viewport
|
<NavigationMenuPrimitive.Viewport
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full origin-top-center overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in md:w-[var(--radix-navigation-menu-viewport-width)]",
|
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -96,7 +96,7 @@ const NavigationMenuIndicator = React.forwardRef<
|
|||||||
<NavigationMenuPrimitive.Indicator
|
<NavigationMenuPrimitive.Indicator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=hidden]:animate-out data-[state=visible]:animate-in",
|
"top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -18,7 +18,7 @@ const PopoverContent = React.forwardRef<
|
|||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in",
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
|
||||||
import type * as React from "react"
|
|
||||||
|
|
||||||
function RadioGroup({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<RadioGroupPrimitive.Root
|
|
||||||
data-slot="radio-group"
|
|
||||||
className={cn("grid gap-3", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function RadioGroupItem({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
|
||||||
return (
|
|
||||||
<RadioGroupPrimitive.Item
|
|
||||||
data-slot="radio-group-item"
|
|
||||||
className={cn(
|
|
||||||
"aspect-square size-4 shrink-0 rounded-full border border-input shadow-xs outline-none transition-shadow focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:aria-invalid:ring-destructive/40",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center text-current">
|
|
||||||
<svg
|
|
||||||
role="img"
|
|
||||||
aria-label="Radio indicator"
|
|
||||||
width="6"
|
|
||||||
height="6"
|
|
||||||
viewBox="0 0 6 6"
|
|
||||||
fill="currentcolor"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<circle cx="3" cy="3" r="3" />
|
|
||||||
</svg>
|
|
||||||
</RadioGroupPrimitive.Indicator>
|
|
||||||
</RadioGroupPrimitive.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { RadioGroup, RadioGroupItem }
|
|
@ -20,7 +20,7 @@ const SheetOverlay = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -60,7 +60,7 @@ const SheetContent = React.forwardRef<
|
|||||||
<SheetOverlay />
|
<SheetOverlay />
|
||||||
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
@ -88,7 +88,7 @@ const SheetTitle = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Title
|
<SheetPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("font-semibold text-foreground text-lg", className)}
|
className={cn("text-lg font-semibold text-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@ -100,7 +100,7 @@ const SheetDescription = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SheetPrimitive.Description
|
<SheetPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -18,7 +18,7 @@ const TooltipContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fade-in-0 zoom-in-95 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 z-50 animate-in overflow-hidden rounded-[10px] border bg-popover px-3 py-1.5 text-popover-foreground text-sm shadow-md data-[state=closed]:animate-out",
|
"z-50 overflow-hidden rounded-[10px] border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md 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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -4,7 +4,7 @@ import getEnv from "./lib/env-entry"
|
|||||||
export const localeItems = [
|
export const localeItems = [
|
||||||
{ code: "en", name: "English" },
|
{ code: "en", name: "English" },
|
||||||
{ code: "ja", name: "日本語" },
|
{ code: "ja", name: "日本語" },
|
||||||
{ code: "zh-TW", name: "中文繁體" },
|
{ code: "zh-t", name: "中文繁體" },
|
||||||
{ code: "zh", name: "中文简体" },
|
{ code: "zh", name: "中文简体" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 56 MiB After Width: | Height: | Size: 56 MiB |
183
lib/dev-geo.ts
Normal file
@ -1,10 +1,8 @@
|
|||||||
import { getClientEnv, getServerEnv } from "./env"
|
import { env } from "next-runtime-env"
|
||||||
import type { EnvKey } from "./env"
|
|
||||||
|
|
||||||
export default function getEnv(key: EnvKey): string | undefined {
|
export default function getEnv(key: string) {
|
||||||
if (key.startsWith("NEXT_PUBLIC_")) {
|
if (key.startsWith("NEXT_PUBLIC_")) {
|
||||||
const clientKey = key.replace("NEXT_PUBLIC_", "") as any
|
return env(key)
|
||||||
return getClientEnv(clientKey)
|
|
||||||
}
|
}
|
||||||
return getServerEnv(key as any)
|
return process.env[key]
|
||||||
}
|
}
|
||||||
|
145
lib/env.ts
@ -1,145 +0,0 @@
|
|||||||
import { env } from "next-runtime-env"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Server-side environment variables
|
|
||||||
*/
|
|
||||||
export interface ServerEnvConfig {
|
|
||||||
/** Nezha API base URL */
|
|
||||||
NezhaBaseUrl: string
|
|
||||||
/** Nezha API authentication token */
|
|
||||||
NezhaAuth: string
|
|
||||||
/** Default locale for the application */
|
|
||||||
DefaultLocale: string
|
|
||||||
/** Force show all servers */
|
|
||||||
ForceShowAllServers: boolean
|
|
||||||
/** Site password */
|
|
||||||
SitePassword: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client-side environment variables (NEXT_PUBLIC_*)
|
|
||||||
*/
|
|
||||||
export interface ClientEnvConfig {
|
|
||||||
/** Nezha data fetch interval in milliseconds */
|
|
||||||
NezhaFetchInterval: number
|
|
||||||
/** Show country flags */
|
|
||||||
ShowFlag: boolean
|
|
||||||
/** Disable cartoon effects */
|
|
||||||
DisableCartoon: boolean
|
|
||||||
/** Show server tags */
|
|
||||||
ShowTag: boolean
|
|
||||||
/** Show network transfer information */
|
|
||||||
ShowNetTransfer: boolean
|
|
||||||
/** Force use SVG flags */
|
|
||||||
ForceUseSvgFlag: boolean
|
|
||||||
/** Fix server names at the top */
|
|
||||||
FixedTopServerName: boolean
|
|
||||||
/** Custom logo URL */
|
|
||||||
CustomLogo: string
|
|
||||||
/** Custom site title */
|
|
||||||
CustomTitle: string
|
|
||||||
/** Custom site description */
|
|
||||||
CustomDescription: string
|
|
||||||
/** Custom navigation links */
|
|
||||||
Links: string
|
|
||||||
/** Disable search engine indexing */
|
|
||||||
DisableIndex: boolean
|
|
||||||
/** Show tag count */
|
|
||||||
ShowTagCount: boolean
|
|
||||||
/** Show IP information */
|
|
||||||
ShowIpInfo: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 环境变量键的类型定义
|
|
||||||
*/
|
|
||||||
export type EnvKey = ServerEnvKey | ClientEnvKey
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 服务器端环境变量键
|
|
||||||
*/
|
|
||||||
export type ServerEnvKey = keyof ServerEnvConfig
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 客户端环境变量键
|
|
||||||
*/
|
|
||||||
export type ClientEnvKey = `NEXT_PUBLIC_${keyof ClientEnvConfig}`
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a server-side environment variable
|
|
||||||
* @param key - Environment variable key
|
|
||||||
* @returns Environment variable value
|
|
||||||
*/
|
|
||||||
export function getServerEnv<K extends keyof ServerEnvConfig>(key: K): string | undefined {
|
|
||||||
const value = process.env[key]
|
|
||||||
if (!value) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a client-side environment variable
|
|
||||||
* @param key - Environment variable key
|
|
||||||
* @returns Environment variable value
|
|
||||||
*/
|
|
||||||
export function getClientEnv<K extends keyof ClientEnvConfig>(key: K): string | undefined {
|
|
||||||
const envKey = `NEXT_PUBLIC_${key}`
|
|
||||||
const value = env(envKey)
|
|
||||||
if (!value) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse boolean environment variable
|
|
||||||
* @param value - Environment variable value
|
|
||||||
* @returns Parsed boolean value
|
|
||||||
*/
|
|
||||||
export function parseBoolean(value: string | undefined): boolean {
|
|
||||||
return value?.toLowerCase() === "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse number environment variable
|
|
||||||
* @param value - Environment variable value
|
|
||||||
* @param defaultValue - Default value if parsing fails
|
|
||||||
* @returns Parsed number value
|
|
||||||
*/
|
|
||||||
export function parseNumber(value: string | undefined, defaultValue: number): number {
|
|
||||||
if (!value) return defaultValue
|
|
||||||
const parsed = Number.parseInt(value, 10)
|
|
||||||
return Number.isNaN(parsed) ? defaultValue : parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all environment variables with their current values
|
|
||||||
*/
|
|
||||||
export function getAllEnvConfig(): { server: ServerEnvConfig; client: ClientEnvConfig } {
|
|
||||||
return {
|
|
||||||
server: {
|
|
||||||
NezhaBaseUrl: getServerEnv("NezhaBaseUrl") || "",
|
|
||||||
NezhaAuth: getServerEnv("NezhaAuth") || "",
|
|
||||||
DefaultLocale: getServerEnv("DefaultLocale") || "",
|
|
||||||
ForceShowAllServers: parseBoolean(getServerEnv("ForceShowAllServers")),
|
|
||||||
SitePassword: getServerEnv("SitePassword") || "",
|
|
||||||
},
|
|
||||||
client: {
|
|
||||||
NezhaFetchInterval: parseNumber(getClientEnv("NezhaFetchInterval"), 5000),
|
|
||||||
ShowFlag: parseBoolean(getClientEnv("ShowFlag")),
|
|
||||||
DisableCartoon: parseBoolean(getClientEnv("DisableCartoon")),
|
|
||||||
ShowTag: parseBoolean(getClientEnv("ShowTag")),
|
|
||||||
ShowNetTransfer: parseBoolean(getClientEnv("ShowNetTransfer")),
|
|
||||||
ForceUseSvgFlag: parseBoolean(getClientEnv("ForceUseSvgFlag")),
|
|
||||||
FixedTopServerName: parseBoolean(getClientEnv("FixedTopServerName")),
|
|
||||||
CustomLogo: getClientEnv("CustomLogo") || "",
|
|
||||||
CustomTitle: getClientEnv("CustomTitle") || "",
|
|
||||||
CustomDescription: getClientEnv("CustomDescription") || "",
|
|
||||||
Links: getClientEnv("Links") || "",
|
|
||||||
DisableIndex: parseBoolean(getClientEnv("DisableIndex")),
|
|
||||||
ShowTagCount: parseBoolean(getClientEnv("ShowTagCount")),
|
|
||||||
ShowIpInfo: parseBoolean(getClientEnv("ShowIpInfo")),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +1,4 @@
|
|||||||
|
import React from "react"
|
||||||
import type { SVGProps } from "react"
|
import type { SVGProps } from "react"
|
||||||
|
|
||||||
export function GetFontLogoClass(platform: string): string {
|
export function GetFontLogoClass(platform: string): string {
|
||||||
@ -49,16 +50,16 @@ export function GetFontLogoClass(platform: string): string {
|
|||||||
) {
|
) {
|
||||||
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")) {
|
||||||
@ -112,16 +113,16 @@ export function GetOsName(platform: string): string {
|
|||||||
) {
|
) {
|
||||||
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")) {
|
||||||
@ -133,11 +134,10 @@ export function GetOsName(platform: string): string {
|
|||||||
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}>
|
||||||
<title>Mage Microsoft Windows</title>
|
|
||||||
<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>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
2
lib/map-string.ts
Normal file
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { type ReactNode, createContext, useContext, useState } from "react"
|
import React, { ReactNode, createContext, useContext, useState } from "react"
|
||||||
|
|
||||||
interface FilterContextType {
|
interface FilterContextType {
|
||||||
filter: boolean
|
filter: boolean
|
@ -1,12 +1,12 @@
|
|||||||
"use server"
|
"use server"
|
||||||
|
|
||||||
import type { NezhaAPI, ServerApi } from "@/app/types/nezha-api"
|
import { NezhaAPI, ServerApi } from "@/app/types/nezha-api"
|
||||||
import type { MakeOptional } from "@/app/types/utils"
|
import { MakeOptional } from "@/app/types/utils"
|
||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import { connection } from "next/server"
|
import { unstable_noStore as noStore } from "next/cache"
|
||||||
|
|
||||||
export async function GetNezhaData() {
|
export async function GetNezhaData() {
|
||||||
await connection()
|
noStore()
|
||||||
|
|
||||||
let nezhaBaseUrl = getEnv("NezhaBaseUrl")
|
let nezhaBaseUrl = getEnv("NezhaBaseUrl")
|
||||||
if (!nezhaBaseUrl) {
|
if (!nezhaBaseUrl) {
|
||||||
@ -71,9 +71,9 @@ export async function GetNezhaData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove unwanted properties
|
// Remove unwanted properties
|
||||||
element.ipv4 = undefined
|
delete element.ipv4
|
||||||
element.ipv6 = undefined
|
delete element.ipv6
|
||||||
element.valid_ip = undefined
|
delete element.valid_ip
|
||||||
|
|
||||||
return element
|
return element
|
||||||
},
|
},
|
||||||
@ -87,8 +87,6 @@ export async function GetNezhaData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function GetServerMonitor({ server_id }: { server_id: number }) {
|
export async function GetServerMonitor({ server_id }: { server_id: number }) {
|
||||||
await connection()
|
|
||||||
|
|
||||||
let nezhaBaseUrl = getEnv("NezhaBaseUrl")
|
let nezhaBaseUrl = getEnv("NezhaBaseUrl")
|
||||||
if (!nezhaBaseUrl) {
|
if (!nezhaBaseUrl) {
|
||||||
console.error("NezhaBaseUrl is not set")
|
console.error("NezhaBaseUrl is not set")
|
||||||
@ -128,13 +126,7 @@ export async function GetServerMonitor({ server_id }: { server_id: number }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GetServerIP({
|
export async function GetServerIP({ server_id }: { server_id: number }): Promise<string> {
|
||||||
server_id,
|
|
||||||
}: {
|
|
||||||
server_id: number
|
|
||||||
}): Promise<string> {
|
|
||||||
await connection()
|
|
||||||
|
|
||||||
let nezhaBaseUrl = getEnv("NezhaBaseUrl")
|
let nezhaBaseUrl = getEnv("NezhaBaseUrl")
|
||||||
if (!nezhaBaseUrl) {
|
if (!nezhaBaseUrl) {
|
||||||
console.error("NezhaBaseUrl is not set")
|
console.error("NezhaBaseUrl is not set")
|
||||||
@ -182,7 +174,6 @@ export async function GetServerIP({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function GetServerDetail({ server_id }: { server_id: number }) {
|
export async function GetServerDetail({ server_id }: { server_id: number }) {
|
||||||
await connection()
|
|
||||||
let nezhaBaseUrl = getEnv("NezhaBaseUrl")
|
let nezhaBaseUrl = getEnv("NezhaBaseUrl")
|
||||||
if (!nezhaBaseUrl) {
|
if (!nezhaBaseUrl) {
|
||||||
console.error("NezhaBaseUrl is not set")
|
console.error("NezhaBaseUrl is not set")
|
||||||
@ -218,9 +209,9 @@ export async function GetServerDetail({ server_id }: { server_id: number }) {
|
|||||||
const timestamp = Date.now() / 1000
|
const timestamp = Date.now() / 1000
|
||||||
const detailData = detailDataList.map((element) => {
|
const detailData = detailDataList.map((element) => {
|
||||||
element.online_status = timestamp - element.last_active <= 180
|
element.online_status = timestamp - element.last_active <= 180
|
||||||
element.ipv4 = undefined
|
delete element.ipv4
|
||||||
element.ipv6 = undefined
|
delete element.ipv6
|
||||||
element.valid_ip = undefined
|
delete element.valid_ip
|
||||||
return element
|
return element
|
||||||
})[0]
|
})[0]
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { type ReactNode, createContext, useContext, useState } from "react"
|
import React, { ReactNode, createContext, useContext, useState } from "react"
|
||||||
|
|
||||||
type Status = "all" | "online" | "offline"
|
type Status = "all" | "online" | "offline"
|
||||||
|
|
47
lib/utils.ts
@ -1,4 +1,4 @@
|
|||||||
import type { NezhaAPISafe } from "@/app/types/nezha-api"
|
import { NezhaAPISafe } from "@/app/types/nezha-api"
|
||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
@ -13,40 +13,18 @@ export function formatNezhaInfo(serverInfo: NezhaAPISafe) {
|
|||||||
process: serverInfo.status.ProcessCount || 0,
|
process: serverInfo.status.ProcessCount || 0,
|
||||||
up: serverInfo.status.NetOutSpeed / 1024 / 1024 || 0,
|
up: serverInfo.status.NetOutSpeed / 1024 / 1024 || 0,
|
||||||
down: serverInfo.status.NetInSpeed / 1024 / 1024 || 0,
|
down: serverInfo.status.NetInSpeed / 1024 / 1024 || 0,
|
||||||
last_active_time_string: serverInfo.last_active
|
|
||||||
? new Date(serverInfo.last_active * 1000).toLocaleString()
|
|
||||||
: "",
|
|
||||||
boot_time: serverInfo.host.BootTime,
|
|
||||||
boot_time_string: serverInfo.host.BootTime
|
|
||||||
? new Date(serverInfo.host.BootTime * 1000).toLocaleString()
|
|
||||||
: "",
|
|
||||||
online: serverInfo.online_status,
|
online: serverInfo.online_status,
|
||||||
uptime: serverInfo.status.Uptime || 0,
|
|
||||||
version: serverInfo.host.Version || null,
|
|
||||||
tcp: serverInfo.status.TcpConnCount || 0,
|
tcp: serverInfo.status.TcpConnCount || 0,
|
||||||
udp: serverInfo.status.UdpConnCount || 0,
|
udp: serverInfo.status.UdpConnCount || 0,
|
||||||
arch: serverInfo.host.Arch || "",
|
|
||||||
mem_total: serverInfo.host.MemTotal || 0,
|
|
||||||
swap_total: serverInfo.host.SwapTotal || 0,
|
|
||||||
disk_total: serverInfo.host.DiskTotal || 0,
|
|
||||||
platform: serverInfo.host.Platform || "",
|
|
||||||
platform_version: serverInfo.host.PlatformVersion || "",
|
|
||||||
mem: (serverInfo.status.MemUsed / serverInfo.host.MemTotal) * 100 || 0,
|
mem: (serverInfo.status.MemUsed / serverInfo.host.MemTotal) * 100 || 0,
|
||||||
swap: (serverInfo.status.SwapUsed / serverInfo.host.SwapTotal) * 100 || 0,
|
swap: (serverInfo.status.SwapUsed / serverInfo.host.SwapTotal) * 100 || 0,
|
||||||
disk: (serverInfo.status.DiskUsed / serverInfo.host.DiskTotal) * 100 || 0,
|
disk: (serverInfo.status.DiskUsed / serverInfo.host.DiskTotal) * 100 || 0,
|
||||||
stg: (serverInfo.status.DiskUsed / serverInfo.host.DiskTotal) * 100 || 0,
|
stg: (serverInfo.status.DiskUsed / serverInfo.host.DiskTotal) * 100 || 0,
|
||||||
net_out_transfer: serverInfo.status.NetOutTransfer || 0,
|
|
||||||
net_in_transfer: serverInfo.status.NetInTransfer || 0,
|
|
||||||
country_code: serverInfo.host.CountryCode,
|
country_code: serverInfo.host.CountryCode,
|
||||||
cpu_info: serverInfo.host.CPU || [],
|
|
||||||
gpu_info: serverInfo.host.GPU || [],
|
|
||||||
load_1: serverInfo.status.Load1?.toFixed(2) || 0.0,
|
|
||||||
load_5: serverInfo.status.Load5?.toFixed(2) || 0.0,
|
|
||||||
load_15: serverInfo.status.Load15?.toFixed(2) || 0.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatBytes(bytes: number, decimals = 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
|
||||||
@ -55,7 +33,7 @@ export function formatBytes(bytes: number, decimals = 2) {
|
|||||||
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
|
||||||
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDaysBetweenDates(date1: string, date2: string): number {
|
export function getDaysBetweenDates(date1: string, date2: string): number {
|
||||||
@ -106,14 +84,11 @@ export function formatRelativeTime(timestamp: number): string {
|
|||||||
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) {
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}h`
|
return `${hours}h`
|
||||||
}
|
} else if (minutes > 0) {
|
||||||
if (minutes > 0) {
|
|
||||||
return `${minutes}m`
|
return `${minutes}m`
|
||||||
}
|
} else if (seconds >= 0) {
|
||||||
if (seconds >= 0) {
|
|
||||||
return `${seconds}s`
|
return `${seconds}s`
|
||||||
}
|
}
|
||||||
return "0s"
|
return "0s"
|
||||||
@ -129,13 +104,3 @@ export function formatTime(timestamp: number): string {
|
|||||||
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}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatTime12(timestamp: number): string {
|
|
||||||
// example: 3:45 PM
|
|
||||||
const date = new Date(timestamp)
|
|
||||||
const hours = date.getHours()
|
|
||||||
const minutes = date.getMinutes()
|
|
||||||
const ampm = hours >= 12 ? "PM" : "AM"
|
|
||||||
const hours12 = hours % 12 || 12
|
|
||||||
return `${hours12}:${minutes.toString().padStart(2, "0")} ${ampm}`
|
|
||||||
}
|
|
||||||
|
@ -85,9 +85,7 @@
|
|||||||
"CPU": "CPU",
|
"CPU": "CPU",
|
||||||
"Upload": "Upload",
|
"Upload": "Upload",
|
||||||
"Download": "Download",
|
"Download": "Download",
|
||||||
"Load": "Load",
|
"Load": "Load"
|
||||||
"LastActive": "Last Active",
|
|
||||||
"BootTime": "Boot Time"
|
|
||||||
},
|
},
|
||||||
"ServerDetailChartClient": {
|
"ServerDetailChartClient": {
|
||||||
"chart_fetch_error_message": "Please check your environment variables and review the server console",
|
"chart_fetch_error_message": "Please check your environment variables and review the server console",
|
||||||
@ -113,7 +111,7 @@
|
|||||||
"p_1079-1199_Simpleandbeautifuldashbo": "Simple and beautiful dashboard"
|
"p_1079-1199_Simpleandbeautifuldashbo": "Simple and beautiful dashboard"
|
||||||
},
|
},
|
||||||
"Overview": {
|
"Overview": {
|
||||||
"p_2277-2331_Overview": "Overview",
|
"p_2277-2331_Overview": "👋 Overview",
|
||||||
"p_2390-2457_wherethetimeis": "where the time is"
|
"p_2390-2457_wherethetimeis": "where the time is"
|
||||||
},
|
},
|
||||||
"Global": {
|
"Global": {
|
||||||
@ -126,15 +124,5 @@
|
|||||||
"h1_490-590_404NotFound": "Server Not Found",
|
"h1_490-590_404NotFound": "Server Not Found",
|
||||||
"h1_490-590_404NotFoundBack": " Press here to go back",
|
"h1_490-590_404NotFoundBack": " Press here to go back",
|
||||||
"h1_490-590_Error": "Something went wrong"
|
"h1_490-590_Error": "Something went wrong"
|
||||||
},
|
|
||||||
"DashCommand": {
|
|
||||||
"TypeCommand": "Type a command or search...",
|
|
||||||
"NoResults": "No results found.",
|
|
||||||
"Servers": "Servers",
|
|
||||||
"Shortcuts": "Shortcuts",
|
|
||||||
"ToggleLightMode": "Toggle Light Mode",
|
|
||||||
"ToggleDarkMode": "Toggle Dark Mode",
|
|
||||||
"ToggleSystemMode": "Toggle System Mode",
|
|
||||||
"Home": "Home"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,9 +85,7 @@
|
|||||||
"CPU": "CPU",
|
"CPU": "CPU",
|
||||||
"Load": "負荷",
|
"Load": "負荷",
|
||||||
"Upload": "Upload",
|
"Upload": "Upload",
|
||||||
"Download": "Download",
|
"Download": "Download"
|
||||||
"LastActive": "Last Active",
|
|
||||||
"BootTime": "Boot Time"
|
|
||||||
},
|
},
|
||||||
"ServerDetailChartClient": {
|
"ServerDetailChartClient": {
|
||||||
"chart_fetch_error_message": "環境変数を確認し、サーバーコンソールを確認してください",
|
"chart_fetch_error_message": "環境変数を確認し、サーバーコンソールを確認してください",
|
||||||
@ -113,7 +111,7 @@
|
|||||||
"p_1079-1199_Simpleandbeautifuldashbo": "シンプルで美しいダッシュボード"
|
"p_1079-1199_Simpleandbeautifuldashbo": "シンプルで美しいダッシュボード"
|
||||||
},
|
},
|
||||||
"Overview": {
|
"Overview": {
|
||||||
"p_2277-2331_Overview": "概要",
|
"p_2277-2331_Overview": "👋 概要",
|
||||||
"p_2390-2457_wherethetimeis": "現在の時間"
|
"p_2390-2457_wherethetimeis": "現在の時間"
|
||||||
},
|
},
|
||||||
"Global": {
|
"Global": {
|
||||||
@ -126,15 +124,5 @@
|
|||||||
"h1_490-590_404NotFound": "サーバーは見つかりません",
|
"h1_490-590_404NotFound": "サーバーは見つかりません",
|
||||||
"h1_490-590_404NotFoundBack": "戻る",
|
"h1_490-590_404NotFoundBack": "戻る",
|
||||||
"h1_490-590_Error": "何らかの問題が発生しました"
|
"h1_490-590_Error": "何らかの問題が発生しました"
|
||||||
},
|
|
||||||
"DashCommand": {
|
|
||||||
"TypeCommand": "コマンドを入力してください",
|
|
||||||
"NoResults": "結果は見つかりませんでした",
|
|
||||||
"Servers": "サーバー",
|
|
||||||
"Shortcuts": "ショートカット",
|
|
||||||
"ToggleLightMode": "ライトモードに切り替え",
|
|
||||||
"ToggleDarkMode": "ダークモードに切り替え",
|
|
||||||
"ToggleSystemMode": "システムモードに切り替え",
|
|
||||||
"Home": "ホーム"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,9 +85,7 @@
|
|||||||
"CPU": "CPU",
|
"CPU": "CPU",
|
||||||
"Upload": "上傳",
|
"Upload": "上傳",
|
||||||
"Download": "下載",
|
"Download": "下載",
|
||||||
"Load": "負載",
|
"Load": "負載"
|
||||||
"LastActive": "最後上報時間",
|
|
||||||
"BootTime": "啟動時間"
|
|
||||||
},
|
},
|
||||||
"ServerDetailChartClient": {
|
"ServerDetailChartClient": {
|
||||||
"chart_fetch_error_message": "獲取伺服器詳情失敗,請檢查您的環境變數並檢查伺服器控制台",
|
"chart_fetch_error_message": "獲取伺服器詳情失敗,請檢查您的環境變數並檢查伺服器控制台",
|
||||||
@ -113,7 +111,7 @@
|
|||||||
"p_1079-1199_Simpleandbeautifuldashbo": "簡單美觀的儀錶板"
|
"p_1079-1199_Simpleandbeautifuldashbo": "簡單美觀的儀錶板"
|
||||||
},
|
},
|
||||||
"Overview": {
|
"Overview": {
|
||||||
"p_2277-2331_Overview": "概覽",
|
"p_2277-2331_Overview": "👋 概覽",
|
||||||
"p_2390-2457_wherethetimeis": "當前時間"
|
"p_2390-2457_wherethetimeis": "當前時間"
|
||||||
},
|
},
|
||||||
"Global": {
|
"Global": {
|
||||||
@ -126,15 +124,5 @@
|
|||||||
"h1_490-590_404NotFound": "伺服器未找到",
|
"h1_490-590_404NotFound": "伺服器未找到",
|
||||||
"h1_490-590_404NotFoundBack": "返回",
|
"h1_490-590_404NotFoundBack": "返回",
|
||||||
"h1_490-590_Error": "發生錯誤"
|
"h1_490-590_Error": "發生錯誤"
|
||||||
},
|
|
||||||
"DashCommand": {
|
|
||||||
"TypeCommand": "輸入命令或搜尋",
|
|
||||||
"NoResults": "沒有結果",
|
|
||||||
"Servers": "伺服器",
|
|
||||||
"Shortcuts": "快捷鍵",
|
|
||||||
"ToggleLightMode": "切換亮色模式",
|
|
||||||
"ToggleDarkMode": "切換暗色模式",
|
|
||||||
"ToggleSystemMode": "切換系統模式",
|
|
||||||
"Home": "首頁"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -85,9 +85,7 @@
|
|||||||
"CPU": "CPU",
|
"CPU": "CPU",
|
||||||
"Upload": "上传",
|
"Upload": "上传",
|
||||||
"Download": "下载",
|
"Download": "下载",
|
||||||
"Load": "负载",
|
"Load": "负载"
|
||||||
"LastActive": "最后上报时间",
|
|
||||||
"BootTime": "启动时间"
|
|
||||||
},
|
},
|
||||||
"ServerDetailChartClient": {
|
"ServerDetailChartClient": {
|
||||||
"chart_fetch_error_message": "获取服务器详情失败,请检查您的环境变量并检查服务器控制台",
|
"chart_fetch_error_message": "获取服务器详情失败,请检查您的环境变量并检查服务器控制台",
|
||||||
@ -113,7 +111,7 @@
|
|||||||
"p_1079-1199_Simpleandbeautifuldashbo": "简单美观的仪表板"
|
"p_1079-1199_Simpleandbeautifuldashbo": "简单美观的仪表板"
|
||||||
},
|
},
|
||||||
"Overview": {
|
"Overview": {
|
||||||
"p_2277-2331_Overview": "概览",
|
"p_2277-2331_Overview": "👋 概览",
|
||||||
"p_2390-2457_wherethetimeis": "当前时间"
|
"p_2390-2457_wherethetimeis": "当前时间"
|
||||||
},
|
},
|
||||||
"Global": {
|
"Global": {
|
||||||
@ -126,15 +124,5 @@
|
|||||||
"h1_490-590_404NotFound": "服务器不存在",
|
"h1_490-590_404NotFound": "服务器不存在",
|
||||||
"h1_490-590_404NotFoundBack": "返回",
|
"h1_490-590_404NotFoundBack": "返回",
|
||||||
"h1_490-590_Error": "发生错误"
|
"h1_490-590_Error": "发生错误"
|
||||||
},
|
|
||||||
"DashCommand": {
|
|
||||||
"TypeCommand": "输入命令或搜索",
|
|
||||||
"NoResults": "结果为空",
|
|
||||||
"Servers": "服务器",
|
|
||||||
"Shortcuts": "快捷键",
|
|
||||||
"ToggleLightMode": "切换亮色模式",
|
|
||||||
"ToggleDarkMode": "切换暗色模式",
|
|
||||||
"ToggleSystemMode": "切换系统模式",
|
|
||||||
"Home": "首页"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import withPWAInit from "@ducanh2912/next-pwa"
|
import withPWAInit from "@ducanh2912/next-pwa"
|
||||||
import withBundleAnalyzer from "@next/bundle-analyzer"
|
import withBundleAnalyzer from "@next/bundle-analyzer"
|
||||||
import createNextIntlPlugin from "next-intl/plugin"
|
import createNextIntlPlugin from "next-intl/plugin"
|
||||||
|
import { env } from "next-runtime-env"
|
||||||
|
|
||||||
const bundleAnalyzer = withBundleAnalyzer({
|
const bundleAnalyzer = withBundleAnalyzer({
|
||||||
enabled: process.env.ANALYZE === "true",
|
enabled: process.env.ANALYZE === "true",
|
||||||
@ -22,16 +23,13 @@ const withPWA = withPWAInit({
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
experimental: {
|
experimental: {
|
||||||
webpackBuildWorker: true,
|
|
||||||
parallelServerBuildTraces: true,
|
|
||||||
parallelServerCompiles: true,
|
|
||||||
inlineCss: true,
|
inlineCss: true,
|
||||||
reactCompiler: true,
|
|
||||||
serverActions: {
|
serverActions: {
|
||||||
allowedOrigins: ["*"],
|
allowedOrigins: ["*"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
poweredByHeader: false,
|
||||||
eslint: {
|
eslint: {
|
||||||
// Warning: This allows production builds to successfully complete even if
|
// Warning: This allows production builds to successfully complete even if
|
||||||
// your project has ESLint errors.
|
// your project has ESLint errors.
|
||||||
|
104
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nezha-dash",
|
"name": "nezha-dash",
|
||||||
"version": "2.9.3",
|
"version": "2.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3040",
|
"dev": "next dev -p 3040",
|
||||||
@ -10,69 +10,77 @@
|
|||||||
"format": "biome format --write .",
|
"format": "biome format --write .",
|
||||||
"check": "biome check",
|
"check": "biome check",
|
||||||
"check:fix": "biome check --fix",
|
"check:fix": "biome check --fix",
|
||||||
"build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/"
|
"build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
|
||||||
|
"build-dev": "next build",
|
||||||
|
"start-dev": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ducanh2912/next-pwa": "^10.2.9",
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.11",
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||||
"@radix-ui/react-label": "^2.1.4",
|
"@radix-ui/react-label": "^2.1.1",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.10",
|
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||||
"@radix-ui/react-popover": "^1.1.11",
|
"@radix-ui/react-popover": "^1.1.4",
|
||||||
"@radix-ui/react-progress": "^1.1.4",
|
"@radix-ui/react-progress": "^1.1.1",
|
||||||
"@radix-ui/react-radio-group": "^1.3.4",
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
"@radix-ui/react-separator": "^1.1.4",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@radix-ui/react-switch": "^1.2.2",
|
"@radix-ui/react-tooltip": "^1.1.6",
|
||||||
"@radix-ui/react-tooltip": "^1.2.4",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"@turf/turf": "^7.1.0",
|
||||||
"@types/crypto-js": "^4.2.2",
|
|
||||||
"@types/d3-geo": "^3.1.0",
|
"@types/d3-geo": "^3.1.0",
|
||||||
"@types/luxon": "^3.6.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"babel-plugin-react-compiler": "^19.0.0-beta-ebf51a3-20250411",
|
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
||||||
"caniuse-lite": "^1.0.30001715",
|
"caniuse-lite": "^1.0.30001690",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"country-flag-icons": "^1.5.13",
|
||||||
"country-flag-icons": "^1.5.18",
|
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"d3-geo": "^3.1.1",
|
"d3-geo": "^3.1.1",
|
||||||
"d3-selection": "^3.0.0",
|
"d3-selection": "^3.0.0",
|
||||||
"flag-icons": "^7.3.2",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"i18n-iso-countries": "^7.14.0",
|
"flag-icons": "^7.2.3",
|
||||||
"lucide-react": "^0.474.0",
|
"framer-motion": "^12.0.0-alpha.2",
|
||||||
"luxon": "^3.6.1",
|
"lucide-react": "^0.454.0",
|
||||||
"maxmind": "^4.3.24",
|
"luxon": "^3.5.0",
|
||||||
"next": "^15.3.1",
|
"maxmind": "^4.3.23",
|
||||||
"next-auth": "^5.0.0-beta.26",
|
"next": "^15.1.2",
|
||||||
"next-intl": "^4.0.3",
|
"next-auth": "^5.0.0-beta.25",
|
||||||
"next-runtime-env": "^3.3.0",
|
"next-intl": "^3.26.3",
|
||||||
"next-themes": "^0.4.6",
|
"next-runtime-env": "^3.2.2",
|
||||||
"react": "^19.1.0",
|
"next-themes": "^0.4.4",
|
||||||
|
"react": "^19.0.0",
|
||||||
"react-device-detect": "^2.2.3",
|
"react-device-detect": "^2.2.3",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-intersection-observer": "^9.16.0",
|
"react-intersection-observer": "^9.14.0",
|
||||||
"react-wrap-balancer": "^1.1.1",
|
"react-wrap-balancer": "^1.1.1",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.0",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.0",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"typescript-eslint": "^8.18.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@next/bundle-analyzer": "^15.3.1",
|
"@next/bundle-analyzer": "^15.1.2",
|
||||||
"@tailwindcss/postcss": "^4.1.4",
|
"@tailwindcss/postcss": "^4.0.0-beta.8",
|
||||||
"@types/node": "^22.14.1",
|
"@types/node": "^22.10.2",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.0.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"postcss": "^8.5.3",
|
"eslint": "^9.17.0",
|
||||||
"tailwindcss": "^4.1.4",
|
"eslint-config-next": "^15.1.2",
|
||||||
"typescript": "^5.8.3",
|
"eslint-plugin-turbo": "^2.3.3",
|
||||||
"vercel": "^41.6.2"
|
"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"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"react-is": "^19.0.0-rc-69d4b800-20241021"
|
"react-is": "^19.0.0-rc-69d4b800-20241021"
|
||||||
}
|
},
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 364 B |
Before Width: | Height: | Size: 370 B |
Before Width: | Height: | Size: 414 B |
@ -3,8 +3,8 @@
|
|||||||
@variant dark (&:is(.dark *));
|
@variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: var(--font-sans), ui-sans-serif, system-ui, sans-serif,
|
--font-sans: var(--font-sans), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
|
||||||
--color-border: hsl(var(--border));
|
--color-border: hsl(var(--border));
|
||||||
--color-input: hsl(var(--input));
|
--color-input: hsl(var(--input));
|
||||||
@ -146,7 +146,6 @@
|
|||||||
--chart-8: 252 50% 50%;
|
--chart-8: 252 50% 50%;
|
||||||
--chart-9: 288 50% 50%;
|
--chart-9: 288 50% 50%;
|
||||||
--chart-10: 324 50% 50%;
|
--chart-10: 324 50% 50%;
|
||||||
--timing: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@ -318,70 +317,3 @@
|
|||||||
.scrollbar-hidden::-webkit-scrollbar {
|
.scrollbar-hidden::-webkit-scrollbar {
|
||||||
display: none; /* Chrome, Safari 和 Opera */
|
display: none; /* Chrome, Safari 和 Opera */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-animate {
|
|
||||||
opacity: 0;
|
|
||||||
filter: blur(10px);
|
|
||||||
animation: tooltip-fade-in 0.2s ease-in-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes tooltip-fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
filter: blur(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
filter: blur(0px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Thanks to next.js. */
|
|
||||||
[data-issues-count-animation] {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-issues-count-animation] > div {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-issues-count-exit].animate {
|
|
||||||
animation: fadeOut 300ms var(--timing) forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-issues-count-enter].animate {
|
|
||||||
animation: fadeIn 300ms var(--timing) forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-issues-count-plural] {
|
|
||||||
display: inline-block;
|
|
||||||
animation: fadeIn 300ms var(--timing) forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
filter: blur(2px);
|
|
||||||
transform: translateY(8px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
filter: blur(0px);
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeOut {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
filter: blur(0px);
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-12px);
|
|
||||||
filter: blur(2px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|