feat: pr lint

This commit is contained in:
hamster1963 2024-12-13 15:09:12 +08:00
parent 17946d5f0b
commit 55cc033cf5
85 changed files with 1718 additions and 2202 deletions

View File

@ -0,0 +1,56 @@
name: Auto Fix Lint and Format
on:
pull_request_target:
types: [opened, synchronize]
jobs:
auto-fix:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Set up Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: "latest"
- name: Install dependencies
run: bun install
- name: Run linter and fix issues
run: bun run lint:fix
- name: Run formatter
run: bun run format
- name: Check for changes
id: check_changes
run: |
git diff --exit-code || echo "has_changes=true" >> $GITHUB_ENV
- name: Commit and push changes
if: steps.check_changes.outputs.has_changes == 'true' || env.has_changes == 'true'
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: auto-fix linting and formatting issues"
commit_options: "--no-verify"
file_pattern: "."
- name: Add PR comment
if: steps.check_changes.outputs.has_changes == 'true' || env.has_changes == 'true'
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Linting and formatting issues were automatically fixed. Please review the changes.'
});

12
.prettierrc.mjs Normal file
View File

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

View File

@ -1,52 +1,49 @@
"use client"; "use client"
import { ServerApi } from "@/app/types/nezha-api"; import { ServerApi } from "@/app/types/nezha-api"
import { nezhaFetcher } from "@/lib/utils"; import { nezhaFetcher } from "@/lib/utils"
import useSWRImmutable from "swr/immutable"; import useSWRImmutable from "swr/immutable"
import { geoJsonString } from "../../../lib/geo-json-string"; import { geoJsonString } from "../../../lib/geo-json-string"
import GlobalInfo from "./GlobalInfo"; import GlobalInfo from "./GlobalInfo"
import GlobalLoading from "./GlobalLoading"; import GlobalLoading from "./GlobalLoading"
import { InteractiveMap } from "./InteractiveMap"; import { InteractiveMap } from "./InteractiveMap"
import { TooltipProvider } from "./TooltipContext"; import { TooltipProvider } from "./TooltipContext"
export default function ServerGlobal() { export default function ServerGlobal() {
const { data: nezhaServerList, error } = useSWRImmutable<ServerApi>( const { data: nezhaServerList, error } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
"/api/server",
nezhaFetcher,
);
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="text-sm font-medium opacity-40">{error.message}</p> <p className="text-sm font-medium opacity-40">{error.message}</p>
</div> </div>
); )
if (!nezhaServerList) { if (!nezhaServerList) {
return <GlobalLoading />; return <GlobalLoading />
} }
const countryList: string[] = []; const countryList: string[] = []
const serverCounts: { [key: string]: number } = {}; const serverCounts: { [key: string]: number } = {}
nezhaServerList.result.forEach((server) => { 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)) {
countryList.push(countryCode); countryList.push(countryCode)
} }
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1; serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1
} }
}); })
const width = 900; const width = 900
const height = 500; const height = 500
const geoJson = JSON.parse(geoJsonString); const geoJson = JSON.parse(geoJsonString)
const filteredFeatures = geoJson.features.filter( const filteredFeatures = geoJson.features.filter(
(feature: any) => feature.properties.iso_a3_eh !== "", (feature: any) => feature.properties.iso_a3_eh !== "",
); )
return ( return (
<section className="flex flex-col gap-4 mt-[3.2px]"> <section className="flex flex-col gap-4 mt-[3.2px]">
@ -64,5 +61,5 @@ export default function ServerGlobal() {
</TooltipProvider> </TooltipProvider>
</div> </div>
</section> </section>
); )
} }

View File

@ -1,18 +1,18 @@
"use client"; "use client"
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl"
type GlobalInfoProps = { type GlobalInfoProps = {
countries: string[]; countries: string[]
}; }
export default function GlobalInfo({ countries }: GlobalInfoProps) { 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="text-sm font-medium 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>
); )
} }

View File

@ -1,10 +1,10 @@
"use client"; "use client"
import { Loader } from "@/components/loading/Loader"; import { Loader } from "@/components/loading/Loader"
import { useTranslations } from "next-intl"; 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="flex flex-col gap-4 mt-[3.2px]"> <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">
@ -12,5 +12,5 @@ export default function GlobalLoading() {
<Loader visible={true} /> <Loader visible={true} />
</div> </div>
</section> </section>
); )
} }

View File

@ -1,18 +1,18 @@
"use client"; "use client"
import { countryCoordinates } from "@/lib/geo-limit"; import { countryCoordinates } from "@/lib/geo-limit"
import { geoEquirectangular, geoPath } from "d3-geo"; import { geoEquirectangular, geoPath } from "d3-geo"
import MapTooltip from "./MapTooltip"; import MapTooltip from "./MapTooltip"
import { useTooltip } from "./TooltipContext"; import { useTooltip } from "./TooltipContext"
interface InteractiveMapProps { interface InteractiveMapProps {
countries: string[]; countries: string[]
serverCounts: { [key: string]: number }; serverCounts: { [key: string]: number }
width: number; width: number
height: number; height: number
filteredFeatures: any[]; filteredFeatures: any[]
nezhaServerList: any; nezhaServerList: any
} }
export function InteractiveMap({ export function InteractiveMap({
@ -23,20 +23,17 @@ export function InteractiveMap({
filteredFeatures, filteredFeatures,
nezhaServerList, nezhaServerList,
}: InteractiveMapProps) { }: InteractiveMapProps) {
const { setTooltipData } = useTooltip(); const { setTooltipData } = useTooltip()
const projection = geoEquirectangular() const projection = geoEquirectangular()
.scale(140) .scale(140)
.translate([width / 2, height / 2]) .translate([width / 2, height / 2])
.rotate([-12, 0, 0]); .rotate([-12, 0, 0])
const path = geoPath().projection(projection); const path = geoPath().projection(projection)
return ( return (
<div <div className="relative w-full aspect-[2/1]" onMouseLeave={() => setTooltipData(null)}>
className="relative w-full aspect-[2/1]"
onMouseLeave={() => setTooltipData(null)}
>
<svg <svg
width={width} width={width}
height={height} height={height}
@ -60,11 +57,9 @@ export function InteractiveMap({
onMouseEnter={() => setTooltipData(null)} onMouseEnter={() => setTooltipData(null)}
/> />
{filteredFeatures.map((feature, index) => { {filteredFeatures.map((feature, index) => {
const isHighlighted = countries.includes( const isHighlighted = countries.includes(feature.properties.iso_a2_eh)
feature.properties.iso_a2_eh,
);
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0; const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0
return ( return (
<path <path
@ -77,31 +72,29 @@ export function InteractiveMap({
} }
onMouseEnter={() => { onMouseEnter={() => {
if (!isHighlighted) { if (!isHighlighted) {
setTooltipData(null); setTooltipData(null)
return; return
} }
if (path.centroid(feature)) { if (path.centroid(feature)) {
const countryCode = feature.properties.iso_a2_eh; const countryCode = feature.properties.iso_a2_eh
const countryServers = nezhaServerList.result const countryServers = nezhaServerList.result
.filter( .filter(
(server: any) => (server: any) => server.host.CountryCode?.toUpperCase() === countryCode,
server.host.CountryCode?.toUpperCase() ===
countryCode,
) )
.map((server: any) => ({ .map((server: any) => ({
name: server.name, name: server.name,
status: server.online_status, status: server.online_status,
})); }))
setTooltipData({ setTooltipData({
centroid: path.centroid(feature), centroid: path.centroid(feature),
country: feature.properties.name, country: feature.properties.name,
count: serverCount, count: serverCount,
servers: countryServers, servers: countryServers,
}); })
} }
}} }}
/> />
); )
})} })}
{/* 渲染不在 filteredFeatures 中的国家标记点 */} {/* 渲染不在 filteredFeatures 中的国家标记点 */}
@ -109,38 +102,35 @@ export function InteractiveMap({
// 检查该国家是否已经在 filteredFeatures 中 // 检查该国家是否已经在 filteredFeatures 中
const isInFilteredFeatures = filteredFeatures.some( const isInFilteredFeatures = filteredFeatures.some(
(feature) => feature.properties.iso_a2_eh === countryCode, (feature) => feature.properties.iso_a2_eh === countryCode,
); )
// 如果已经在 filteredFeatures 中,跳过 // 如果已经在 filteredFeatures 中,跳过
if (isInFilteredFeatures) return null; if (isInFilteredFeatures) return null
// 获取国家的经纬度 // 获取国家的经纬度
const coords = countryCoordinates[countryCode]; const coords = countryCoordinates[countryCode]
if (!coords) return null; if (!coords) return null
// 使用投影函数将经纬度转换为 SVG 坐标 // 使用投影函数将经纬度转换为 SVG 坐标
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0]; const [x, y] = projection([coords.lng, coords.lat]) || [0, 0]
const serverCount = serverCounts[countryCode] || 0; const serverCount = serverCounts[countryCode] || 0
return ( return (
<g <g
key={countryCode} key={countryCode}
onMouseEnter={() => { onMouseEnter={() => {
const countryServers = nezhaServerList.result const countryServers = nezhaServerList.result
.filter( .filter((server: any) => server.host.CountryCode?.toUpperCase() === countryCode)
(server: any) =>
server.host.CountryCode?.toUpperCase() === countryCode,
)
.map((server: any) => ({ .map((server: any) => ({
name: server.name, name: server.name,
status: server.online_status, status: server.online_status,
})); }))
setTooltipData({ setTooltipData({
centroid: [x, y], centroid: [x, y],
country: coords.name, country: coords.name,
count: serverCount, count: serverCount,
servers: countryServers, servers: countryServers,
}); })
}} }}
className="cursor-pointer" className="cursor-pointer"
> >
@ -151,11 +141,11 @@ export function InteractiveMap({
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all" className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
/> />
</g> </g>
); )
})} })}
</g> </g>
</svg> </svg>
<MapTooltip /> <MapTooltip />
</div> </div>
); )
} }

View File

@ -1,20 +1,20 @@
"use client"; "use client"
import { AnimatePresence, m } from "framer-motion"; import { AnimatePresence, m } from "framer-motion"
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl"
import { memo } from "react"; import { memo } from "react"
import { useTooltip } from "./TooltipContext"; import { useTooltip } from "./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")
if (!tooltipData) return null; if (!tooltipData) return null
const sortedServers = tooltipData.servers.sort((a, b) => { const sortedServers = tooltipData.servers.sort((a, b) => {
return a.status === b.status ? 0 : a.status ? 1 : -1; return a.status === b.status ? 0 : a.status ? 1 : -1
}); })
return ( return (
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
@ -30,14 +30,12 @@ const MapTooltip = memo(function MapTooltip() {
transform: "translate(10%, -50%)", transform: "translate(10%, -50%)",
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.stopPropagation(); e.stopPropagation()
}} }}
> >
<div> <div>
<p className="font-medium"> <p className="font-medium">
{tooltipData.country === "China" {tooltipData.country === "China" ? "Mainland China" : tooltipData.country}
? "Mainland China"
: tooltipData.country}
</p> </p>
<p className="text-neutral-600 dark:text-neutral-400 mb-1"> <p className="text-neutral-600 dark:text-neutral-400 mb-1">
{tooltipData.count} {t("Servers")} {tooltipData.count} {t("Servers")}
@ -63,7 +61,7 @@ const MapTooltip = memo(function MapTooltip() {
</div> </div>
</m.div> </m.div>
</AnimatePresence> </AnimatePresence>
); )
}); })
export default MapTooltip; export default MapTooltip

View File

@ -1,14 +1,8 @@
"use client"; "use client"
import NetworkChartLoading from "@/app/(main)/ClientComponents/NetworkChartLoading"; import NetworkChartLoading from "@/app/(main)/ClientComponents/NetworkChartLoading"
import { NezhaAPIMonitor, ServerMonitorChart } from "@/app/types/nezha-api"; import { NezhaAPIMonitor, ServerMonitorChart } from "@/app/types/nezha-api"
import { import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { import {
ChartConfig, ChartConfig,
ChartContainer, ChartContainer,
@ -16,68 +10,59 @@ import {
ChartLegendContent, ChartLegendContent,
ChartTooltip, ChartTooltip,
ChartTooltipContent, ChartTooltipContent,
} from "@/components/ui/chart"; } from "@/components/ui/chart"
import { Label } from "@/components/ui/label"; 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 { 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"
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
import useSWR from "swr"; import useSWR from "swr"
interface ResultItem { interface ResultItem {
created_at: number; created_at: number
[key: string]: number; [key: string]: number
} }
export function NetworkChartClient({ export function NetworkChartClient({ server_id, show }: { server_id: number; show: boolean }) {
server_id, const t = useTranslations("NetworkChartClient")
show,
}: {
server_id: number;
show: boolean;
}) {
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}`,
nezhaFetcher, nezhaFetcher,
{ {
refreshInterval: refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 15000,
Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 15000,
isVisible: () => show, isVisible: () => show,
}, },
); )
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="text-sm font-medium opacity-40">{error.message}</p> <p className="text-sm font-medium opacity-40">{error.message}</p>
<p className="text-sm font-medium opacity-40"> <p className="text-sm font-medium opacity-40">{t("chart_fetch_error_message")}</p>
{t("chart_fetch_error_message")}
</p>
</div> </div>
<NetworkChartLoading /> <NetworkChartLoading />
</> </>
); )
} }
if (!data) return <NetworkChartLoading />; if (!data) return <NetworkChartLoading />
const transformedData = transformData(data); const transformedData = transformData(data)
const formattedData = formatData(data); const formattedData = formatData(data)
const initChartConfig = { const initChartConfig = {
avg_delay: { avg_delay: {
label: t("avg_delay"), label: t("avg_delay"),
}, },
} satisfies ChartConfig; } satisfies ChartConfig
const chartDataKey = Object.keys(transformedData); const chartDataKey = Object.keys(transformedData)
return ( return (
<NetworkChart <NetworkChart
@ -87,7 +72,7 @@ export function NetworkChartClient({
serverName={data[0].server_name} serverName={data[0].server_name}
formattedData={formattedData} formattedData={formattedData}
/> />
); )
} }
export const NetworkChart = React.memo(function NetworkChart({ export const NetworkChart = React.memo(function NetworkChart({
@ -97,33 +82,33 @@ export const NetworkChart = React.memo(function NetworkChart({
serverName, serverName,
formattedData, formattedData,
}: { }: {
chartDataKey: string[]; chartDataKey: string[]
chartConfig: ChartConfig; chartConfig: ChartConfig
chartData: ServerMonitorChart; chartData: ServerMonitorChart
serverName: string; serverName: string
formattedData: ResultItem[]; formattedData: ResultItem[]
}) { }) {
const t = useTranslations("NetworkChart"); const t = useTranslations("NetworkChart")
const defaultChart = "All"; const defaultChart = "All"
const [activeChart, setActiveChart] = React.useState(defaultChart); const [activeChart, setActiveChart] = React.useState(defaultChart)
const [isPeakEnabled, setIsPeakEnabled] = React.useState(false); const [isPeakEnabled, setIsPeakEnabled] = React.useState(false)
const handleButtonClick = useCallback( const handleButtonClick = useCallback(
(chart: string) => { (chart: string) => {
setActiveChart((prev) => (prev === chart ? defaultChart : chart)); setActiveChart((prev) => (prev === chart ? defaultChart : chart))
}, },
[defaultChart], [defaultChart],
); )
const getColorByIndex = useCallback( const getColorByIndex = useCallback(
(chart: string) => { (chart: string) => {
const index = chartDataKey.indexOf(chart); const index = chartDataKey.indexOf(chart)
return `hsl(var(--chart-${(index % 10) + 1}))`; return `hsl(var(--chart-${(index % 10) + 1}))`
}, },
[chartDataKey], [chartDataKey],
); )
const chartButtons = useMemo( const chartButtons = useMemo(
() => () =>
@ -134,16 +119,14 @@ export const NetworkChart = React.memo(function NetworkChart({
className={`relative z-30 flex cursor-pointer grow basis-0 flex-col justify-center gap-1 border-b border-neutral-200 dark:border-neutral-800 px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6`} className={`relative z-30 flex cursor-pointer grow basis-0 flex-col justify-center gap-1 border-b border-neutral-200 dark:border-neutral-800 px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6`}
onClick={() => handleButtonClick(key)} onClick={() => handleButtonClick(key)}
> >
<span className="whitespace-nowrap text-xs text-muted-foreground"> <span className="whitespace-nowrap text-xs text-muted-foreground">{key}</span>
{key}
</span>
<span className="text-md font-bold 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>
)), )),
[chartDataKey, activeChart, chartData, handleButtonClick], [chartDataKey, activeChart, chartData, handleButtonClick],
); )
const chartLines = useMemo(() => { const chartLines = useMemo(() => {
if (activeChart !== defaultChart) { if (activeChart !== defaultChart) {
@ -156,7 +139,7 @@ export const NetworkChart = React.memo(function NetworkChart({
dataKey="avg_delay" dataKey="avg_delay"
stroke={getColorByIndex(activeChart)} stroke={getColorByIndex(activeChart)}
/> />
); )
} }
return chartDataKey.map((key) => ( return chartDataKey.map((key) => (
<Line <Line
@ -169,65 +152,50 @@ export const NetworkChart = React.memo(function NetworkChart({
stroke={getColorByIndex(key)} stroke={getColorByIndex(key)}
connectNulls={true} connectNulls={true}
/> />
)); ))
}, [activeChart, defaultChart, chartDataKey, getColorByIndex]); }, [activeChart, defaultChart, chartDataKey, getColorByIndex])
const processedData = useMemo(() => { const processedData = useMemo(() => {
if (!isPeakEnabled) { if (!isPeakEnabled) {
return activeChart === defaultChart return activeChart === defaultChart ? formattedData : chartData[activeChart]
? formattedData
: chartData[activeChart];
} }
// 如果开启了削峰,对数据进行处理 // 如果开启了削峰,对数据进行处理
const data = ( const data = (
activeChart === defaultChart ? formattedData : chartData[activeChart] activeChart === defaultChart ? formattedData : chartData[activeChart]
) as ResultItem[]; ) as ResultItem[]
const windowSize = 7; // 增加到7个点的移动平均 const windowSize = 7 // 增加到7个点的移动平均
const weights = [0.1, 0.1, 0.15, 0.3, 0.15, 0.1, 0.1]; // 加权平均的权重 const weights = [0.1, 0.1, 0.15, 0.3, 0.15, 0.1, 0.1] // 加权平均的权重
return data.map((point, index) => { return data.map((point, index) => {
if (index < windowSize - 1) return point; if (index < windowSize - 1) return point
const window = data.slice(index - windowSize + 1, index + 1); const window = data.slice(index - windowSize + 1, index + 1)
const smoothed = { ...point } as ResultItem; const smoothed = { ...point } as ResultItem
if (activeChart === defaultChart) { if (activeChart === defaultChart) {
// 处理所有线路的数据 // 处理所有线路的数据
chartDataKey.forEach((key) => { 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[]
if (values.length === windowSize) { if (values.length === windowSize) {
smoothed[key] = values.reduce( smoothed[key] = values.reduce((acc, val, idx) => acc + val * weights[idx], 0)
(acc, val, idx) => acc + val * weights[idx],
0,
);
} }
}); })
} else { } else {
// 处理单条线路的数据 // 处理单条线路的数据
const values = window const values = window
.map((w) => w.avg_delay) .map((w) => w.avg_delay)
.filter((v) => v !== undefined && v !== null) as number[]; .filter((v) => v !== undefined && v !== null) as number[]
if (values.length === windowSize) { if (values.length === windowSize) {
smoothed.avg_delay = values.reduce( smoothed.avg_delay = values.reduce((acc, val, idx) => acc + val * weights[idx], 0)
(acc, val, idx) => acc + val * weights[idx],
0,
);
} }
} }
return smoothed; return smoothed
}); })
}, [ }, [isPeakEnabled, activeChart, formattedData, chartData, chartDataKey, defaultChart])
isPeakEnabled,
activeChart,
formattedData,
chartData,
chartDataKey,
defaultChart,
]);
return ( return (
<Card> <Card>
@ -240,11 +208,7 @@ export const NetworkChart = React.memo(function NetworkChart({
{chartDataKey.length} {t("ServerMonitorCount")} {chartDataKey.length} {t("ServerMonitorCount")}
</CardDescription> </CardDescription>
<div className="flex items-center mt-0.5 space-x-2"> <div className="flex items-center mt-0.5 space-x-2">
<Switch <Switch id="Peak" checked={isPeakEnabled} onCheckedChange={setIsPeakEnabled} />
id="Peak"
checked={isPeakEnabled}
onCheckedChange={setIsPeakEnabled}
/>
<Label className="text-xs" htmlFor="Peak"> <Label className="text-xs" htmlFor="Peak">
Peak cut Peak cut
</Label> </Label>
@ -253,15 +217,8 @@ export const NetworkChart = React.memo(function NetworkChart({
<div className="flex flex-wrap w-full">{chartButtons}</div> <div className="flex flex-wrap w-full">{chartButtons}</div>
</CardHeader> </CardHeader>
<CardContent className="pr-2 pl-0 py-4 sm:pt-6 sm:pb-6 sm:pr-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 <ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full">
config={chartConfig} <LineChart accessibilityLayer data={processedData} margin={{ left: 12, right: 12 }}>
className="aspect-auto h-[250px] w-full"
>
<LineChart
accessibilityLayer
data={processedData}
margin={{ left: 12, right: 12 }}
>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<XAxis <XAxis
dataKey="created_at" dataKey="created_at"
@ -286,67 +243,64 @@ export const NetworkChart = React.memo(function NetworkChart({
indicator={"line"} indicator={"line"}
labelKey="created_at" labelKey="created_at"
labelFormatter={(_, payload) => { labelFormatter={(_, payload) => {
return formatTime(payload[0].payload.created_at); return formatTime(payload[0].payload.created_at)
}} }}
/> />
} }
/> />
{activeChart === defaultChart && ( {activeChart === defaultChart && <ChartLegend content={<ChartLegendContent />} />}
<ChartLegend content={<ChartLegendContent />} />
)}
{chartLines} {chartLines}
</LineChart> </LineChart>
</ChartContainer> </ChartContainer>
</CardContent> </CardContent>
</Card> </Card>
); )
}); })
const transformData = (data: NezhaAPIMonitor[]) => { const transformData = (data: NezhaAPIMonitor[]) => {
const monitorData: ServerMonitorChart = {}; const monitorData: ServerMonitorChart = {}
data.forEach((item) => { data.forEach((item) => {
const monitorName = item.monitor_name; const monitorName = item.monitor_name
if (!monitorData[monitorName]) { if (!monitorData[monitorName]) {
monitorData[monitorName] = []; monitorData[monitorName] = []
} }
for (let i = 0; i < item.created_at.length; i++) { for (let i = 0; i < item.created_at.length; i++) {
monitorData[monitorName].push({ monitorData[monitorName].push({
created_at: item.created_at[i], created_at: item.created_at[i],
avg_delay: item.avg_delay[i], avg_delay: item.avg_delay[i],
}); })
} }
}); })
return monitorData; return monitorData
}; }
const formatData = (rawData: NezhaAPIMonitor[]) => { const formatData = (rawData: NezhaAPIMonitor[]) => {
const result: { [time: number]: ResultItem } = {}; const result: { [time: number]: ResultItem } = {}
const allTimes = new Set<number>(); const allTimes = new Set<number>()
rawData.forEach((item) => { rawData.forEach((item) => {
item.created_at.forEach((time) => allTimes.add(time)); item.created_at.forEach((time) => allTimes.add(time))
}); })
const allTimeArray = Array.from(allTimes).sort((a, b) => a - b); const allTimeArray = Array.from(allTimes).sort((a, b) => a - b)
rawData.forEach((item) => { rawData.forEach((item) => {
const { monitor_name, created_at, avg_delay } = item; const { monitor_name, created_at, avg_delay } = item
allTimeArray.forEach((time) => { allTimeArray.forEach((time) => {
if (!result[time]) { if (!result[time]) {
result[time] = { created_at: time }; result[time] = { created_at: time }
} }
const timeIndex = created_at.indexOf(time); const timeIndex = created_at.indexOf(time)
// @ts-expect-error - avg_delay is an array // @ts-expect-error - avg_delay is an array
result[time][monitor_name] = result[time][monitor_name] = timeIndex !== -1 ? avg_delay[timeIndex] : null
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)
}; }

View File

@ -1,5 +1,5 @@
import { Loader } from "@/components/loading/Loader"; import { Loader } from "@/components/loading/Loader"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
export default function NetworkChartLoading() { export default function NetworkChartLoading() {
return ( return (
@ -19,5 +19,5 @@ export default function NetworkChartLoading() {
<div className="aspect-auto h-[250px] w-full"></div> <div className="aspect-auto h-[250px] w-full"></div>
</CardContent> </CardContent>
</Card> </Card>
); )
} }

View File

@ -1,77 +1,59 @@
"use client"; "use client"
import { ServerDetailChartLoading } from "@/app/(main)/ClientComponents/ServerDetailLoading"; import { ServerDetailChartLoading } from "@/app/(main)/ClientComponents/ServerDetailLoading"
import { NezhaAPISafe, ServerApi } from "@/app/types/nezha-api"; import { NezhaAPISafe, ServerApi } from "@/app/types/nezha-api"
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 { ChartConfig, ChartContainer } from "@/components/ui/chart"; import { ChartConfig, ChartContainer } from "@/components/ui/chart"
import { import { formatBytes, formatNezhaInfo, formatRelativeTime, nezhaFetcher } from "@/lib/utils"
formatBytes, import { useTranslations } from "next-intl"
formatNezhaInfo, import { useEffect, useState } from "react"
formatRelativeTime, import { Area, AreaChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
nezhaFetcher, import useSWRImmutable from "swr/immutable"
} from "@/lib/utils";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import {
Area,
AreaChart,
CartesianGrid,
Line,
LineChart,
XAxis,
YAxis,
} from "recharts";
import useSWRImmutable from "swr/immutable";
type cpuChartData = { type cpuChartData = {
timeStamp: string; timeStamp: string
cpu: number; cpu: number
}; }
type processChartData = { type processChartData = {
timeStamp: string; timeStamp: string
process: number; process: number
}; }
type diskChartData = { type diskChartData = {
timeStamp: string; timeStamp: string
disk: number; disk: number
}; }
type memChartData = { type memChartData = {
timeStamp: string; timeStamp: string
mem: number; mem: number
swap: number; swap: number
}; }
type networkChartData = { type networkChartData = {
timeStamp: string; timeStamp: string
upload: number; upload: number
download: number; download: number
}; }
type connectChartData = { type connectChartData = {
timeStamp: string; timeStamp: string
tcp: number; tcp: number
udp: number; udp: number
}; }
export default function ServerDetailChartClient({ export default function ServerDetailChartClient({
server_id, server_id,
}: { }: {
server_id: number; server_id: number
show: boolean; show: boolean
}) { }) {
const t = useTranslations("ServerDetailChartClient"); const t = useTranslations("ServerDetailChartClient")
const { data: allFallbackData } = useSWRImmutable<ServerApi>( const { data: allFallbackData } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
"/api/server", const fallbackData = allFallbackData?.result?.find((item) => item.id === server_id)
nezhaFetcher,
);
const fallbackData = allFallbackData?.result?.find(
(item) => item.id === server_id,
);
const { data, error } = useSWRImmutable<NezhaAPISafe>( const { data, error } = useSWRImmutable<NezhaAPISafe>(
`/api/detail?server_id=${server_id}`, `/api/detail?server_id=${server_id}`,
@ -79,21 +61,19 @@ export default function ServerDetailChartClient({
{ {
fallbackData, fallbackData,
}, },
); )
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="text-sm font-medium opacity-40">{error.message}</p> <p className="text-sm font-medium opacity-40">{error.message}</p>
<p className="text-sm font-medium opacity-40"> <p className="text-sm font-medium opacity-40">{t("chart_fetch_error_message")}</p>
{t("chart_fetch_error_message")}
</p>
</div> </div>
</> </>
); )
} }
if (!data) return <ServerDetailChartLoading />; if (!data) return <ServerDetailChartLoading />
return ( return (
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3"> <section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
@ -104,38 +84,38 @@ export default function ServerDetailChartClient({
<NetworkChart data={data} /> <NetworkChart data={data} />
<ConnectChart data={data} /> <ConnectChart data={data} />
</section> </section>
); )
} }
function CpuChart({ data }: { data: NezhaAPISafe }) { function CpuChart({ data }: { data: NezhaAPISafe }) {
const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[]); const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[])
const { cpu } = formatNezhaInfo(data); const { cpu } = formatNezhaInfo(data)
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const timestamp = Date.now().toString(); const timestamp = Date.now().toString()
let newData = [] as cpuChartData[]; let newData = [] as cpuChartData[]
if (cpuChartData.length === 0) { if (cpuChartData.length === 0) {
newData = [ newData = [
{ timeStamp: timestamp, cpu: cpu }, { timeStamp: timestamp, cpu: cpu },
{ timeStamp: timestamp, cpu: cpu }, { timeStamp: timestamp, cpu: cpu },
]; ]
} else { } else {
newData = [...cpuChartData, { timeStamp: timestamp, cpu: cpu }]; newData = [...cpuChartData, { timeStamp: timestamp, cpu: cpu }]
} }
if (newData.length > 30) { if (newData.length > 30) {
newData.shift(); newData.shift()
} }
setCpuChartData(newData); setCpuChartData(newData)
} }
}, [data]); }, [data])
const chartConfig = { const chartConfig = {
cpu: { cpu: {
label: "CPU", label: "CPU",
}, },
} satisfies ChartConfig; } satisfies ChartConfig
return ( return (
<Card> <Card>
@ -144,9 +124,7 @@ function CpuChart({ data }: { data: NezhaAPISafe }) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-md font-medium">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="text-xs text-end w-10 font-medium"> <p className="text-xs text-end w-10 font-medium">{cpu.toFixed(0)}%</p>
{cpu.toFixed(0)}%
</p>
<AnimatedCircularProgressBar <AnimatedCircularProgressBar
className="size-3 text-[0px]" className="size-3 text-[0px]"
max={100} max={100}
@ -156,10 +134,7 @@ function CpuChart({ data }: { data: NezhaAPISafe }) {
/> />
</section> </section>
</div> </div>
<ChartContainer <ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={cpuChartData} data={cpuChartData}
@ -200,45 +175,40 @@ function CpuChart({ data }: { data: NezhaAPISafe }) {
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
); )
} }
function ProcessChart({ data }: { data: NezhaAPISafe }) { function ProcessChart({ data }: { data: NezhaAPISafe }) {
const t = useTranslations("ServerDetailChartClient"); const t = useTranslations("ServerDetailChartClient")
const [processChartData, setProcessChartData] = useState( const [processChartData, setProcessChartData] = useState([] as processChartData[])
[] as processChartData[],
);
const { process } = formatNezhaInfo(data); const { process } = formatNezhaInfo(data)
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const timestamp = Date.now().toString(); const timestamp = Date.now().toString()
let newData = [] as processChartData[]; let newData = [] as processChartData[]
if (processChartData.length === 0) { if (processChartData.length === 0) {
newData = [ newData = [
{ timeStamp: timestamp, process: process }, { timeStamp: timestamp, process: process },
{ timeStamp: timestamp, process: process }, { timeStamp: timestamp, process: process },
]; ]
} else { } else {
newData = [ newData = [...processChartData, { timeStamp: timestamp, process: process }]
...processChartData,
{ timeStamp: timestamp, process: process },
];
} }
if (newData.length > 30) { if (newData.length > 30) {
newData.shift(); newData.shift()
} }
setProcessChartData(newData); setProcessChartData(newData)
} }
}, [data]); }, [data])
const chartConfig = { const chartConfig = {
process: { process: {
label: "Process", label: "Process",
}, },
} satisfies ChartConfig; } satisfies ChartConfig
return ( return (
<Card> <Card>
@ -250,10 +220,7 @@ function ProcessChart({ data }: { data: NezhaAPISafe }) {
<p className="text-xs text-end w-10 font-medium">{process}</p> <p className="text-xs text-end w-10 font-medium">{process}</p>
</section> </section>
</div> </div>
<ChartContainer <ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={processChartData} data={processChartData}
@ -273,12 +240,7 @@ function ProcessChart({ data }: { data: NezhaAPISafe }) {
interval="preserveStartEnd" interval="preserveStartEnd"
tickFormatter={(value) => formatRelativeTime(value)} tickFormatter={(value) => formatRelativeTime(value)}
/> />
<YAxis <YAxis tickLine={false} axisLine={false} mirror={true} tickMargin={-15} />
tickLine={false}
axisLine={false}
mirror={true}
tickMargin={-15}
/>
<Area <Area
isAnimationActive={false} isAnimationActive={false}
dataKey="process" dataKey="process"
@ -292,37 +254,34 @@ function ProcessChart({ data }: { data: NezhaAPISafe }) {
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
); )
} }
function MemChart({ data }: { data: NezhaAPISafe }) { function MemChart({ data }: { data: NezhaAPISafe }) {
const t = useTranslations("ServerDetailChartClient"); const t = useTranslations("ServerDetailChartClient")
const [memChartData, setMemChartData] = useState([] as memChartData[]); const [memChartData, setMemChartData] = useState([] as memChartData[])
const { mem, swap } = formatNezhaInfo(data); const { mem, swap } = formatNezhaInfo(data)
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const timestamp = Date.now().toString(); const timestamp = Date.now().toString()
let newData = [] as memChartData[]; let newData = [] as memChartData[]
if (memChartData.length === 0) { if (memChartData.length === 0) {
newData = [ newData = [
{ timeStamp: timestamp, mem: mem, swap: swap }, { timeStamp: timestamp, mem: mem, swap: swap },
{ timeStamp: timestamp, mem: mem, swap: swap }, { timeStamp: timestamp, mem: mem, swap: swap },
]; ]
} else { } else {
newData = [ newData = [...memChartData, { timeStamp: timestamp, mem: mem, swap: swap }]
...memChartData,
{ timeStamp: timestamp, mem: mem, swap: swap },
];
} }
if (newData.length > 30) { if (newData.length > 30) {
newData.shift(); newData.shift()
} }
setMemChartData(newData); setMemChartData(newData)
} }
}, [data]); }, [data])
const chartConfig = { const chartConfig = {
mem: { mem: {
@ -331,7 +290,7 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
swap: { swap: {
label: "Swap", label: "Swap",
}, },
} satisfies ChartConfig; } satisfies ChartConfig
return ( return (
<Card> <Card>
@ -368,18 +327,14 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
</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 text-[11px] font-medium items-center gap-2"> <div className="flex text-[11px] font-medium items-center gap-2">
{formatBytes(data.status.MemUsed)} /{" "} {formatBytes(data.status.MemUsed)} / {formatBytes(data.host.MemTotal)}
{formatBytes(data.host.MemTotal)}
</div> </div>
<div className="flex text-[11px] font-medium items-center gap-2"> <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>
</div> </div>
<ChartContainer <ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={memChartData} data={memChartData}
@ -428,40 +383,40 @@ function MemChart({ data }: { data: NezhaAPISafe }) {
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
); )
} }
function DiskChart({ data }: { data: NezhaAPISafe }) { function DiskChart({ data }: { data: NezhaAPISafe }) {
const t = useTranslations("ServerDetailChartClient"); const t = useTranslations("ServerDetailChartClient")
const [diskChartData, setDiskChartData] = useState([] as diskChartData[]); const [diskChartData, setDiskChartData] = useState([] as diskChartData[])
const { disk } = formatNezhaInfo(data); const { disk } = formatNezhaInfo(data)
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const timestamp = Date.now().toString(); const timestamp = Date.now().toString()
let newData = [] as diskChartData[]; let newData = [] as diskChartData[]
if (diskChartData.length === 0) { if (diskChartData.length === 0) {
newData = [ newData = [
{ timeStamp: timestamp, disk: disk }, { timeStamp: timestamp, disk: disk },
{ timeStamp: timestamp, disk: disk }, { timeStamp: timestamp, disk: disk },
]; ]
} else { } else {
newData = [...diskChartData, { timeStamp: timestamp, disk: disk }]; newData = [...diskChartData, { timeStamp: timestamp, disk: disk }]
} }
if (newData.length > 30) { if (newData.length > 30) {
newData.shift(); newData.shift()
} }
setDiskChartData(newData); setDiskChartData(newData)
} }
}, [data]); }, [data])
const chartConfig = { const chartConfig = {
disk: { disk: {
label: "Disk", label: "Disk",
}, },
} satisfies ChartConfig; } satisfies ChartConfig
return ( return (
<Card> <Card>
@ -471,9 +426,7 @@ function DiskChart({ data }: { data: NezhaAPISafe }) {
<p className="text-md font-medium">{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="text-xs text-end w-10 font-medium"> <p className="text-xs text-end w-10 font-medium">{disk.toFixed(0)}%</p>
{disk.toFixed(0)}%
</p>
<AnimatedCircularProgressBar <AnimatedCircularProgressBar
className="size-3 text-[0px]" className="size-3 text-[0px]"
max={100} max={100}
@ -483,15 +436,11 @@ function DiskChart({ data }: { data: NezhaAPISafe }) {
/> />
</section> </section>
<div className="flex text-[11px] font-medium items-center gap-2"> <div className="flex text-[11px] font-medium items-center gap-2">
{formatBytes(data.status.DiskUsed)} /{" "} {formatBytes(data.status.DiskUsed)} / {formatBytes(data.host.DiskTotal)}
{formatBytes(data.host.DiskTotal)}
</div> </div>
</section> </section>
</div> </div>
<ChartContainer <ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={diskChartData} data={diskChartData}
@ -532,44 +481,39 @@ function DiskChart({ data }: { data: NezhaAPISafe }) {
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
); )
} }
function NetworkChart({ data }: { data: NezhaAPISafe }) { function NetworkChart({ data }: { data: NezhaAPISafe }) {
const t = useTranslations("ServerDetailChartClient"); const t = useTranslations("ServerDetailChartClient")
const [networkChartData, setNetworkChartData] = useState( const [networkChartData, setNetworkChartData] = useState([] as networkChartData[])
[] as networkChartData[],
);
const { up, down } = formatNezhaInfo(data); const { up, down } = formatNezhaInfo(data)
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const timestamp = Date.now().toString(); const timestamp = Date.now().toString()
let newData = [] as networkChartData[]; let newData = [] as networkChartData[]
if (networkChartData.length === 0) { if (networkChartData.length === 0) {
newData = [ newData = [
{ timeStamp: timestamp, upload: up, download: down }, { timeStamp: timestamp, upload: up, download: down },
{ timeStamp: timestamp, upload: up, download: down }, { timeStamp: timestamp, upload: up, download: down },
]; ]
} else { } else {
newData = [ newData = [...networkChartData, { timeStamp: timestamp, upload: up, download: down }]
...networkChartData,
{ timeStamp: timestamp, upload: up, download: down },
];
} }
if (newData.length > 30) { if (newData.length > 30) {
newData.shift(); newData.shift()
} }
setNetworkChartData(newData); setNetworkChartData(newData)
} }
}, [data]); }, [data])
let maxDownload = Math.max(...networkChartData.map((item) => item.download)); let maxDownload = Math.max(...networkChartData.map((item) => item.download))
maxDownload = Math.ceil(maxDownload); maxDownload = Math.ceil(maxDownload)
if (maxDownload < 1) { if (maxDownload < 1) {
maxDownload = 1; maxDownload = 1
} }
const chartConfig = { const chartConfig = {
@ -579,7 +523,7 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
download: { download: {
label: "Download", label: "Download",
}, },
} satisfies ChartConfig; } satisfies ChartConfig
return ( return (
<Card> <Card>
@ -595,9 +539,7 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
</div> </div>
</div> </div>
<div className="flex flex-col w-20"> <div className="flex flex-col w-20">
<p className=" text-xs text-muted-foreground"> <p className=" text-xs text-muted-foreground">{t("Download")}</p>
{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> <span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]"></span>
<p className="text-xs font-medium">{down.toFixed(2)} M/s</p> <p className="text-xs font-medium">{down.toFixed(2)} M/s</p>
@ -605,10 +547,7 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
</div> </div>
</section> </section>
</div> </div>
<ChartContainer <ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<LineChart <LineChart
accessibilityLayer accessibilityLayer
data={networkChartData} data={networkChartData}
@ -660,37 +599,32 @@ function NetworkChart({ data }: { data: NezhaAPISafe }) {
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
); )
} }
function ConnectChart({ data }: { data: NezhaAPISafe }) { function ConnectChart({ data }: { data: NezhaAPISafe }) {
const [connectChartData, setConnectChartData] = useState( const [connectChartData, setConnectChartData] = useState([] as connectChartData[])
[] as connectChartData[],
);
const { tcp, udp } = formatNezhaInfo(data); const { tcp, udp } = formatNezhaInfo(data)
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const timestamp = Date.now().toString(); const timestamp = Date.now().toString()
let newData = [] as connectChartData[]; let newData = [] as connectChartData[]
if (connectChartData.length === 0) { if (connectChartData.length === 0) {
newData = [ newData = [
{ timeStamp: timestamp, tcp: tcp, udp: udp }, { timeStamp: timestamp, tcp: tcp, udp: udp },
{ timeStamp: timestamp, tcp: tcp, udp: udp }, { timeStamp: timestamp, tcp: tcp, udp: udp },
]; ]
} else { } else {
newData = [ newData = [...connectChartData, { timeStamp: timestamp, tcp: tcp, udp: udp }]
...connectChartData,
{ timeStamp: timestamp, tcp: tcp, udp: udp },
];
} }
if (newData.length > 30) { if (newData.length > 30) {
newData.shift(); newData.shift()
} }
setConnectChartData(newData); setConnectChartData(newData)
} }
}, [data]); }, [data])
const chartConfig = { const chartConfig = {
tcp: { tcp: {
@ -699,7 +633,7 @@ function ConnectChart({ data }: { data: NezhaAPISafe }) {
udp: { udp: {
label: "UDP", label: "UDP",
}, },
} satisfies ChartConfig; } satisfies ChartConfig
return ( return (
<Card> <Card>
@ -723,10 +657,7 @@ function ConnectChart({ data }: { data: NezhaAPISafe }) {
</div> </div>
</section> </section>
</div> </div>
<ChartContainer <ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
config={chartConfig}
className="aspect-auto h-[130px] w-full"
>
<LineChart <LineChart
accessibilityLayer accessibilityLayer
data={connectChartData} data={connectChartData}
@ -775,5 +706,5 @@ function ConnectChart({ data }: { data: NezhaAPISafe }) {
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
); )
} }

View File

@ -1,86 +1,74 @@
"use client"; "use client"
import { ServerDetailLoading } from "@/app/(main)/ClientComponents/ServerDetailLoading"; import { ServerDetailLoading } from "@/app/(main)/ClientComponents/ServerDetailLoading"
import { NezhaAPISafe, ServerApi } from "@/app/types/nezha-api"; import { NezhaAPISafe, ServerApi } from "@/app/types/nezha-api"
import { BackIcon } from "@/components/Icon"; import { BackIcon } from "@/components/Icon"
import ServerFlag from "@/components/ServerFlag"; import ServerFlag from "@/components/ServerFlag"
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 getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry"
import { cn, formatBytes, nezhaFetcher } from "@/lib/utils"; import { cn, formatBytes, nezhaFetcher } from "@/lib/utils"
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"
import useSWR from "swr"; import useSWR from "swr"
import useSWRImmutable from "swr/immutable"; import useSWRImmutable from "swr/immutable"
export default function ServerDetailClient({ export default function ServerDetailClient({ server_id }: { server_id: number }) {
server_id, const t = useTranslations("ServerDetailClient")
}: { const router = useRouter()
server_id: number;
}) {
const t = useTranslations("ServerDetailClient");
const router = useRouter();
const [hasHistory, setHasHistory] = useState(false); const [hasHistory, setHasHistory] = useState(false)
useEffect(() => { useEffect(() => {
window.scrollTo({ top: 0, left: 0, behavior: "instant" }); window.scrollTo({ top: 0, left: 0, behavior: "instant" })
}, []); }, [])
useEffect(() => { useEffect(() => {
const previousPath = sessionStorage.getItem("fromMainPage"); const previousPath = sessionStorage.getItem("fromMainPage")
if (previousPath) { if (previousPath) {
setHasHistory(true); setHasHistory(true)
} }
}, []); }, [])
const linkClick = () => { const linkClick = () => {
if (hasHistory) { if (hasHistory) {
router.back(); router.back()
} else { } else {
router.push(`/`); router.push(`/`)
} }
}; }
const { data: allFallbackData, isLoading } = useSWRImmutable<ServerApi>( const { data: allFallbackData, isLoading } = useSWRImmutable<ServerApi>(
"/api/server", "/api/server",
nezhaFetcher, nezhaFetcher,
); )
const fallbackData = allFallbackData?.result?.find( const fallbackData = allFallbackData?.result?.find((item) => item.id === server_id)
(item) => item.id === server_id,
);
if (!fallbackData && !isLoading) { if (!fallbackData && !isLoading) {
notFound(); notFound()
} }
const { data, error } = useSWR<NezhaAPISafe>( const { data, error } = useSWR<NezhaAPISafe>(`/api/detail?server_id=${server_id}`, nezhaFetcher, {
`/api/detail?server_id=${server_id}`, refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000,
nezhaFetcher, dedupingInterval: 1000,
{ fallbackData,
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 5000, revalidateOnMount: false,
dedupingInterval: 1000, revalidateIfStale: false,
fallbackData, })
revalidateOnMount: false,
revalidateIfStale: false,
},
);
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="text-sm font-medium opacity-40">{error.message}</p> <p className="text-sm font-medium opacity-40">{error.message}</p>
<p className="text-sm font-medium opacity-40"> <p className="text-sm font-medium opacity-40">{t("detail_fetch_error_message")}</p>
{t("detail_fetch_error_message")}
</p>
</div> </div>
</> </>
); )
} }
if (!data) return <ServerDetailLoading />; if (!data) return <ServerDetailLoading />
return ( return (
<div> <div>
@ -163,9 +151,7 @@ export default function ServerDetailClient({
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("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-xs text-start"> <div className="text-xs text-start">{data?.host.CountryCode.toUpperCase()}</div>
{data?.host.CountryCode.toUpperCase()}
</div>
<ServerFlag <ServerFlag
className="text-[11px] -mt-[1px]" className="text-[11px] -mt-[1px]"
country_code={data?.host.CountryCode} country_code={data?.host.CountryCode}
@ -218,9 +204,8 @@ export default function ServerDetailClient({
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Load")}</p> <p className="text-xs text-muted-foreground">{t("Load")}</p>
<div className="text-xs"> <div className="text-xs">
{data.status.Load1.toFixed(2) || "0.00"} /{" "} {data.status.Load1.toFixed(2) || "0.00"} / {data.status.Load5.toFixed(2) || "0.00"}{" "}
{data.status.Load5.toFixed(2) || "0.00"} /{" "} / {data.status.Load15.toFixed(2) || "0.00"}
{data.status.Load15.toFixed(2) || "0.00"}
</div> </div>
</section> </section>
</CardContent> </CardContent>
@ -230,10 +215,7 @@ export default function ServerDetailClient({
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Upload")}</p> <p className="text-xs text-muted-foreground">{t("Upload")}</p>
{data.status.NetOutTransfer ? ( {data.status.NetOutTransfer ? (
<div className="text-xs"> <div className="text-xs"> {formatBytes(data.status.NetOutTransfer)} </div>
{" "}
{formatBytes(data.status.NetOutTransfer)}{" "}
</div>
) : ( ) : (
<div className="text-xs">Unknown</div> <div className="text-xs">Unknown</div>
)} )}
@ -245,10 +227,7 @@ export default function ServerDetailClient({
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Download")}</p> <p className="text-xs text-muted-foreground">{t("Download")}</p>
{data.status.NetInTransfer ? ( {data.status.NetInTransfer ? (
<div className="text-xs"> <div className="text-xs"> {formatBytes(data.status.NetInTransfer)} </div>
{" "}
{formatBytes(data.status.NetInTransfer)}{" "}
</div>
) : ( ) : (
<div className="text-xs">Unknown</div> <div className="text-xs">Unknown</div>
)} )}
@ -257,5 +236,5 @@ export default function ServerDetailClient({
</Card> </Card>
</section> </section>
</div> </div>
); )
} }

View File

@ -1,6 +1,6 @@
import { BackIcon } from "@/components/Icon"; import { BackIcon } from "@/components/Icon"
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton"
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation"
export function ServerDetailChartLoading() { export function ServerDetailChartLoading() {
return ( return (
@ -14,17 +14,17 @@ export function ServerDetailChartLoading() {
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</section> </section>
</div> </div>
); )
} }
export function ServerDetailLoading() { export function ServerDetailLoading() {
const router = useRouter(); const router = useRouter()
return ( return (
<> <>
<div <div
onClick={() => { onClick={() => {
router.push(`/`); router.push(`/`)
}} }}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl" className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
> >
@ -33,5 +33,5 @@ export function ServerDetailLoading() {
</div> </div>
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</> </>
); )
} }

View File

@ -1,26 +1,23 @@
"use client"; "use client"
import { 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"
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl"
import useSWRImmutable from "swr/immutable"; import useSWRImmutable from "swr/immutable"
export default function ServerIPInfo({ server_id }: { server_id: number }) { export default function ServerIPInfo({ server_id }: { server_id: number }) {
const t = useTranslations("IPInfo"); const t = useTranslations("IPInfo")
const { data } = useSWRImmutable<IPInfo>( const { data } = useSWRImmutable<IPInfo>(`/api/server-ip?server_id=${server_id}`, nezhaFetcher)
`/api/server-ip?server_id=${server_id}`,
nezhaFetcher,
);
if (!data) { if (!data) {
return ( return (
<div className="mb-11"> <div className="mb-11">
<Loader visible /> <Loader visible />
</div> </div>
); )
} }
return ( return (
@ -31,9 +28,7 @@ export default function ServerIPInfo({ server_id }: { server_id: number }) {
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"ASN"}</p> <p className="text-xs text-muted-foreground">{"ASN"}</p>
<div className="text-xs"> <div className="text-xs">{data.asn.autonomous_system_organization}</div>
{data.asn.autonomous_system_organization}
</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
@ -42,12 +37,8 @@ export default function ServerIPInfo({ server_id }: { server_id: number }) {
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">{t("asn_number")}</p>
{t("asn_number")} <div className="text-xs">AS{data.asn.autonomous_system_number}</div>
</p>
<div className="text-xs">
AS{data.asn.autonomous_system_number}
</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
@ -56,12 +47,8 @@ export default function ServerIPInfo({ server_id }: { server_id: number }) {
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">{t("registered_country")}</p>
{t("registered_country")} <div className="text-xs">{data.city.registered_country?.names.en}</div>
</p>
<div className="text-xs">
{data.city.registered_country?.names.en}
</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
@ -90,9 +77,7 @@ export default function ServerIPInfo({ server_id }: { server_id: number }) {
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">{t("longitude")}</p>
{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>
@ -112,9 +97,7 @@ export default function ServerIPInfo({ server_id }: { server_id: number }) {
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">{t("time_zone")}</p>
{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>
@ -124,9 +107,7 @@ export default function ServerIPInfo({ server_id }: { server_id: number }) {
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">{t("postal_code")}</p>
{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>
@ -134,5 +115,5 @@ export default function ServerIPInfo({ server_id }: { server_id: number }) {
)} )}
</section> </section>
</> </>
); )
} }

View File

@ -1,82 +1,79 @@
"use client"; "use client"
import { ServerApi } from "@/app/types/nezha-api"; import { ServerApi } from "@/app/types/nezha-api"
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 getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry"
import { useFilter } from "@/lib/network-filter-context"; import { useFilter } from "@/lib/network-filter-context"
import { useStatus } from "@/lib/status-context"; import { useStatus } from "@/lib/status-context"
import { cn, nezhaFetcher } from "@/lib/utils"; import { cn, nezhaFetcher } 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 useSWR from "swr"; import useSWR from "swr"
import GlobalLoading from "./GlobalLoading"; import GlobalLoading from "./GlobalLoading"
const ServerGlobal = dynamic(() => import("./Global"), { const ServerGlobal = dynamic(() => import("./Global"), {
ssr: false, ssr: false,
loading: () => <GlobalLoading />, loading: () => <GlobalLoading />,
}); })
export default function ServerListClient() { export default function ServerListClient() {
const { status } = useStatus(); const { status } = useStatus()
const { filter } = useFilter(); const { filter } = useFilter()
const t = useTranslations("ServerListClient"); const t = useTranslations("ServerListClient")
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null)
const defaultTag = "defaultTag"; const defaultTag = "defaultTag"
const [tag, setTag] = useState<string>(defaultTag); const [tag, setTag] = useState<string>(defaultTag)
const [showMap, setShowMap] = useState<boolean>(false); const [showMap, setShowMap] = useState<boolean>(false)
const [inline, setInline] = useState<string>("0"); const [inline, setInline] = useState<string>("0")
useEffect(() => { useEffect(() => {
const inlineState = localStorage.getItem("inline"); const inlineState = localStorage.getItem("inline")
if (inlineState !== null) { if (inlineState !== null) {
setInline(inlineState); setInline(inlineState)
} }
}, []); }, [])
useEffect(() => { useEffect(() => {
const savedTag = sessionStorage.getItem("selectedTag") || defaultTag; const savedTag = sessionStorage.getItem("selectedTag") || defaultTag
setTag(savedTag); setTag(savedTag)
restoreScrollPosition(); restoreScrollPosition()
}, []); }, [])
const handleTagChange = (newTag: string) => { const handleTagChange = (newTag: string) => {
setTag(newTag); setTag(newTag)
sessionStorage.setItem("selectedTag", newTag); sessionStorage.setItem("selectedTag", newTag)
sessionStorage.setItem( sessionStorage.setItem("scrollPosition", String(containerRef.current?.scrollTop || 0))
"scrollPosition", }
String(containerRef.current?.scrollTop || 0),
);
};
const restoreScrollPosition = () => { const restoreScrollPosition = () => {
const savedPosition = sessionStorage.getItem("scrollPosition"); const savedPosition = sessionStorage.getItem("scrollPosition")
if (savedPosition && containerRef.current) { if (savedPosition && containerRef.current) {
containerRef.current.scrollTop = Number(savedPosition); containerRef.current.scrollTop = Number(savedPosition)
} }
}; }
useEffect(() => { useEffect(() => {
const handleRouteChange = () => { const handleRouteChange = () => {
restoreScrollPosition(); restoreScrollPosition()
}; }
window.addEventListener("popstate", handleRouteChange); window.addEventListener("popstate", handleRouteChange)
return () => { return () => {
window.removeEventListener("popstate", handleRouteChange); window.removeEventListener("popstate", handleRouteChange)
}; }
}, []); }, [])
const { data, error } = useSWR<ServerApi>("/api/server", nezhaFetcher, { const { data, error } = useSWR<ServerApi>("/api/server", nezhaFetcher, {
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000, refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000,
dedupingInterval: 1000, dedupingInterval: 1000,
}); })
if (error) if (error)
return ( return (
@ -84,61 +81,57 @@ export default function ServerListClient() {
<p className="text-sm font-medium opacity-40">{error.message}</p> <p className="text-sm font-medium opacity-40">{error.message}</p>
<p className="text-sm font-medium opacity-40">{t("error_message")}</p> <p className="text-sm font-medium opacity-40">{t("error_message")}</p>
</div> </div>
); )
if (!data?.result) return null; if (!data?.result) return null
const { result } = data; const { result } = data
const sortedServers = result.sort((a, b) => { const sortedServers = result.sort((a, b) => {
const displayIndexDiff = (b.display_index || 0) - (a.display_index || 0); const displayIndexDiff = (b.display_index || 0) - (a.display_index || 0)
if (displayIndexDiff !== 0) return displayIndexDiff; if (displayIndexDiff !== 0) return displayIndexDiff
return a.id - b.id; return a.id - b.id
}); })
const filteredServersByStatus = const filteredServersByStatus =
status === "all" status === "all"
? sortedServers ? sortedServers
: sortedServers.filter((server) => : sortedServers.filter((server) =>
[status].includes(server.online_status ? "online" : "offline"), [status].includes(server.online_status ? "online" : "offline"),
); )
const allTag = filteredServersByStatus const allTag = filteredServersByStatus.map((server) => server.tag).filter(Boolean)
.map((server) => server.tag) const uniqueTags = [...new Set(allTag)]
.filter(Boolean); uniqueTags.unshift(defaultTag)
const uniqueTags = [...new Set(allTag)];
uniqueTags.unshift(defaultTag);
const filteredServers = const filteredServers =
tag === defaultTag tag === defaultTag
? filteredServersByStatus ? filteredServersByStatus
: filteredServersByStatus.filter((server) => server.tag === tag); : filteredServersByStatus.filter((server) => server.tag === tag)
if (filter) { if (filter) {
filteredServers.sort((a, b) => { 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 -1; if (a.online_status && !b.online_status) return -1
if (!a.online_status && !b.online_status) return 0; if (!a.online_status && !b.online_status) return 0
return ( return (
b.status.NetInSpeed + b.status.NetInSpeed + b.status.NetOutSpeed - (a.status.NetInSpeed + a.status.NetOutSpeed)
b.status.NetOutSpeed - )
(a.status.NetInSpeed + a.status.NetOutSpeed) })
);
});
} }
const tagCountMap: Record<string, number> = {}; const tagCountMap: Record<string, number> = {}
filteredServersByStatus.forEach((server) => { filteredServersByStatus.forEach((server) => {
if (server.tag) { if (server.tag) {
tagCountMap[server.tag] = (tagCountMap[server.tag] || 0) + 1; tagCountMap[server.tag] = (tagCountMap[server.tag] || 0) + 1
} }
}); })
return ( return (
<> <>
<section className="flex items-center gap-2 w-full overflow-hidden"> <section className="flex items-center gap-2 w-full overflow-hidden">
<button <button
onClick={() => { onClick={() => {
setShowMap(!showMap); setShowMap(!showMap)
}} }}
className={cn( className={cn(
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]", "rounded-[50px] 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)]",
@ -151,14 +144,13 @@ export default function ServerListClient() {
</button> </button>
<button <button
onClick={() => { onClick={() => {
setInline(inline === "0" ? "1" : "0"); setInline(inline === "0" ? "1" : "0")
localStorage.setItem("inline", inline === "0" ? "1" : "0"); localStorage.setItem("inline", inline === "0" ? "1" : "0")
}} }}
className={cn( className={cn(
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ", "rounded-[50px] 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)] ",
{ {
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": "shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": inline === "1",
inline === "1",
}, },
)} )}
> >
@ -186,15 +178,12 @@ export default function ServerListClient() {
)} )}
{inline === "0" && ( {inline === "0" && (
<section <section ref={containerRef} className="grid grid-cols-1 gap-2 md:grid-cols-2">
ref={containerRef}
className="grid grid-cols-1 gap-2 md:grid-cols-2"
>
{filteredServers.map((serverInfo) => ( {filteredServers.map((serverInfo) => (
<ServerCard key={serverInfo.id} serverInfo={serverInfo} /> <ServerCard key={serverInfo.id} serverInfo={serverInfo} />
))} ))}
</section> </section>
)} )}
</> </>
); )
} }

View File

@ -1,31 +1,25 @@
"use client"; "use client"
import { ServerApi } from "@/app/types/nezha-api"; import { ServerApi } from "@/app/types/nezha-api"
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 { useFilter } from "@/lib/network-filter-context"
import { useStatus } from "@/lib/status-context"; import { useStatus } from "@/lib/status-context"
import { cn, formatBytes, nezhaFetcher } from "@/lib/utils"; import { cn, formatBytes, nezhaFetcher } from "@/lib/utils"
import blogMan from "@/public/blog-man.webp"; import blogMan from "@/public/blog-man.webp"
import { import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
ArrowDownCircleIcon, import { useTranslations } from "next-intl"
ArrowUpCircleIcon, import Image from "next/image"
} from "@heroicons/react/20/solid"; import useSWRImmutable from "swr/immutable"
import { useTranslations } from "next-intl";
import Image from "next/image";
import useSWRImmutable from "swr/immutable";
export default function ServerOverviewClient() { export default function ServerOverviewClient() {
const { status, setStatus } = useStatus(); const { status, setStatus } = useStatus()
const { filter, setFilter } = useFilter(); const { filter, setFilter } = useFilter()
const t = useTranslations("ServerOverviewClient"); const t = useTranslations("ServerOverviewClient")
const { data, error, isLoading } = useSWRImmutable<ServerApi>( const { data, error, isLoading } = useSWRImmutable<ServerApi>("/api/server", nezhaFetcher)
"/api/server", const disableCartoon = getEnv("NEXT_PUBLIC_DisableCartoon") === "true"
nezhaFetcher,
);
const disableCartoon = getEnv("NEXT_PUBLIC_DisableCartoon") === "true";
if (error) { if (error) {
return ( return (
@ -35,7 +29,7 @@ export default function ServerOverviewClient() {
</p> </p>
<p className="text-sm font-medium opacity-40">{t("error_message")}</p> <p className="text-sm font-medium opacity-40">{t("error_message")}</p>
</div> </div>
); )
} }
return ( return (
@ -43,26 +37,20 @@ export default function ServerOverviewClient() {
<section className="grid grid-cols-2 gap-4 lg:grid-cols-4"> <section className="grid grid-cols-2 gap-4 lg:grid-cols-4">
<Card <Card
onClick={() => { onClick={() => {
setFilter(false); setFilter(false)
setStatus("all"); setStatus("all")
}} }}
className={cn( className={cn("cursor-pointer hover:border-blue-500 transition-all group")}
"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="text-sm font-medium md:text-base"> <p className="text-sm font-medium md:text-base">{t("p_816-881_Totalservers")}</p>
{t("p_816-881_Totalservers")}
</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="relative flex h-2 w-2"> <span className="relative flex h-2 w-2">
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span> <span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
</span> </span>
{data?.result ? ( {data?.result ? (
<div className="text-lg font-semibold"> <div className="text-lg font-semibold">{data?.result.length}</div>
{data?.result.length}
</div>
) : ( ) : (
<div className="flex h-7 items-center"> <div className="flex h-7 items-center">
<Loader visible={true} /> <Loader visible={true} />
@ -74,8 +62,8 @@ export default function ServerOverviewClient() {
</Card> </Card>
<Card <Card
onClick={() => { onClick={() => {
setFilter(false); setFilter(false)
setStatus("online"); setStatus("online")
}} }}
className={cn( className={cn(
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all", "cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all",
@ -86,18 +74,14 @@ export default function ServerOverviewClient() {
> >
<CardContent className="flex h-full items-center px-6 py-3"> <CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base"> <p className="text-sm font-medium md:text-base">{t("p_1610-1676_Onlineservers")}</p>
{t("p_1610-1676_Onlineservers")}
</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="relative flex h-2 w-2"> <span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span> <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span> <span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
</span> </span>
{data?.result ? ( {data?.result ? (
<div className="text-lg font-semibold"> <div className="text-lg font-semibold">{data?.live_servers}</div>
{data?.live_servers}
</div>
) : ( ) : (
<div className="flex h-7 items-center"> <div className="flex h-7 items-center">
<Loader visible={true} /> <Loader visible={true} />
@ -109,8 +93,8 @@ export default function ServerOverviewClient() {
</Card> </Card>
<Card <Card
onClick={() => { onClick={() => {
setFilter(false); setFilter(false)
setStatus("offline"); setStatus("offline")
}} }}
className={cn( className={cn(
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all", "cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all",
@ -121,18 +105,14 @@ export default function ServerOverviewClient() {
> >
<CardContent className="flex h-full items-center px-6 py-3"> <CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base"> <p className="text-sm font-medium md:text-base">{t("p_2532-2599_Offlineservers")}</p>
{t("p_2532-2599_Offlineservers")}
</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="relative flex h-2 w-2"> <span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span> <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span> <span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
</span> </span>
{data?.result ? ( {data?.result ? (
<div className="text-lg font-semibold"> <div className="text-lg font-semibold">{data?.offline_servers}</div>
{data?.offline_servers}
</div>
) : ( ) : (
<div className="flex h-7 items-center"> <div className="flex h-7 items-center">
<Loader visible={true} /> <Loader visible={true} />
@ -144,8 +124,8 @@ export default function ServerOverviewClient() {
</Card> </Card>
<Card <Card
onClick={() => { onClick={() => {
setStatus("all"); setStatus("all")
setFilter(true); setFilter(true)
}} }}
className={cn( className={cn(
"cursor-pointer hover:ring-purple-500 ring-1 ring-transparent transition-all group", "cursor-pointer hover:ring-purple-500 ring-1 ring-transparent transition-all group",
@ -157,9 +137,7 @@ export default function ServerOverviewClient() {
<CardContent className="flex h-full items-center relative px-6 py-3"> <CardContent className="flex h-full items-center relative px-6 py-3">
<section className="flex flex-col gap-1 w-full"> <section className="flex flex-col gap-1 w-full">
<div className="flex items-center w-full justify-between"> <div className="flex items-center w-full justify-between">
<p className="text-sm font-medium md:text-base"> <p className="text-sm font-medium md:text-base">{t("network")}</p>
{t("network")}
</p>
</div> </div>
{data?.result ? ( {data?.result ? (
<> <>
@ -206,5 +184,5 @@ export default function ServerOverviewClient() {
</div> </div>
)} )}
</> </>
); )
} }

View File

@ -1,38 +1,38 @@
"use client"; "use client"
import { 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<{
name: string; name: string
status: boolean; status: boolean
}>; }>
} }
interface TooltipContextType { interface TooltipContextType {
tooltipData: TooltipData | null; tooltipData: TooltipData | null
setTooltipData: (data: TooltipData | null) => void; setTooltipData: (data: TooltipData | null) => void
} }
const TooltipContext = createContext<TooltipContextType | undefined>(undefined); const TooltipContext = createContext<TooltipContextType | undefined>(undefined)
export function TooltipProvider({ children }: { children: ReactNode }) { export function TooltipProvider({ children }: { children: ReactNode }) {
const [tooltipData, setTooltipData] = useState<TooltipData | null>(null); const [tooltipData, setTooltipData] = useState<TooltipData | null>(null)
return ( return (
<TooltipContext.Provider value={{ tooltipData, setTooltipData }}> <TooltipContext.Provider value={{ tooltipData, setTooltipData }}>
{children} {children}
</TooltipContext.Provider> </TooltipContext.Provider>
); )
} }
export function useTooltip() { export function useTooltip() {
const context = useContext(TooltipContext); const context = useContext(TooltipContext)
if (context === undefined) { if (context === undefined) {
throw new Error("useTooltip must be used within a TooltipProvider"); throw new Error("useTooltip must be used within a TooltipProvider")
} }
return context; return context
} }

View File

@ -1,9 +1,9 @@
import pack from "@/package.json"; import pack from "@/package.json"
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl"
export default function Footer() { export default function Footer() {
const t = useTranslations("Footer"); const t = useTranslations("Footer")
const version = pack.version; const version = pack.version
return ( return (
<footer className="mx-auto w-full max-w-5xl"> <footer className="mx-auto w-full max-w-5xl">
<section className="flex flex-col"> <section className="flex flex-col">
@ -31,5 +31,5 @@ export default function Footer() {
</section> </section>
</section> </section>
</footer> </footer>
); )
} }

View File

@ -1,30 +1,30 @@
"use client"; "use client"
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 React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react"
function Header() { function Header() {
const t = useTranslations("Header"); const t = useTranslations("Header")
const customLogo = getEnv("NEXT_PUBLIC_CustomLogo"); const customLogo = getEnv("NEXT_PUBLIC_CustomLogo")
const customTitle = getEnv("NEXT_PUBLIC_CustomTitle"); const customTitle = getEnv("NEXT_PUBLIC_CustomTitle")
const customDescription = getEnv("NEXT_PUBLIC_CustomDescription"); const customDescription = getEnv("NEXT_PUBLIC_CustomDescription")
const router = useRouter(); const router = useRouter()
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={() => { onClick={() => {
sessionStorage.removeItem("selectedTag"); sessionStorage.removeItem("selectedTag")
router.push(`/`); router.push(`/`)
}} }}
className="flex cursor-pointer items-center text-base font-medium" className="flex cursor-pointer items-center text-base font-medium"
> >
@ -45,14 +45,9 @@ function Header() {
/> />
</div> </div>
{customTitle ? customTitle : "NezhaDash"} {customTitle ? customTitle : "NezhaDash"}
<Separator <Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" />
orientation="vertical"
className="mx-2 hidden h-4 w-[1px] md:block"
/>
<p className="hidden text-sm font-medium opacity-40 md:block"> <p className="hidden text-sm font-medium opacity-40 md:block">
{customDescription {customDescription ? customDescription : t("p_1079-1199_Simpleandbeautifuldashbo")}
? customDescription
: t("p_1079-1199_Simpleandbeautifuldashbo")}
</p> </p>
</section> </section>
<section className="flex items-center gap-2"> <section className="flex items-center gap-2">
@ -63,20 +58,20 @@ function Header() {
</section> </section>
<Overview /> <Overview />
</div> </div>
); )
} }
type links = { type links = {
link: string; link: string
name: string; name: string
}; }
function Links() { function Links() {
const linksEnv = getEnv("NEXT_PUBLIC_Links"); const linksEnv = getEnv("NEXT_PUBLIC_Links")
const links: links[] | null = linksEnv ? JSON.parse(linksEnv) : null; const links: links[] | null = linksEnv ? JSON.parse(linksEnv) : null
if (!links) return null; if (!links) return null
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -91,47 +86,45 @@ function Links() {
> >
{link.name} {link.name}
</a> </a>
); )
})} })}
</div> </div>
); )
} }
// https://github.com/streamich/react-use/blob/master/src/useInterval.ts // https://github.com/streamich/react-use/blob/master/src/useInterval.ts
const useInterval = (callback: () => void, delay: number | null) => { const useInterval = (callback: () => void, delay: number | null) => {
const savedCallback = useRef<() => void>(() => {}); const savedCallback = useRef<() => void>(() => {})
useEffect(() => { useEffect(() => {
savedCallback.current = callback; savedCallback.current = callback
}); })
useEffect(() => { useEffect(() => {
if (delay !== null) { if (delay !== null) {
const interval = setInterval(() => savedCallback.current(), delay || 0); const interval = setInterval(() => savedCallback.current(), delay || 0)
return () => clearInterval(interval); return () => clearInterval(interval)
} }
return undefined; return undefined
}, [delay]); }, [delay])
}; }
function Overview() { function Overview() {
const t = useTranslations("Overview"); const t = useTranslations("Overview")
const [mouted, setMounted] = useState(false); const [mouted, setMounted] = useState(false)
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true)
}, []); }, [])
const timeOption = DateTime.TIME_SIMPLE; const timeOption = DateTime.TIME_SIMPLE
timeOption.hour12 = true; timeOption.hour12 = true
const [timeString, setTimeString] = useState( const [timeString, setTimeString] = useState(
DateTime.now().setLocale("en-US").toLocaleString(timeOption), DateTime.now().setLocale("en-US").toLocaleString(timeOption),
); )
useInterval(() => { useInterval(() => {
setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption)); setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption))
}, 1000); }, 1000)
return ( return (
<section className={"mt-10 flex flex-col md:mt-16"}> <section className={"mt-10 flex flex-col md:mt-16"}>
<p className="text-base font-semibold">{t("p_2277-2331_Overview")}</p> <p className="text-base font-semibold">{t("p_2277-2331_Overview")}</p>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<p className="text-sm font-medium opacity-50"> <p className="text-sm font-medium opacity-50">{t("p_2390-2457_wherethetimeis")}</p>
{t("p_2390-2457_wherethetimeis")}
</p>
{mouted ? ( {mouted ? (
<p className="text-sm font-medium">{timeString}</p> <p className="text-sm font-medium">{timeString}</p>
) : ( ) : (
@ -139,6 +132,6 @@ function Overview() {
)} )}
</div> </div>
</section> </section>
); )
} }
export default Header; export default Header

View File

@ -1,13 +1,13 @@
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 { auth } from "@/auth"; import { auth } from "@/auth"
import { SignIn } from "@/components/SignIn"; import { SignIn } from "@/components/SignIn"
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry"
import React from "react"; import React from "react"
type DashboardProps = { type DashboardProps = {
children: React.ReactNode; children: React.ReactNode
}; }
export default function MainLayout({ children }: DashboardProps) { export default function MainLayout({ children }: DashboardProps) {
return ( return (
<div className="flex min-h-screen w-full flex-col"> <div className="flex min-h-screen w-full flex-col">
@ -17,15 +17,15 @@ export default function MainLayout({ children }: DashboardProps) {
<Footer /> <Footer />
</main> </main>
</div> </div>
); )
} }
async function AuthProtected({ children }: DashboardProps) { async function AuthProtected({ children }: DashboardProps) {
if (getEnv("SitePassword")) { if (getEnv("SitePassword")) {
const session = await auth(); const session = await auth()
if (!session) { if (!session) {
return <SignIn />; return <SignIn />
} }
} }
return children; return children
} }

View File

@ -1,5 +1,5 @@
import ServerList from "@/components/ServerList"; import ServerList from "@/components/ServerList"
import ServerOverview from "@/components/ServerOverview"; import ServerOverview from "@/components/ServerOverview"
export default async function Home() { export default async function Home() {
return ( return (
@ -7,5 +7,5 @@ export default async function Home() {
<ServerOverview /> <ServerOverview />
<ServerList /> <ServerList />
</div> </div>
); )
} }

View File

@ -1,48 +1,36 @@
"use client"; "use client"
import { NetworkChartClient } from "@/app/(main)/ClientComponents/NetworkChart"; import { NetworkChartClient } from "@/app/(main)/ClientComponents/NetworkChart"
import ServerDetailChartClient from "@/app/(main)/ClientComponents/ServerDetailChartClient"; import ServerDetailChartClient from "@/app/(main)/ClientComponents/ServerDetailChartClient"
import ServerDetailClient from "@/app/(main)/ClientComponents/ServerDetailClient"; import ServerDetailClient from "@/app/(main)/ClientComponents/ServerDetailClient"
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"
import ServerIPInfo from "../../ClientComponents/ServerIPInfo"; import ServerIPInfo from "../../ClientComponents/ServerIPInfo"
export default function Page(props: { params: Promise<{ id: string }> }) { export default function Page(props: { params: Promise<{ id: string }> }) {
const params = use(props.params); const params = use(props.params)
const tabs = ["Detail", "Network"]; const tabs = ["Detail", "Network"]
const [currentTab, setCurrentTab] = useState(tabs[0]); const [currentTab, setCurrentTab] = useState(tabs[0])
return ( return (
<div 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={Number(params.id)} /> <ServerDetailClient server_id={Number(params.id)} />
<section className="flex items-center my-2 w-full"> <section className="flex items-center my-2 w-full">
<Separator className="flex-1" /> <Separator className="flex-1" />
<div className="flex justify-center w-full max-w-[200px]"> <div className="flex justify-center w-full max-w-[200px]">
<TabSwitch <TabSwitch tabs={tabs} currentTab={currentTab} setCurrentTab={setCurrentTab} />
tabs={tabs}
currentTab={currentTab}
setCurrentTab={setCurrentTab}
/>
</div> </div>
<Separator className="flex-1" /> <Separator className="flex-1" />
</section> </section>
<div style={{ display: currentTab === tabs[0] ? "block" : "none" }}> <div style={{ display: currentTab === tabs[0] ? "block" : "none" }}>
<ServerDetailChartClient <ServerDetailChartClient server_id={Number(params.id)} show={currentTab === tabs[0]} />
server_id={Number(params.id)}
show={currentTab === tabs[0]}
/>
</div> </div>
<div style={{ display: currentTab === tabs[1] ? "block" : "none" }}> <div style={{ display: currentTab === tabs[1] ? "block" : "none" }}>
{getEnv("NEXT_PUBLIC_ShowIpInfo") && ( {getEnv("NEXT_PUBLIC_ShowIpInfo") && <ServerIPInfo server_id={Number(params.id)} />}
<ServerIPInfo server_id={Number(params.id)} /> <NetworkChartClient server_id={Number(params.id)} show={currentTab === tabs[1]} />
)}
<NetworkChartClient
server_id={Number(params.id)}
show={currentTab === tabs[1]}
/>
</div> </div>
</div> </div>
); )
} }

View File

@ -1,3 +1,3 @@
import { handlers } from "@/auth"; import { handlers } from "@/auth"
export const { GET, POST } = handlers; export const { GET, POST } = handlers

View File

@ -1,50 +1,44 @@
import { auth } from "@/auth"; 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 { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server"
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic"
interface ResError extends Error { interface ResError extends Error {
statusCode: number; statusCode: number
message: string; message: string
} }
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
if (getEnv("SitePassword")) { if (getEnv("SitePassword")) {
const session = await auth(); const session = await auth()
if (!session) { if (!session) {
redirect("/"); redirect("/")
} }
} }
const { searchParams } = new URL(req.url); const { searchParams } = new URL(req.url)
const server_id = searchParams.get("server_id"); const server_id = searchParams.get("server_id")
if (!server_id) { if (!server_id) {
return NextResponse.json( return NextResponse.json({ error: "server_id is required" }, { status: 400 })
{ error: "server_id is required" },
{ status: 400 },
);
} }
try { try {
const serverIdNum = parseInt(server_id, 10); const serverIdNum = parseInt(server_id, 10)
if (isNaN(serverIdNum)) { if (isNaN(serverIdNum)) {
return NextResponse.json( return NextResponse.json({ error: "server_id must be a valid number" }, { status: 400 })
{ error: "server_id must be a valid number" },
{ status: 400 },
);
} }
const detailData = await GetServerDetail({ server_id: serverIdNum }); const detailData = await GetServerDetail({ server_id: serverIdNum })
return NextResponse.json(detailData, { status: 200 }); return NextResponse.json(detailData, { status: 200 })
} catch (error) { } catch (error) {
const err = error as ResError; const err = error as ResError
console.error("Error in GET handler:", err); console.error("Error in GET handler:", err)
const statusCode = err.statusCode || 500; const statusCode = err.statusCode || 500
const message = err.message || "Internal Server Error"; const message = err.message || "Internal Server Error"
return NextResponse.json({ error: message }, { status: statusCode }); return NextResponse.json({ error: message }, { status: statusCode })
} }
} }

View File

@ -1,52 +1,46 @@
import { auth } from "@/auth"; 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 { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server"
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic"
interface ResError extends Error { interface ResError extends Error {
statusCode: number; statusCode: number
message: string; message: string
} }
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
if (getEnv("SitePassword")) { if (getEnv("SitePassword")) {
const session = await auth(); const session = await auth()
if (!session) { if (!session) {
redirect("/"); redirect("/")
} }
} }
const { searchParams } = new URL(req.url); const { searchParams } = new URL(req.url)
const server_id = searchParams.get("server_id"); const server_id = searchParams.get("server_id")
if (!server_id) { if (!server_id) {
return NextResponse.json( return NextResponse.json({ error: "server_id is required" }, { status: 400 })
{ error: "server_id is required" },
{ status: 400 },
);
} }
try { try {
const serverIdNum = parseInt(server_id, 10); const serverIdNum = parseInt(server_id, 10)
if (isNaN(serverIdNum)) { if (isNaN(serverIdNum)) {
return NextResponse.json( return NextResponse.json({ error: "server_id must be a number" }, { status: 400 })
{ error: "server_id must be a number" },
{ status: 400 },
);
} }
const monitorData = await GetServerMonitor({ const monitorData = await GetServerMonitor({
server_id: serverIdNum, server_id: serverIdNum,
}); })
return NextResponse.json(monitorData, { status: 200 }); return NextResponse.json(monitorData, { status: 200 })
} catch (error) { } catch (error) {
const err = error as ResError; const err = error as ResError
console.error("Error in GET handler:", err); console.error("Error in GET handler:", err)
const statusCode = err.statusCode || 500; const statusCode = err.statusCode || 500
const message = err.message || "Internal Server Error"; const message = err.message || "Internal Server Error"
return NextResponse.json({ error: message }, { status: statusCode }); return NextResponse.json({ error: message }, { status: statusCode })
} }
} }

View File

@ -1,72 +1,66 @@
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 fs from "fs"; import fs from "fs"
import { AsnResponse, CityResponse, Reader } from "maxmind"; import { AsnResponse, CityResponse, Reader } from "maxmind"
import { redirect } from "next/navigation"; import { redirect } from "next/navigation"
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server"
import path from "path"; import path from "path"
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic"
interface ResError extends Error { interface ResError extends Error {
statusCode: number; statusCode: number
message: string; message: string
} }
export type IPInfo = { export type IPInfo = {
city: CityResponse; city: CityResponse
asn: AsnResponse; asn: AsnResponse
}; }
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
if (getEnv("SitePassword")) { if (getEnv("SitePassword")) {
const session = await auth(); const session = await auth()
if (!session) { if (!session) {
redirect("/"); redirect("/")
} }
} }
if (!getEnv("NEXT_PUBLIC_ShowIpInfo")) { if (!getEnv("NEXT_PUBLIC_ShowIpInfo")) {
return NextResponse.json( return NextResponse.json({ error: "NEXT_PUBLIC_ShowIpInfo is disable" }, { status: 400 })
{ error: "NEXT_PUBLIC_ShowIpInfo is disable" },
{ status: 400 },
);
} }
const { searchParams } = new URL(req.url); const { searchParams } = new URL(req.url)
const server_id = searchParams.get("server_id"); const server_id = searchParams.get("server_id")
if (!server_id) { if (!server_id) {
return NextResponse.json( return NextResponse.json({ error: "server_id is required" }, { status: 400 })
{ error: "server_id is required" },
{ status: 400 },
);
} }
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", "GeoLite2-City.mmdb"); const cityDbPath = path.join(process.cwd(), "lib", "GeoLite2-City.mmdb")
const asnDbPath = path.join(process.cwd(), "lib", "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)
const cityLookup = new Reader<CityResponse>(cityDbBuffer); const cityLookup = new Reader<CityResponse>(cityDbBuffer)
const asnLookup = new Reader<AsnResponse>(asnDbBuffer); const asnLookup = new Reader<AsnResponse>(asnDbBuffer)
const data: IPInfo = { const data: IPInfo = {
city: cityLookup.get(ip) as CityResponse, city: cityLookup.get(ip) as CityResponse,
asn: asnLookup.get(ip) as AsnResponse, asn: asnLookup.get(ip) as AsnResponse,
}; }
return NextResponse.json(data, { status: 200 }); return NextResponse.json(data, { status: 200 })
} catch (error) { } catch (error) {
const err = error as ResError; const err = error as ResError
console.error("Error in GET handler:", err); console.error("Error in GET handler:", err)
const statusCode = err.statusCode || 500; const statusCode = err.statusCode || 500
const message = err.message || "Internal Server Error"; const message = err.message || "Internal Server Error"
return NextResponse.json({ error: message }, { status: statusCode }); return NextResponse.json({ error: message }, { status: statusCode })
} }
} }

View File

@ -1,32 +1,32 @@
import { auth } from "@/auth"; import { auth } from "@/auth"
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry"
import { GetNezhaData } from "@/lib/serverFetch"; import { GetNezhaData } from "@/lib/serverFetch"
import { redirect } from "next/navigation"; import { redirect } from "next/navigation"
import { NextResponse } from "next/server"; import { NextResponse } from "next/server"
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic"
interface ResError extends Error { interface ResError extends Error {
statusCode: number; statusCode: number
message: string; message: string
} }
export async function GET() { export async function GET() {
if (getEnv("SitePassword")) { if (getEnv("SitePassword")) {
const session = await auth(); const session = await auth()
if (!session) { if (!session) {
redirect("/"); redirect("/")
} }
} }
try { try {
const data = await GetNezhaData(); const data = await GetNezhaData()
return NextResponse.json(data, { status: 200 }); return NextResponse.json(data, { status: 200 })
} catch (error) { } catch (error) {
const err = error as ResError; const err = error as ResError
console.error("Error in GET handler:", err); console.error("Error in GET handler:", err)
const statusCode = err.statusCode || 500; const statusCode = err.statusCode || 500
const message = err.message || "Internal Server Error"; const message = err.message || "Internal Server Error"
return NextResponse.json({ error: message }, { status: statusCode }); return NextResponse.json({ error: message }, { status: statusCode })
} }
} }

View File

@ -1,28 +1,28 @@
// @auto-i18n-check. Please do not delete the line. // @auto-i18n-check. Please do not delete the line.
import { ThemeColorManager } from "@/components/ThemeColorManager"; import { ThemeColorManager } from "@/components/ThemeColorManager"
import { MotionProvider } from "@/components/motion/motion-provider"; 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 { FilterProvider } from "@/lib/network-filter-context"
import { StatusProvider } from "@/lib/status-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 { 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 React from "react"; import React from "react"
const fontSans = FontSans({ const fontSans = FontSans({
subsets: ["latin"], subsets: ["latin"],
variable: "--font-sans", variable: "--font-sans",
}); })
const customTitle = getEnv("NEXT_PUBLIC_CustomTitle"); const customTitle = getEnv("NEXT_PUBLIC_CustomTitle")
const customDescription = getEnv("NEXT_PUBLIC_CustomDescription"); const customDescription = getEnv("NEXT_PUBLIC_CustomDescription")
const disableIndex = getEnv("NEXT_PUBLIC_DisableIndex"); const disableIndex = getEnv("NEXT_PUBLIC_DisableIndex")
export const metadata: Metadata = { export const metadata: Metadata = {
manifest: "/manifest.json", manifest: "/manifest.json",
@ -37,22 +37,18 @@ export const metadata: Metadata = {
index: disableIndex ? false : true, index: disableIndex ? false : true,
follow: disableIndex ? false : true, follow: disableIndex ? false : true,
}, },
}; }
export const viewport: Viewport = { export const viewport: Viewport = {
width: "device-width", width: "device-width",
initialScale: 1, initialScale: 1,
maximumScale: 1, maximumScale: 1,
userScalable: false, userScalable: false,
}; }
export default async function LocaleLayout({ export default async function LocaleLayout({ children }: { children: React.ReactNode }) {
children, const locale = await getLocale()
}: { const messages = await getMessages()
children: React.ReactNode;
}) {
const locale = await getLocale();
const messages = await getMessages();
return ( return (
<html lang={locale} suppressHydrationWarning> <html lang={locale} suppressHydrationWarning>
@ -67,12 +63,7 @@ export default async function LocaleLayout({
href="https://fastly.jsdelivr.net/npm/font-logos@1/assets/font-logos.css" href="https://fastly.jsdelivr.net/npm/font-logos@1/assets/font-logos.css"
/> />
</head> </head>
<body <body className={cn("min-h-screen bg-background font-sans antialiased", fontSans.variable)}>
className={cn(
"min-h-screen bg-background font-sans antialiased",
fontSans.variable,
)}
>
<MotionProvider> <MotionProvider>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
@ -92,5 +83,5 @@ export default async function LocaleLayout({
</MotionProvider> </MotionProvider>
</body> </body>
</html> </html>
); )
} }

View File

@ -1,11 +1,11 @@
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 Footer from "./(main)/footer"
import Header from "./(main)/header"; 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">
@ -13,13 +13,11 @@ export default function NotFoundPage() {
<section className="flex flex-col items-center min-h-44 justify-center gap-2"> <section className="flex flex-col items-center min-h-44 justify-center gap-2">
<p className="text-sm font-semibold">{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="text-sm font-medium opacity-40"> <p className="text-sm font-medium opacity-40">{t("h1_490-590_404NotFoundBack")}</p>
{t("h1_490-590_404NotFoundBack")}
</p>
</Link> </Link>
</section> </section>
<Footer /> <Footer />
</main> </main>
</div> </div>
); )
} }

View File

@ -1,77 +1,77 @@
export type ServerApi = { export type ServerApi = {
live_servers: number; live_servers: number
offline_servers: number; offline_servers: number
total_out_bandwidth: number; total_out_bandwidth: number
total_in_bandwidth: number; total_in_bandwidth: number
total_out_speed: number; total_out_speed: number
total_in_speed: number; total_in_speed: number
result: NezhaAPISafe[]; result: NezhaAPISafe[]
}; }
export type NezhaAPISafe = Omit<NezhaAPI, "ipv4" | "ipv6" | "valid_ip">; export type NezhaAPISafe = Omit<NezhaAPI, "ipv4" | "ipv6" | "valid_ip">
export interface NezhaAPI { export interface NezhaAPI {
id: number; id: number
name: string; name: string
tag: string; tag: string
last_active: number; last_active: number
online_status: boolean; online_status: boolean
ipv4: string; ipv4: string
ipv6: string; ipv6: string
valid_ip: string; valid_ip: string
display_index: number; display_index: number
hide_for_guest: boolean; hide_for_guest: boolean
host: NezhaAPIHost; host: NezhaAPIHost
status: NezhaAPIStatus; status: NezhaAPIStatus
} }
export interface NezhaAPIHost { export interface NezhaAPIHost {
Platform: string; Platform: string
PlatformVersion: string; PlatformVersion: string
CPU: string[]; CPU: string[]
MemTotal: number; MemTotal: number
DiskTotal: number; DiskTotal: number
SwapTotal: number; SwapTotal: number
Arch: string; Arch: string
Virtualization: string; Virtualization: string
BootTime: number; BootTime: number
CountryCode: string; CountryCode: string
Version: string; Version: string
GPU: string[]; GPU: string[]
} }
export interface NezhaAPIStatus { export interface NezhaAPIStatus {
CPU: number; CPU: number
MemUsed: number; MemUsed: number
SwapUsed: number; SwapUsed: number
DiskUsed: number; DiskUsed: number
NetInTransfer: number; NetInTransfer: number
NetOutTransfer: number; NetOutTransfer: number
NetInSpeed: number; NetInSpeed: number
NetOutSpeed: number; NetOutSpeed: number
Uptime: number; Uptime: number
Load1: number; Load1: number
Load5: number; Load5: number
Load15: number; Load15: number
TcpConnCount: number; TcpConnCount: number
UdpConnCount: number; UdpConnCount: number
ProcessCount: number; ProcessCount: number
Temperatures: number; Temperatures: number
GPU: number; GPU: number
} }
export type ServerMonitorChart = { export type ServerMonitorChart = {
[key: string]: { [key: string]: {
created_at: number; created_at: number
avg_delay: number; avg_delay: number
}[]; }[]
}; }
export interface NezhaAPIMonitor { export interface NezhaAPIMonitor {
monitor_id: number; monitor_id: number
monitor_name: string; monitor_name: string
server_id: number; server_id: number
server_name: string; server_name: string
created_at: number[]; created_at: number[]
avg_delay: number[]; avg_delay: number[]
} }

View File

@ -1,2 +1 @@
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & export type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
Partial<Pick<T, K>>;

18
auth.ts
View File

@ -1,7 +1,7 @@
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"; import getEnv from "./lib/env-entry"
export const { handlers, signIn, signOut, auth } = NextAuth({ export const { handlers, signIn, signOut, auth } = NextAuth({
secret: process.env.AUTH_SECRET ?? "this_is_nezha_dash_web_secret", secret: process.env.AUTH_SECRET ?? "this_is_nezha_dash_web_secret",
@ -12,11 +12,11 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
credentials: { password: { label: "Password", type: "password" } }, credentials: { password: { label: "Password", type: "password" } },
// authorization function // authorization function
async authorize(credentials) { async authorize(credentials) {
const { password } = credentials; const { password } = credentials
if (password === getEnv("SitePassword")) { if (password === getEnv("SitePassword")) {
return { id: "nezha-dash-auth" }; return { id: "nezha-dash-auth" }
} }
return { error: "Invalid password" }; return { error: "Invalid password" }
}, },
}), }),
], ],
@ -24,9 +24,9 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
async signIn({ user }) { async signIn({ user }) {
// @ts-ignore // @ts-ignore
if (user.error) { if (user.error) {
return false; return false
} }
return true; return true
}, },
}, },
}); })

View File

@ -1,20 +1,20 @@
import React from "react"; import React from "react"
const BlurLayers = () => { const BlurLayers = () => {
const computeLayerStyle = (index: number) => { const computeLayerStyle = (index: number) => {
const blurAmount = index * 3.7037; const blurAmount = index * 3.7037
const maskStart = index * 10; const maskStart = index * 10
let maskEnd = maskStart + 20; let maskEnd = maskStart + 20
if (maskEnd > 100) { if (maskEnd > 100) {
maskEnd = 100; maskEnd = 100
} }
return { return {
backdropFilter: `blur-sm(${blurAmount}px)`, backdropFilter: `blur-sm(${blurAmount}px)`,
WebkitBackdropFilter: `blur-sm(${blurAmount}px)`, WebkitBackdropFilter: `blur-sm(${blurAmount}px)`,
zIndex: index + 1, zIndex: index + 1,
maskImage: `linear-gradient(rgba(0, 0, 0, 0) ${maskStart}%, rgb(0, 0, 0) ${maskEnd}%)`, maskImage: `linear-gradient(rgba(0, 0, 0, 0) ${maskStart}%, rgb(0, 0, 0) ${maskEnd}%)`,
}; }
}; }
// 根据层数动态生成层 // 根据层数动态生成层
const layers = Array.from({ length: 5 }).map((_, index) => ( const layers = Array.from({ length: 5 }).map((_, index) => (
@ -23,13 +23,13 @@ const BlurLayers = () => {
className={"absolute inset-0 h-full w-full"} className={"absolute inset-0 h-full w-full"}
style={computeLayerStyle(index)} style={computeLayerStyle(index)}
/> />
)); ))
return ( return (
<div className={"fixed bottom-0 left-0 right-0 z-50 h-[140px]"}> <div className={"fixed bottom-0 left-0 right-0 z-50 h-[140px]"}>
<div className={"relative h-full"}>{layers}</div> <div className={"relative h-full"}>{layers}</div>
</div> </div>
); )
}; }
export default BlurLayers; export default BlurLayers

View File

@ -1,31 +1,31 @@
"use client"; "use client"
import { useFilter } from "@/lib/network-filter-context"; import { useFilter } from "@/lib/network-filter-context"
import { useStatus } from "@/lib/status-context"; import { useStatus } from "@/lib/status-context"
import { ServerStackIcon } from "@heroicons/react/20/solid"; import { ServerStackIcon } from "@heroicons/react/20/solid"
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation"
import { useEffect } from "react"; import { useEffect } from "react"
export default function GlobalBackButton() { export default function GlobalBackButton() {
const router = useRouter(); const router = useRouter()
const { setStatus } = useStatus(); const { setStatus } = useStatus()
const { setFilter } = useFilter(); const { setFilter } = useFilter()
useEffect(() => { useEffect(() => {
setStatus("all"); setStatus("all")
setFilter(false); setFilter(false)
sessionStorage.removeItem("selectedTag"); sessionStorage.removeItem("selectedTag")
router.prefetch(`/`); router.prefetch(`/`)
}, []); }, [])
return ( return (
<button <button
onClick={() => { onClick={() => {
router.push(`/`); 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)] " 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]" /> <ServerStackIcon className="size-[13px]" />
</button> </button>
); )
} }

View File

@ -1,11 +1,11 @@
import Image from "next/image"; import Image from "next/image"
export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) { export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return ( return (
<svg viewBox="0 0 496 512" fill="white" {...props}> <svg viewBox="0 0 496 512" fill="white" {...props}>
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" /> <path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
</svg> </svg>
); )
} }
export function BackIcon() { export function BackIcon() {
@ -30,5 +30,5 @@ export function BackIcon() {
height="20" height="20"
/> />
</> </>
); )
} }

View File

@ -1,25 +1,25 @@
"use client"; "use client"
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu"
import { localeItems } from "@/i18n-metadata"; import { localeItems } from "@/i18n-metadata"
import { setUserLocale } from "@/i18n/locale"; import { setUserLocale } from "@/i18n/locale"
import { CheckCircleIcon } 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"; import * as React from "react"
export function LanguageSwitcher() { export function LanguageSwitcher() {
const locale = useLocale(); const locale = useLocale()
const handleSelect = (e: Event, newLocale: string) => { const handleSelect = (e: Event, newLocale: string) => {
e.preventDefault(); // 阻止默认的关闭行为 e.preventDefault() // 阻止默认的关闭行为
setUserLocale(newLocale); setUserLocale(newLocale)
}; }
return ( return (
<DropdownMenu> <DropdownMenu>
@ -40,11 +40,10 @@ export function LanguageSwitcher() {
onSelect={(e) => handleSelect(e, item.code)} onSelect={(e) => handleSelect(e, item.code)}
className={locale === item.code ? "bg-muted gap-3" : ""} className={locale === item.code ? "bg-muted gap-3" : ""}
> >
{item.name}{" "} {item.name} {locale === item.code && <CheckCircleIcon className="size-4" />}
{locale === item.code && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); )
} }

View File

@ -1,35 +1,26 @@
import { 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"
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card"
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry"
import { import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class"
GetFontLogoClass, import { cn, formatBytes, formatNezhaInfo } from "@/lib/utils"
GetOsName, import { useTranslations } from "next-intl"
MageMicrosoftWindows, import Link from "next/link"
} from "@/lib/logo-class";
import { cn, formatBytes, formatNezhaInfo } from "@/lib/utils";
import { useTranslations } from "next-intl";
import Link from "next/link";
export default function ServerCard({ export default function ServerCard({ serverInfo }: { serverInfo: NezhaAPISafe }) {
serverInfo, const t = useTranslations("ServerCard")
}: {
serverInfo: NezhaAPISafe;
}) {
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)
const showFlag = getEnv("NEXT_PUBLIC_ShowFlag") === "true"; const showFlag = getEnv("NEXT_PUBLIC_ShowFlag") === "true"
const showNetTransfer = getEnv("NEXT_PUBLIC_ShowNetTransfer") === "true"; const showNetTransfer = getEnv("NEXT_PUBLIC_ShowNetTransfer") === "true"
const fixedTopServerName = const fixedTopServerName = getEnv("NEXT_PUBLIC_FixedTopServerName") === "true"
getEnv("NEXT_PUBLIC_FixedTopServerName") === "true";
const saveSession = () => { const saveSession = () => {
sessionStorage.setItem("fromMainPage", "true"); sessionStorage.setItem("fromMainPage", "true")
}; }
return online ? ( return online ? (
<Link onClick={saveSession} href={`/server/${id}`} prefetch={true}> <Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
@ -75,11 +66,7 @@ export default function ServerCard({
})} })}
> >
{fixedTopServerName && ( {fixedTopServerName && (
<div <div className={"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"}>
className={
"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"
}
>
<div className="text-xs font-semibold"> <div className="text-xs font-semibold">
{host.Platform.includes("Windows") ? ( {host.Platform.includes("Windows") ? (
<MageMicrosoftWindows className="size-[10px]" /> <MageMicrosoftWindows className="size-[10px]" />
@ -90,48 +77,36 @@ export default function ServerCard({
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("System")}</p> <p className="text-xs text-muted-foreground">{t("System")}</p>
<div className="flex items-center text-[10.5px] font-semibold"> <div className="flex items-center text-[10.5px] font-semibold">
{host.Platform.includes("Windows") {host.Platform.includes("Windows") ? "Windows" : GetOsName(host.Platform)}
? "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-xs text-muted-foreground">{t("CPU")}</p> <p className="text-xs text-muted-foreground">{t("CPU")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
{cpu.toFixed(2)}%
</div>
<ServerUsageBar value={cpu} /> <ServerUsageBar value={cpu} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("Mem")}</p> <p className="text-xs text-muted-foreground">{t("Mem")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div>
{mem.toFixed(2)}%
</div>
<ServerUsageBar value={mem} /> <ServerUsageBar value={mem} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("STG")}</p> <p className="text-xs text-muted-foreground">{t("STG")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
{stg.toFixed(2)}%
</div>
<ServerUsageBar value={stg} /> <ServerUsageBar value={stg} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("Upload")}</p> <p className="text-xs text-muted-foreground">{t("Upload")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{up >= 1024 {up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`}
? `${(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-xs text-muted-foreground">{t("Download")}</p> <p className="text-xs text-muted-foreground">{t("Download")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{down >= 1024 {down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`}
? `${(down / 1024).toFixed(2)}G/s`
: `${down.toFixed(2)}M/s`}
</div> </div>
</div> </div>
</section> </section>
@ -158,9 +133,7 @@ export default function ServerCard({
<Card <Card
className={cn( className={cn(
"flex flex-col items-center justify-start gap-3 p-3 md:px-5", "flex flex-col items-center justify-start gap-3 p-3 md:px-5",
showNetTransfer showNetTransfer ? "lg:min-h-[91px] min-h-[123px]" : "lg:min-h-[61px] min-h-[93px]",
? "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,
@ -175,24 +148,16 @@ export default function ServerCard({
> >
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span> <span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<div <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"> <div className="relative">
<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>
); )
} }

View File

@ -1,33 +1,25 @@
import { 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"
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry"
import { import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class"
GetFontLogoClass, import { cn, formatBytes, formatNezhaInfo } from "@/lib/utils"
GetOsName, import { useTranslations } from "next-intl"
MageMicrosoftWindows, import Link from "next/link"
} from "@/lib/logo-class";
import { cn, formatBytes, formatNezhaInfo } from "@/lib/utils";
import { useTranslations } from "next-intl";
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, const t = useTranslations("ServerCard")
}: {
serverInfo: NezhaAPISafe;
}) {
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)
const showFlag = getEnv("NEXT_PUBLIC_ShowFlag") === "true"; const showFlag = getEnv("NEXT_PUBLIC_ShowFlag") === "true"
const saveSession = () => { const saveSession = () => {
sessionStorage.setItem("fromMainPage", "true"); sessionStorage.setItem("fromMainPage", "true")
}; }
return online ? ( return online ? (
<Link onClick={saveSession} href={`/server/${id}`} prefetch={true}> <Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
@ -63,9 +55,7 @@ export default function ServerCardInline({
<Separator orientation="vertical" className="h-8 mx-0 ml-2" /> <Separator orientation="vertical" className="h-8 mx-0 ml-2" />
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}> <section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
<div <div className={"items-center flex flex-row gap-2 whitespace-nowrap"}>
className={"items-center flex flex-row gap-2 whitespace-nowrap"}
>
<div className="text-xs font-semibold"> <div className="text-xs font-semibold">
{host.Platform.includes("Windows") ? ( {host.Platform.includes("Windows") ? (
<MageMicrosoftWindows className="size-[10px]" /> <MageMicrosoftWindows className="size-[10px]" />
@ -76,9 +66,7 @@ export default function ServerCardInline({
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("System")}</p> <p className="text-xs text-muted-foreground">{t("System")}</p>
<div className="flex items-center text-[10.5px] font-semibold"> <div className="flex items-center text-[10.5px] font-semibold">
{host.Platform.includes("Windows") {host.Platform.includes("Windows") ? "Windows" : GetOsName(host.Platform)}
? "Windows"
: GetOsName(host.Platform)}
</div> </div>
</div> </div>
</div> </div>
@ -90,53 +78,39 @@ export default function ServerCardInline({
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("CPU")}</p> <p className="text-xs text-muted-foreground">{t("CPU")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
{cpu.toFixed(2)}%
</div>
<ServerUsageBar value={cpu} /> <ServerUsageBar value={cpu} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("Mem")}</p> <p className="text-xs text-muted-foreground">{t("Mem")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div>
{mem.toFixed(2)}%
</div>
<ServerUsageBar value={mem} /> <ServerUsageBar value={mem} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("STG")}</p> <p className="text-xs text-muted-foreground">{t("STG")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
{stg.toFixed(2)}%
</div>
<ServerUsageBar value={stg} /> <ServerUsageBar value={stg} />
</div> </div>
<div className={"flex w-16 flex-col"}> <div className={"flex w-16 flex-col"}>
<p className="text-xs text-muted-foreground">{t("Upload")}</p> <p className="text-xs text-muted-foreground">{t("Upload")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{up >= 1024 {up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`}
? `${(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-xs text-muted-foreground">{t("Download")}</p> <p className="text-xs text-muted-foreground">{t("Download")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{down >= 1024 {down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`}
? `${(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-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">{t("TotalUpload")}</p>
{t("TotalUpload")}
</p>
<div className="flex items-center text-xs font-semibold"> <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-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">{t("TotalDownload")}</p>
{t("TotalDownload")}
</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center text-xs font-semibold">
{formatBytes(serverInfo.status.NetInTransfer)} {formatBytes(serverInfo.status.NetInTransfer)}
</div> </div>
@ -157,24 +131,16 @@ export default function ServerCardInline({
> >
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span> <span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<div <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>
); )
} }

View File

@ -1,6 +1,6 @@
import { NezhaAPISafe } from "@/app/types/nezha-api"; import { NezhaAPISafe } from "@/app/types/nezha-api"
import { cn, formatBytes } from "@/lib/utils"; import { cn, formatBytes } from "@/lib/utils"
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl"
export function ServerCardPopoverCard({ export function ServerCardPopoverCard({
className, className,
@ -8,31 +8,27 @@ export function ServerCardPopoverCard({
content, content,
children, children,
}: { }: {
className?: string; className?: string
title: string; title: string
content?: string; content?: string
children?: React.ReactNode; children?: React.ReactNode
}) { }) {
return ( return (
<div className={cn("mb-[6px] flex w-full flex-col", className)}> <div className={cn("mb-[6px] flex w-full flex-col", className)}>
<div className="text-sm font-semibold">{title}</div> <div className="text-sm font-semibold">{title}</div>
{children ? ( {children ? children : <div className="break-all text-xs font-medium">{content}</div>}
children
) : (
<div className="break-all text-xs font-medium">{content}</div>
)}
</div> </div>
); )
} }
export default function ServerCardPopover({ export default function ServerCardPopover({
host, host,
status, status,
}: { }: {
host: NezhaAPISafe["host"]; host: NezhaAPISafe["host"]
status: NezhaAPISafe["status"]; status: NezhaAPISafe["status"]
}) { }) {
const t = useTranslations("ServerCardPopover"); const t = useTranslations("ServerCardPopover")
return ( return (
<section className="max-w-[300px]"> <section className="max-w-[300px]">
<ServerCardPopoverCard <ServerCardPopoverCard
@ -69,5 +65,5 @@ export default function ServerCardPopover({
content={`${(status.Uptime / 86400).toFixed(0)} Days`} content={`${(status.Uptime / 86400).toFixed(0)} Days`}
/> />
</section> </section>
); )
} }

View File

@ -1,47 +1,47 @@
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import getUnicodeFlagIcon from "country-flag-icons/unicode"; import getUnicodeFlagIcon from "country-flag-icons/unicode"
import { useEffect, useState } from "react"; import { useEffect, useState } from "react"
export default function ServerFlag({ export default function ServerFlag({
country_code, country_code,
className, className,
}: { }: {
country_code: string; country_code: string
className?: string; className?: string
}) { }) {
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false); const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false)
const useSvgFlag = getEnv("NEXT_PUBLIC_ForceUseSvgFlag") === "true"; const useSvgFlag = getEnv("NEXT_PUBLIC_ForceUseSvgFlag") === "true"
useEffect(() => { useEffect(() => {
if (useSvgFlag) { if (useSvgFlag) {
// 如果环境变量要求直接使用 SVG则无需检查 Emoji 支持 // 如果环境变量要求直接使用 SVG则无需检查 Emoji 支持
setSupportsEmojiFlags(false); setSupportsEmojiFlags(false)
return; return
} }
const checkEmojiSupport = () => { const checkEmojiSupport = () => {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d")
const emojiFlag = "🇺🇸"; // 使用美国国旗作为测试 const emojiFlag = "🇺🇸" // 使用美国国旗作为测试
if (!ctx) return; if (!ctx) return
ctx.fillStyle = "#000"; ctx.fillStyle = "#000"
ctx.textBaseline = "top"; ctx.textBaseline = "top"
ctx.font = "32px Arial"; ctx.font = "32px Arial"
ctx.fillText(emojiFlag, 0, 0); ctx.fillText(emojiFlag, 0, 0)
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0; const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0
setSupportsEmojiFlags(support); setSupportsEmojiFlags(support)
}; }
checkEmojiSupport(); checkEmojiSupport()
}, [useSvgFlag]); // 将 `useSvgFlag` 作为依赖,当其变化时重新触发 }, [useSvgFlag]) // 将 `useSvgFlag` 作为依赖,当其变化时重新触发
if (!country_code) return null; if (!country_code) return null
if (supportsEmojiFlags && country_code.toLowerCase() === "tw") { if (supportsEmojiFlags && country_code.toLowerCase() === "tw") {
country_code = "cn"; country_code = "cn"
} }
return ( return (
@ -52,5 +52,5 @@ export default function ServerFlag({
getUnicodeFlagIcon(country_code) getUnicodeFlagIcon(country_code)
)} )}
</span> </span>
); )
} }

View File

@ -1,5 +1,5 @@
import ServerListClient from "@/app/(main)/ClientComponents/ServerListClient"; import ServerListClient from "@/app/(main)/ClientComponents/ServerListClient"
export default async function ServerList() { export default async function ServerList() {
return <ServerListClient />; return <ServerListClient />
} }

View File

@ -1,5 +1,5 @@
import ServerOverviewClient from "@/app/(main)/ClientComponents/ServerOverviewClient"; import ServerOverviewClient from "@/app/(main)/ClientComponents/ServerOverviewClient"
export default async function ServerOverview() { export default async function ServerOverview() {
return <ServerOverviewClient />; return <ServerOverviewClient />
} }

View File

@ -1,9 +1,9 @@
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress"
import React from "react"; import React from "react"
type ServerUsageBarProps = { type ServerUsageBarProps = {
value: number; value: number
}; }
export default function ServerUsageBar({ value }: ServerUsageBarProps) { export default function ServerUsageBar({ value }: ServerUsageBarProps) {
return ( return (
@ -11,14 +11,8 @@ export default function ServerUsageBar({ value }: ServerUsageBarProps) {
aria-label={"Server Usage Bar"} aria-label={"Server Usage Bar"}
aria-labelledby={"Server Usage Bar"} aria-labelledby={"Server Usage Bar"}
value={value} value={value}
indicatorClassName={ indicatorClassName={value > 90 ? "bg-red-500" : value > 70 ? "bg-orange-400" : "bg-green-500"}
value > 90
? "bg-red-500"
: value > 70
? "bg-orange-400"
: "bg-green-500"
}
className={"h-[3px] rounded-sm"} className={"h-[3px] rounded-sm"}
/> />
); )
} }

View File

@ -1,69 +1,60 @@
"use client"; "use client"
import { getCsrfToken, signIn } from "next-auth/react"; import { getCsrfToken, signIn } from "next-auth/react"
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl"
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"; import { useEffect, useState } from "react"
import { Loader } from "./loading/Loader"; import { Loader } from "./loading/Loader"
export function SignIn() { export function SignIn() {
const t = useTranslations("SignIn"); const t = useTranslations("SignIn")
const [csrfToken, setCsrfToken] = useState(""); const [csrfToken, setCsrfToken] = useState("")
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false)
const [errorState, setErrorState] = useState(false); const [errorState, setErrorState] = useState(false)
const [successState, setSuccessState] = useState(false); const [successState, setSuccessState] = useState(false)
const router = useRouter(); const router = useRouter()
useEffect(() => { useEffect(() => {
async function loadProviders() { async function loadProviders() {
const csrf = await getCsrfToken(); const csrf = await getCsrfToken()
setCsrfToken(csrf); setCsrfToken(csrf)
} }
loadProviders(); loadProviders()
}, []); }, [])
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault()
setLoading(true); setLoading(true)
const formData = new FormData(e.currentTarget); const formData = new FormData(e.currentTarget)
const password = formData.get("password") as string; const password = formData.get("password") as string
const res = await signIn("credentials", { const res = await signIn("credentials", {
password: password, password: password,
redirect: false, redirect: false,
}); })
if (res?.error) { if (res?.error) {
console.log("login error"); console.log("login error")
setErrorState(true); setErrorState(true)
setSuccessState(false); setSuccessState(false)
} else { } else {
console.log("login success"); console.log("login success")
setErrorState(false); setErrorState(false)
setSuccessState(true); setSuccessState(true)
router.push("/"); router.push("/")
router.refresh(); router.refresh()
} }
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-col items-center justify-start 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 && ( {errorState && <p className="text-red-500 text-sm font-semibold">{t("ErrorMessage")}</p>}
<p className="text-red-500 text-sm font-semibold">
{t("ErrorMessage")}
</p>
)}
{successState && ( {successState && (
<p className="text-green-500 text-sm font-semibold"> <p className="text-green-500 text-sm font-semibold">{t("SuccessMessage")}</p>
{t("SuccessMessage")}
</p>
)} )}
<p className="text-base font-semibold">{t("SignInMessage")}</p> <p className="text-base font-semibold">{t("SignInMessage")}</p>
<input <input
@ -81,5 +72,5 @@ export function SignIn() {
</button> </button>
</section> </section>
</form> </form>
); )
} }

View File

@ -1,10 +1,10 @@
"use client"; "use client"
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import { m } from "framer-motion"; import { m } from "framer-motion"
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl"
import React, { createRef, useEffect, useRef } from "react"; import React, { createRef, useEffect, useRef } from "react"
export default function Switch({ export default function Switch({
allTag, allTag,
@ -12,51 +12,51 @@ export default function Switch({
tagCountMap, tagCountMap,
onTagChange, onTagChange,
}: { }: {
allTag: string[]; allTag: string[]
nowTag: string; nowTag: string
tagCountMap: Record<string, number>; tagCountMap: Record<string, number>
onTagChange: (tag: string) => void; onTagChange: (tag: string) => void
}) { }) {
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")
useEffect(() => { useEffect(() => {
const savedTag = sessionStorage.getItem("selectedTag"); const savedTag = sessionStorage.getItem("selectedTag")
if (savedTag && allTag.includes(savedTag)) { if (savedTag && allTag.includes(savedTag)) {
onTagChange(savedTag); onTagChange(savedTag)
} }
}, [allTag, onTagChange]); }, [allTag, onTagChange])
useEffect(() => { useEffect(() => {
const container = scrollRef.current; const container = scrollRef.current
if (!container) return; if (!container) return
const isOverflowing = container.scrollWidth > container.clientWidth; const isOverflowing = container.scrollWidth > container.clientWidth
if (!isOverflowing) return; if (!isOverflowing) return
const onWheel = (e: WheelEvent) => { const onWheel = (e: WheelEvent) => {
e.preventDefault(); e.preventDefault()
container.scrollLeft += e.deltaY; container.scrollLeft += e.deltaY
}; }
container.addEventListener("wheel", onWheel, { passive: false }); container.addEventListener("wheel", onWheel, { passive: false })
return () => { return () => {
container.removeEventListener("wheel", onWheel); container.removeEventListener("wheel", onWheel)
}; }
}, []); }, [])
useEffect(() => { useEffect(() => {
const currentTagRef = tagRefs.current[allTag.indexOf(nowTag)]; const currentTagRef = tagRefs.current[allTag.indexOf(nowTag)]
if (currentTagRef && currentTagRef.current) { if (currentTagRef && currentTagRef.current) {
currentTagRef.current.scrollIntoView({ currentTagRef.current.scrollIntoView({
behavior: "smooth", behavior: "smooth",
block: "nearest", block: "nearest",
inline: "center", inline: "center",
}); })
} }
}, [nowTag]); }, [nowTag])
return ( return (
<div <div
@ -71,9 +71,7 @@ export default function Switch({
onClick={() => onTagChange(tag)} onClick={() => onTagChange(tag)}
className={cn( className={cn(
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500", "relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500",
nowTag === tag nowTag === tag ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500",
? "text-black dark:text-white"
: "text-stone-400 dark:text-stone-500",
)} )}
> >
{nowTag === tag && ( {nowTag === tag && (
@ -89,17 +87,14 @@ export default function Switch({
<div className="relative z-20 flex items-center gap-1"> <div className="relative z-20 flex items-center gap-1">
<div className="whitespace-nowrap flex items-center gap-2"> <div className="whitespace-nowrap flex items-center gap-2">
{tag === "defaultTag" ? t("defaultTag") : tag}{" "} {tag === "defaultTag" ? t("defaultTag") : tag}{" "}
{getEnv("NEXT_PUBLIC_ShowTagCount") === "true" && {getEnv("NEXT_PUBLIC_ShowTagCount") === "true" && tag !== "defaultTag" && (
tag !== "defaultTag" && ( <div className="w-fit px-1.5 rounded-full bg-muted">{tagCountMap[tag]}</div>
<div className="w-fit px-1.5 rounded-full bg-muted"> )}
{tagCountMap[tag]}
</div>
)}
</div> </div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
); )
} }

View File

@ -1,20 +1,20 @@
"use client"; "use client"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import { m } from "framer-motion"; import { m } from "framer-motion"
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl"
import React from "react"; import React from "react"
export default function TabSwitch({ export default function TabSwitch({
tabs, tabs,
currentTab, currentTab,
setCurrentTab, setCurrentTab,
}: { }: {
tabs: string[]; tabs: string[]
currentTab: string; currentTab: string
setCurrentTab: (tab: string) => void; setCurrentTab: (tab: string) => void
}) { }) {
const t = useTranslations("TabSwitch"); const t = useTranslations("TabSwitch")
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="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">
@ -46,5 +46,5 @@ export default function TabSwitch({
))} ))}
</div> </div>
</div> </div>
); )
} }

View File

@ -1,41 +1,39 @@
"use client"; "use client"
import { useTheme } from "next-themes"; import { useTheme } from "next-themes"
import { useEffect } from "react"; import { useEffect } from "react"
export function ThemeColorManager() { export function ThemeColorManager() {
const { theme, systemTheme } = useTheme(); const { theme, systemTheme } = useTheme()
useEffect(() => { useEffect(() => {
const updateThemeColor = () => { const updateThemeColor = () => {
const currentTheme = theme === "system" ? systemTheme : theme; const currentTheme = theme === "system" ? systemTheme : theme
const meta = document.querySelector('meta[name="theme-color"]'); const meta = document.querySelector('meta[name="theme-color"]')
if (!meta) { if (!meta) {
const newMeta = document.createElement("meta"); const newMeta = document.createElement("meta")
newMeta.name = "theme-color"; newMeta.name = "theme-color"
document.head.appendChild(newMeta); document.head.appendChild(newMeta)
} }
const themeColor = const themeColor =
currentTheme === "dark" currentTheme === "dark"
? "hsl(30 15% 8%)" // 深色模式背景色 ? "hsl(30 15% 8%)" // 深色模式背景色
: "hsl(0 0% 98%)"; // 浅色模式背景色 : "hsl(0 0% 98%)" // 浅色模式背景色
document document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
.querySelector('meta[name="theme-color"]') }
?.setAttribute("content", themeColor);
};
// Update on mount and theme change // Update on mount and theme change
updateThemeColor(); updateThemeColor()
// Listen for system theme changes // Listen for system theme changes
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
mediaQuery.addEventListener("change", updateThemeColor); mediaQuery.addEventListener("change", updateThemeColor)
return () => mediaQuery.removeEventListener("change", updateThemeColor); return () => mediaQuery.removeEventListener("change", updateThemeColor)
}, [theme, systemTheme]); }, [theme, systemTheme])
return null; return null
} }

View File

@ -1,26 +1,26 @@
"use client"; "use client"
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import { CheckCircleIcon } from "@heroicons/react/20/solid"; import { CheckCircleIcon } from "@heroicons/react/20/solid"
import { Moon, Sun } from "lucide-react"; 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"
export function ModeToggle() { export function ModeToggle() {
const { setTheme, theme } = useTheme(); const { setTheme, theme } = useTheme()
const t = useTranslations("ThemeSwitcher"); const t = useTranslations("ThemeSwitcher")
const handleSelect = (e: Event, newTheme: string) => { const handleSelect = (e: Event, newTheme: string) => {
e.preventDefault(); e.preventDefault()
setTheme(newTheme); setTheme(newTheme)
}; }
return ( return (
<DropdownMenu> <DropdownMenu>
@ -40,24 +40,21 @@ export function ModeToggle() {
className={cn({ "gap-3 bg-muted": theme === "light" })} className={cn({ "gap-3 bg-muted": theme === "light" })}
onSelect={(e) => handleSelect(e, "light")} onSelect={(e) => handleSelect(e, "light")}
> >
{t("Light")}{" "} {t("Light")} {theme === "light" && <CheckCircleIcon className="size-4" />}
{theme === "light" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
className={cn({ "gap-3 bg-muted": theme === "dark" })} className={cn({ "gap-3 bg-muted": theme === "dark" })}
onSelect={(e) => handleSelect(e, "dark")} onSelect={(e) => handleSelect(e, "dark")}
> >
{t("Dark")}{" "} {t("Dark")} {theme === "dark" && <CheckCircleIcon className="size-4" />}
{theme === "dark" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
className={cn({ "gap-3 bg-muted": theme === "system" })} className={cn({ "gap-3 bg-muted": theme === "system" })}
onSelect={(e) => handleSelect(e, "system")} onSelect={(e) => handleSelect(e, "system")}
> >
{t("System")}{" "} {t("System")} {theme === "system" && <CheckCircleIcon className="size-4" />}
{theme === "system" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); )
} }

View File

@ -1,4 +1,4 @@
const bars = Array(8).fill(0); const bars = Array(8).fill(0)
export const Loader = ({ visible }: { visible: boolean }) => { export const Loader = ({ visible }: { visible: boolean }) => {
return ( return (
@ -9,5 +9,5 @@ export const Loader = ({ visible }: { visible: boolean }) => {
))} ))}
</div> </div>
</div> </div>
); )
}; }

View File

@ -1 +1 @@
export { domMax as default } from "framer-motion"; export { domMax as default } from "framer-motion"

View File

@ -1,14 +1,13 @@
"use client"; "use client"
import { LazyMotion } from "framer-motion"; import { LazyMotion } from "framer-motion"
const loadFeatures = () => const loadFeatures = () => import("./framer-lazy-feature").then((res) => res.default)
import("./framer-lazy-feature").then((res) => res.default);
export const MotionProvider = ({ children }: { children: React.ReactNode }) => { export const MotionProvider = ({ children }: { children: React.ReactNode }) => {
return ( return (
<LazyMotion features={loadFeatures} strict key="framer"> <LazyMotion features={loadFeatures} strict key="framer">
{children} {children}
</LazyMotion> </LazyMotion>
); )
}; }

View File

@ -1,11 +1,11 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
interface Props { interface Props {
max: number; max: number
value: number; value: number
min: number; min: number
className?: string; className?: string
primaryColor?: string; primaryColor?: string
} }
export default function AnimatedCircularProgressBar({ export default function AnimatedCircularProgressBar({
@ -15,9 +15,9 @@ export default function AnimatedCircularProgressBar({
primaryColor, primaryColor,
className, className,
}: Props) { }: Props) {
const circumference = 2 * Math.PI * 45; const circumference = 2 * Math.PI * 45
const percentPx = circumference / 100; const percentPx = circumference / 100
const currentPercent = ((value - min) / (max - min)) * 100; const currentPercent = ((value - min) / (max - min)) * 100
return ( return (
<div <div
@ -37,12 +37,7 @@ export default function AnimatedCircularProgressBar({
} as React.CSSProperties } as React.CSSProperties
} }
> >
<svg <svg fill="none" className="size-full" strokeWidth="2" viewBox="0 0 100 100">
fill="none"
className="size-full"
strokeWidth="2"
viewBox="0 0 100 100"
>
{currentPercent <= 90 && currentPercent >= 0 && ( {currentPercent <= 90 && currentPercent >= 0 && (
<circle <circle
cx="50" cx="50"
@ -62,8 +57,7 @@ export default function AnimatedCircularProgressBar({
transform: transform:
"rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)", "rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
transition: "all var(--transition-length) ease var(--delay)", transition: "all var(--transition-length) ease var(--delay)",
transformOrigin: transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties } as React.CSSProperties
} }
/> />
@ -90,8 +84,7 @@ export default function AnimatedCircularProgressBar({
transitionProperty: "stroke-dasharray,transform", transitionProperty: "stroke-dasharray,transform",
transform: transform:
"rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))", "rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))",
transformOrigin: transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties } as React.CSSProperties
} }
/> />
@ -103,5 +96,5 @@ export default function AnimatedCircularProgressBar({
{currentPercent} {currentPercent}
</span> </span>
</div> </div>
); )
} }

View File

@ -1,16 +1,16 @@
import Image from "next/image"; import Image from "next/image"
import Link from "next/link"; import Link from "next/link"
import React from "react"; import React from "react"
export const AnimatedTooltip = ({ export const AnimatedTooltip = ({
items, items,
}: { }: {
items: { items: {
id: number; id: number
name: string; name: string
designation: string; designation: string
image: string; image: string
}[]; }[]
}) => { }) => {
return ( return (
<> <>
@ -29,5 +29,5 @@ export const AnimatedTooltip = ({
</div> </div>
))} ))}
</> </>
); )
}; }

View File

@ -1,14 +1,13 @@
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 * 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",
{ {
variants: { variants: {
variant: { variant: {
default: default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: destructive:
@ -20,16 +19,14 @@ const badgeVariants = cva(
variant: "default", variant: "default",
}, },
}, },
); )
export interface BadgeProps export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {} VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return ( return <div className={cn(badgeVariants({ variant }), className)} {...props} />
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
} }
export { Badge, badgeVariants }; export { Badge, badgeVariants }

View File

@ -1,7 +1,7 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot"; import { Slot } from "@radix-ui/react-slot"
import { type VariantProps, cva } from "class-variance-authority"; import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"; import * as React from "react"
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50", "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
@ -9,12 +9,9 @@ const buttonVariants = cva(
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
"bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
outline: secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
@ -30,26 +27,22 @@ const buttonVariants = cva(
size: "default", size: "default",
}, },
}, },
); )
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean; asChild?: boolean
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"; const Comp = asChild ? Slot : "button"
return ( return (
<Comp <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
className={cn(buttonVariants({ variant, size, className }))} )
ref={ref}
{...props}
/>
);
}, },
); )
Button.displayName = "Button"; Button.displayName = "Button"
export { Button, buttonVariants }; export { Button, buttonVariants }

View File

@ -1,85 +1,58 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import * as React from "react"; import * as React from "react"
const Card = React.forwardRef< const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div
>(({ className, ...props }, ref) => ( ref={ref}
<div className={cn(
ref={ref} "rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
className={cn( className,
"rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none", )}
className, {...props}
)} />
{...props} ),
/> )
)); Card.displayName = "Card"
Card.displayName = "Card";
const CardHeader = React.forwardRef< const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<div )
ref={ref} CardHeader.displayName = "CardHeader"
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef< const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLHeadingElement> <h3
>(({ className, ...props }, ref) => ( ref={ref}
<h3 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
ref={ref} {...props}
className={cn( />
"text-2xl font-semibold leading-none tracking-tight", ),
className, )
)} CardTitle.displayName = "CardTitle"
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef< const CardDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<p <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
ref={ref} ))
className={cn("text-sm text-muted-foreground", className)} CardDescription.displayName = "CardDescription"
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef< const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> )
)); CardContent.displayName = "CardContent"
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef< const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<div )
ref={ref} CardFooter.displayName = "CardFooter"
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export { export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View File

@ -1,49 +1,47 @@
"use client"; "use client"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import * as React from "react"; import * as React from "react"
import * as RechartsPrimitive from "recharts"; import * as RechartsPrimitive from "recharts"
// Format: { THEME_NAME: CSS_SELECTOR } // Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const; const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = { export type ChartConfig = {
[k: string]: { [k: string]: {
label?: React.ReactNode; label?: React.ReactNode
icon?: React.ComponentType; icon?: React.ComponentType
} & ( } & (
| { color?: string; theme?: never } | { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> } | { color?: never; theme: Record<keyof typeof THEMES, string> }
); )
}; }
type ChartContextProps = { type ChartContextProps = {
config: ChartConfig; config: ChartConfig
}; }
const ChartContext = React.createContext<ChartContextProps | null>(null); const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() { function useChart() {
const context = React.useContext(ChartContext); const context = React.useContext(ChartContext)
if (!context) { if (!context) {
throw new Error("useChart must be used within a <ChartContainer />"); throw new Error("useChart must be used within a <ChartContainer />")
} }
return context; return context
} }
const ChartContainer = React.forwardRef< const ChartContainer = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
config: ChartConfig; config: ChartConfig
children: React.ComponentProps< children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"]
typeof RechartsPrimitive.ResponsiveContainer
>["children"];
} }
>(({ id, className, children, config, ...props }, ref) => { >(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId(); const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return ( return (
<ChartContext.Provider value={{ config }}> <ChartContext.Provider value={{ config }}>
@ -57,22 +55,18 @@ const ChartContainer = React.forwardRef<
{...props} {...props}
> >
<ChartStyle id={chartId} config={config} /> <ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer> <RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div> </div>
</ChartContext.Provider> </ChartContext.Provider>
); )
}); })
ChartContainer.displayName = "Chart"; ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter( const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color)
([, config]) => config.theme || config.color,
);
if (!colorConfig.length) { if (!colorConfig.length) {
return null; return null
} }
return ( return (
@ -84,10 +78,8 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
${prefix} [data-chart=${id}] { ${prefix} [data-chart=${id}] {
${colorConfig ${colorConfig
.map(([key, itemConfig]) => { .map(([key, itemConfig]) => {
const color = const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || return color ? ` --color-${key}: ${color};` : null
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
}) })
.join("\n")} .join("\n")}
} }
@ -96,20 +88,20 @@ ${colorConfig
.join("\n"), .join("\n"),
}} }}
/> />
); )
}; }
const ChartTooltip = RechartsPrimitive.Tooltip; const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef< const ChartTooltipContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> & React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
hideLabel?: boolean; hideLabel?: boolean
hideIndicator?: boolean; hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"; indicator?: "line" | "dot" | "dashed"
nameKey?: string; nameKey?: string
labelKey?: string; labelKey?: string
} }
>( >(
( (
@ -130,53 +122,43 @@ const ChartTooltipContent = React.forwardRef<
}, },
ref, ref,
) => { ) => {
const { config } = useChart(); const { config } = useChart()
const tooltipLabel = React.useMemo(() => { const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) { if (hideLabel || !payload?.length) {
return null; return null
} }
const [item] = payload; const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`; const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key); const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value = const value =
!labelKey && typeof label === "string" !labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label ? config[label as keyof typeof config]?.label || label
: itemConfig?.label; : itemConfig?.label
if (labelFormatter) { if (labelFormatter) {
return ( return (
<div className={cn("font-medium", labelClassName)}> <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
{labelFormatter(value, payload)} )
</div>
);
} }
if (!value) { if (!value) {
return null; return null
} }
return <div className={cn("font-medium", labelClassName)}>{value}</div>; return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [ }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey])
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
if (!active || !payload?.length) { if (!active || !payload?.length) {
return null; return null
} }
payload.sort((a, b) => { payload.sort((a, b) => {
return Number(b.value) - Number(a.value); return Number(b.value) - Number(a.value)
}); })
const nestLabel = payload.length === 1 && indicator !== "dot"; const nestLabel = payload.length === 1 && indicator !== "dot"
return ( return (
<div <div
@ -189,9 +171,9 @@ const ChartTooltipContent = React.forwardRef<
{!nestLabel ? tooltipLabel : null} {!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5"> <div className="grid gap-1.5">
{payload.map((item, index) => { {payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`; const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key); const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color; const indicatorColor = color || item.payload.fill || item.color
return ( return (
<div <div
@ -257,112 +239,94 @@ const ChartTooltipContent = React.forwardRef<
</> </>
)} )}
</div> </div>
); )
})} })}
</div> </div>
</div> </div>
); )
}, },
); )
ChartTooltipContent.displayName = "ChartTooltip"; ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend; const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef< const ChartLegendContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean; hideIcon?: boolean
nameKey?: string; nameKey?: string
} }
>( >(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
( const { config } = useChart()
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref,
) => {
const { config } = useChart();
if (!payload?.length) { if (!payload?.length) {
return null; return null
} }
return ( return (
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"flex flex-wrap items-center justify-center gap-4", "flex flex-wrap items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3", verticalAlign === "top" ? "pb-3" : "pt-3",
className, className,
)} )}
> >
{payload.map((item) => { {payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`; const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key); const itemConfig = getPayloadConfigFromPayload(config, item, key)
return ( return (
<div <div
key={item.value} key={item.value}
className={cn( className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground", "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
)} )}
> >
{itemConfig?.icon && !hideIcon ? ( {itemConfig?.icon && !hideIcon ? (
<itemConfig.icon /> <itemConfig.icon />
) : ( ) : (
<div <div
className="h-2 w-2 shrink-0 rounded-[2px]" className="h-2 w-2 shrink-0 rounded-[2px]"
style={{ style={{
backgroundColor: item.color, backgroundColor: item.color,
}} }}
/> />
)} )}
{key} {key}
</div> </div>
); )
})} })}
</div> </div>
); )
}, })
); ChartLegendContent.displayName = "ChartLegend"
ChartLegendContent.displayName = "ChartLegend";
// Helper to extract item config from a payload. // Helper to extract item config from a payload.
function getPayloadConfigFromPayload( function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
config: ChartConfig,
payload: unknown,
key: string,
) {
if (typeof payload !== "object" || payload === null) { if (typeof payload !== "object" || payload === null) {
return undefined; return undefined
} }
const payloadPayload = const payloadPayload =
"payload" in payload && "payload" in payload && typeof payload.payload === "object" && payload.payload !== null
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload ? payload.payload
: undefined; : undefined
let configLabelKey: string = key; let configLabelKey: string = key
if ( if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
key in payload && configLabelKey = payload[key as keyof typeof payload] as string
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if ( } else if (
payloadPayload && payloadPayload &&
key in payloadPayload && key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string" typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) { ) {
configLabelKey = payloadPayload[ configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string
key as keyof typeof payloadPayload
] as string;
} }
return configLabelKey in config return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
? config[configLabelKey]
: config[key as keyof typeof config];
} }
export { export {
@ -372,4 +336,4 @@ export {
ChartLegend, ChartLegend,
ChartLegendContent, ChartLegendContent,
ChartStyle, ChartStyle,
}; }

View File

@ -1,26 +1,26 @@
"use client"; "use client"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"; import { Check, ChevronRight, Circle } from "lucide-react"
import * as React from "react"; import * as React from "react"
const DropdownMenu = DropdownMenuPrimitive.Root; const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group; const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal; const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub; const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef< const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean; inset?: boolean
} }
>(({ className, inset, children, ...props }, ref) => ( >(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
@ -35,9 +35,8 @@ const DropdownMenuSubTrigger = React.forwardRef<
{children} {children}
<ChevronRight className="ml-auto h-4 w-4" /> <ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)); ))
DropdownMenuSubTrigger.displayName = DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@ -51,9 +50,8 @@ const DropdownMenuSubContent = React.forwardRef<
)} )}
{...props} {...props}
/> />
)); ))
DropdownMenuSubContent.displayName = DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@ -70,13 +68,13 @@ const DropdownMenuContent = React.forwardRef<
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
)); ))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>, React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean; inset?: boolean
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
@ -88,8 +86,8 @@ const DropdownMenuItem = React.forwardRef<
)} )}
{...props} {...props}
/> />
)); ))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef< const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@ -111,9 +109,8 @@ const DropdownMenuCheckboxItem = React.forwardRef<
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
)); ))
DropdownMenuCheckboxItem.displayName = DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@ -134,26 +131,22 @@ const DropdownMenuRadioItem = React.forwardRef<
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
)); ))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef< const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>, React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean; inset?: boolean
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
ref={ref} ref={ref}
className={cn( className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props} {...props}
/> />
)); ))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef< const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@ -164,21 +157,13 @@ const DropdownMenuSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)} className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} {...props}
/> />
)); ))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({ const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
className, return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
...props }
}: React.HTMLAttributes<HTMLSpanElement>) => { DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export { export {
DropdownMenu, DropdownMenu,
@ -196,4 +181,4 @@ export {
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuTrigger,
}; }

View File

@ -1,7 +1,7 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import * as React from "react"; import * as React from "react"
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>; export type InputProps = React.InputHTMLAttributes<HTMLInputElement>
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {
@ -15,9 +15,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
ref={ref} ref={ref}
{...props} {...props}
/> />
); )
}, },
); )
Input.displayName = "Input"; Input.displayName = "Input"
export { Input }; export { Input }

View File

@ -1,25 +1,20 @@
"use client"; "use client"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import * as LabelPrimitive from "@radix-ui/react-label"; import * as LabelPrimitive from "@radix-ui/react-label"
import { type VariantProps, cva } from "class-variance-authority"; import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"; import * as React from "react"
const labelVariants = cva( const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
); )
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<LabelPrimitive.Root <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
ref={ref} ))
className={cn(labelVariants(), className)} Label.displayName = LabelPrimitive.Root.displayName
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label }; export { Label }

View File

@ -1,8 +1,8 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"; import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"; import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react"
import * as React from "react"; import * as React from "react"
const NavigationMenu = React.forwardRef< const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>, React.ElementRef<typeof NavigationMenuPrimitive.Root>,
@ -10,17 +10,14 @@ const NavigationMenu = React.forwardRef<
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root <NavigationMenuPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props} {...props}
> >
{children} {children}
<NavigationMenuViewport /> <NavigationMenuViewport />
</NavigationMenuPrimitive.Root> </NavigationMenuPrimitive.Root>
)); ))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName; NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef< const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>, React.ElementRef<typeof NavigationMenuPrimitive.List>,
@ -28,20 +25,17 @@ const NavigationMenuList = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List <NavigationMenuPrimitive.List
ref={ref} ref={ref}
className={cn( className={cn("group flex flex-1 list-none items-center justify-center space-x-1", className)}
"group flex flex-1 list-none items-center justify-center space-x-1",
className,
)}
{...props} {...props}
/> />
)); ))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName; NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item; const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva( const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-transparent px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50", "group inline-flex h-10 w-max items-center justify-center rounded-md bg-transparent px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50",
); )
const NavigationMenuTrigger = React.forwardRef< const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>, React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
@ -58,8 +52,8 @@ const NavigationMenuTrigger = React.forwardRef<
aria-hidden="true" aria-hidden="true"
/> />
</NavigationMenuPrimitive.Trigger> </NavigationMenuPrimitive.Trigger>
)); ))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName; NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef< const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>, React.ElementRef<typeof NavigationMenuPrimitive.Content>,
@ -73,10 +67,10 @@ const NavigationMenuContent = React.forwardRef<
)} )}
{...props} {...props}
/> />
)); ))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName; NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link; const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef< const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>, React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
@ -92,9 +86,8 @@ const NavigationMenuViewport = React.forwardRef<
{...props} {...props}
/> />
</div> </div>
)); ))
NavigationMenuViewport.displayName = NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef< const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>, React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
@ -110,9 +103,8 @@ const NavigationMenuIndicator = React.forwardRef<
> >
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" /> <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator> </NavigationMenuPrimitive.Indicator>
)); ))
NavigationMenuIndicator.displayName = NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName
NavigationMenuPrimitive.Indicator.displayName;
export { export {
NavigationMenu, NavigationMenu,
@ -124,4 +116,4 @@ export {
NavigationMenuTrigger, NavigationMenuTrigger,
navigationMenuTriggerStyle, navigationMenuTriggerStyle,
NavigationMenuViewport, NavigationMenuViewport,
}; }

View File

@ -1,12 +1,12 @@
"use client"; "use client"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import * as PopoverPrimitive from "@radix-ui/react-popover"; import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react"; import * as React from "react"
const Popover = PopoverPrimitive.Root; const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger; const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef< const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, React.ElementRef<typeof PopoverPrimitive.Content>,
@ -24,7 +24,7 @@ const PopoverContent = React.forwardRef<
{...props} {...props}
/> />
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
)); ))
PopoverContent.displayName = PopoverPrimitive.Content.displayName; PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }; export { Popover, PopoverTrigger, PopoverContent }

View File

@ -1,21 +1,18 @@
"use client"; "use client"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import * as ProgressPrimitive from "@radix-ui/react-progress"; import * as ProgressPrimitive from "@radix-ui/react-progress"
import * as React from "react"; import * as React from "react"
const Progress = React.forwardRef< const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>, React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & { React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string; // 添加一个新的可选属性来自定义Indicator的类名 indicatorClassName?: string // 添加一个新的可选属性来自定义Indicator的类名
} }
>(({ className, value, indicatorClassName, ...props }, ref) => ( >(({ className, value, indicatorClassName, ...props }, ref) => (
<ProgressPrimitive.Root <ProgressPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className,
)}
{...props} {...props}
> >
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
@ -26,7 +23,7 @@ const Progress = React.forwardRef<
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>
)); ))
Progress.displayName = ProgressPrimitive.Root.displayName; Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }; export { Progress }

View File

@ -1,30 +1,25 @@
"use client"; "use client"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import * as SeparatorPrimitive from "@radix-ui/react-separator"; import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react"; import * as React from "react"
const Separator = React.forwardRef< const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>, React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>( >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
( <SeparatorPrimitive.Root
{ className, orientation = "horizontal", decorative = true, ...props }, ref={ref}
ref, decorative={decorative}
) => ( orientation={orientation}
<SeparatorPrimitive.Root className={cn(
ref={ref} "shrink-0 bg-border",
decorative={decorative} orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
orientation={orientation} className,
className={cn( )}
"shrink-0 bg-border", {...props}
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", />
className, ))
)} Separator.displayName = SeparatorPrimitive.Root.displayName
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator }; export { Separator }

View File

@ -1,18 +1,18 @@
"use client"; "use client"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import * as SheetPrimitive from "@radix-ui/react-dialog"; import * as SheetPrimitive from "@radix-ui/react-dialog"
import { type VariantProps, cva } from "class-variance-authority"; import { type VariantProps, cva } from "class-variance-authority"
import { X } from "lucide-react"; import { X } from "lucide-react"
import * as React from "react"; import * as React from "react"
const Sheet = SheetPrimitive.Root; const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger; const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close; const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal; const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef< const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>, React.ElementRef<typeof SheetPrimitive.Overlay>,
@ -26,8 +26,8 @@ const SheetOverlay = React.forwardRef<
{...props} {...props}
ref={ref} ref={ref}
/> />
)); ))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva( const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
@ -46,7 +46,7 @@ const sheetVariants = cva(
side: "right", side: "right",
}, },
}, },
); )
interface SheetContentProps interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
@ -58,11 +58,7 @@ const SheetContent = React.forwardRef<
>(({ side = "right", className, children, ...props }, ref) => ( >(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal> <SheetPortal>
<SheetOverlay /> <SheetOverlay />
<SheetPrimitive.Content <SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children} {children}
<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"> <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" />
@ -70,36 +66,21 @@ const SheetContent = React.forwardRef<
</SheetPrimitive.Close> </SheetPrimitive.Close>
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPortal>
)); ))
SheetContent.displayName = SheetPrimitive.Content.displayName; SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({ const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
className, <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
...props )
}: React.HTMLAttributes<HTMLDivElement>) => ( SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn( className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
"flex flex-col space-y-2 text-center sm:text-left",
className,
)}
{...props} {...props}
/> />
); )
SheetHeader.displayName = "SheetHeader"; SheetFooter.displayName = "SheetFooter"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef< const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>, React.ElementRef<typeof SheetPrimitive.Title>,
@ -110,8 +91,8 @@ const SheetTitle = React.forwardRef<
className={cn("text-lg font-semibold text-foreground", className)} className={cn("text-lg font-semibold text-foreground", className)}
{...props} {...props}
/> />
)); ))
SheetTitle.displayName = SheetPrimitive.Title.displayName; SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef< const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>, React.ElementRef<typeof SheetPrimitive.Description>,
@ -122,8 +103,8 @@ const SheetDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)); ))
SheetDescription.displayName = SheetPrimitive.Description.displayName; SheetDescription.displayName = SheetPrimitive.Description.displayName
export { export {
Sheet, Sheet,
@ -136,4 +117,4 @@ export {
SheetPortal, SheetPortal,
SheetTitle, SheetTitle,
SheetTrigger, SheetTrigger,
}; }

View File

@ -1,15 +1,7 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
function Skeleton({ function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
className, return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
} }
export { Skeleton }; export { Skeleton }

View File

@ -1,8 +1,8 @@
"use client"; "use client"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import * as SwitchPrimitives from "@radix-ui/react-switch"; import * as SwitchPrimitives from "@radix-ui/react-switch"
import * as React from "react"; import * as React from "react"
const Switch = React.forwardRef< const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>, React.ElementRef<typeof SwitchPrimitives.Root>,
@ -22,7 +22,7 @@ const Switch = React.forwardRef<
)} )}
/> />
</SwitchPrimitives.Root> </SwitchPrimitives.Root>
)); ))
Switch.displayName = SwitchPrimitives.Root.displayName; Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }; export { Switch }

View File

@ -1,14 +1,14 @@
"use client"; "use client"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from "react"; import * as React from "react"
const TooltipProvider = TooltipPrimitive.Provider; const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root; const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger; const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef< const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
@ -23,7 +23,7 @@ const TooltipContent = React.forwardRef<
)} )}
{...props} {...props}
/> />
)); ))
TooltipContent.displayName = TooltipPrimitive.Content.displayName; TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }

View File

@ -1,5 +1,5 @@
// @auto-i18n-check. Please do not delete the line. // @auto-i18n-check. Please do not delete the line.
import getEnv from "./lib/env-entry"; import getEnv from "./lib/env-entry"
export const localeItems = [ export const localeItems = [
{ code: "en", name: "English" }, { code: "en", name: "English" },
@ -19,7 +19,7 @@ export const localeItems = [
//{code: 'ru', name: 'Русский'}, //{code: 'ru', name: 'Русский'},
//{code: 'th', name: 'ไทย'}, //{code: 'th', name: 'ไทย'},
//{code: 'vi', name: 'Tiếng Việt'}, //{code: 'vi', name: 'Tiếng Việt'},
]; ]
export const locales = localeItems.map((item) => item.code); export const locales = localeItems.map((item) => item.code)
export const defaultLocale = getEnv("DefaultLocale") || "en"; export const defaultLocale = getEnv("DefaultLocale") || "en"

View File

@ -1,17 +1,14 @@
"use server"; "use server"
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry"
import { cookies } from "next/headers"; import { cookies } from "next/headers"
const COOKIE_NAME = "NEXT_LOCALE"; const COOKIE_NAME = "NEXT_LOCALE"
export async function getUserLocale() { export async function getUserLocale() {
return ( return (await cookies()).get(COOKIE_NAME)?.value || (getEnv("DefaultLocale") ?? "en")
(await cookies()).get(COOKIE_NAME)?.value ||
(getEnv("DefaultLocale") ?? "en")
);
} }
export async function setUserLocale(locale: string) { export async function setUserLocale(locale: string) {
(await cookies()).set(COOKIE_NAME, locale); ;(await cookies()).set(COOKIE_NAME, locale)
} }

View File

@ -1,11 +1,11 @@
import { getUserLocale } from "@/i18n/locale"; import { getUserLocale } from "@/i18n/locale"
import { getRequestConfig } from "next-intl/server"; import { getRequestConfig } from "next-intl/server"
export default getRequestConfig(async () => { export default getRequestConfig(async () => {
const locale = await getUserLocale(); const locale = await getUserLocale()
return { return {
locale, locale,
messages: (await import(`../messages/${locale}.json`)).default, messages: (await import(`../messages/${locale}.json`)).default,
}; }
}); })

View File

@ -180,4 +180,4 @@ export const devGeoString = `{"type":"FeatureCollection","features":[
{"type":"Feature","id":"ZMB","properties":{"name":"Zambia"},"geometry":{"type":"Polygon","coordinates":[[[32.759375,-9.230599],[33.231388,-9.676722],[33.485688,-10.525559],[33.31531,-10.79655],[33.114289,-11.607198],[33.306422,-12.435778],[32.991764,-12.783871],[32.688165,-13.712858],[33.214025,-13.97186],[30.179481,-14.796099],[30.274256,-15.507787],[29.516834,-15.644678],[28.947463,-16.043051],[28.825869,-16.389749],[28.467906,-16.4684],[27.598243,-17.290831],[27.044427,-17.938026],[26.706773,-17.961229],[26.381935,-17.846042],[25.264226,-17.73654],[25.084443,-17.661816],[25.07695,-17.578823],[24.682349,-17.353411],[24.033862,-17.295843],[23.215048,-17.523116],[22.562478,-16.898451],[21.887843,-16.08031],[21.933886,-12.898437],[24.016137,-12.911046],[23.930922,-12.565848],[24.079905,-12.191297],[23.904154,-11.722282],[24.017894,-11.237298],[23.912215,-10.926826],[24.257155,-10.951993],[24.314516,-11.262826],[24.78317,-11.238694],[25.418118,-11.330936],[25.75231,-11.784965],[26.553088,-11.92444],[27.16442,-11.608748],[27.388799,-12.132747],[28.155109,-12.272481],[28.523562,-12.698604],[28.934286,-13.248958],[29.699614,-13.257227],[29.616001,-12.178895],[29.341548,-12.360744],[28.642417,-11.971569],[28.372253,-11.793647],[28.49607,-10.789884],[28.673682,-9.605925],[28.449871,-9.164918],[28.734867,-8.526559],[29.002912,-8.407032],[30.346086,-8.238257],[30.740015,-8.340007],[31.157751,-8.594579],[31.556348,-8.762049],[32.191865,-8.930359],[32.759375,-9.230599]]]}}, {"type":"Feature","id":"ZMB","properties":{"name":"Zambia"},"geometry":{"type":"Polygon","coordinates":[[[32.759375,-9.230599],[33.231388,-9.676722],[33.485688,-10.525559],[33.31531,-10.79655],[33.114289,-11.607198],[33.306422,-12.435778],[32.991764,-12.783871],[32.688165,-13.712858],[33.214025,-13.97186],[30.179481,-14.796099],[30.274256,-15.507787],[29.516834,-15.644678],[28.947463,-16.043051],[28.825869,-16.389749],[28.467906,-16.4684],[27.598243,-17.290831],[27.044427,-17.938026],[26.706773,-17.961229],[26.381935,-17.846042],[25.264226,-17.73654],[25.084443,-17.661816],[25.07695,-17.578823],[24.682349,-17.353411],[24.033862,-17.295843],[23.215048,-17.523116],[22.562478,-16.898451],[21.887843,-16.08031],[21.933886,-12.898437],[24.016137,-12.911046],[23.930922,-12.565848],[24.079905,-12.191297],[23.904154,-11.722282],[24.017894,-11.237298],[23.912215,-10.926826],[24.257155,-10.951993],[24.314516,-11.262826],[24.78317,-11.238694],[25.418118,-11.330936],[25.75231,-11.784965],[26.553088,-11.92444],[27.16442,-11.608748],[27.388799,-12.132747],[28.155109,-12.272481],[28.523562,-12.698604],[28.934286,-13.248958],[29.699614,-13.257227],[29.616001,-12.178895],[29.341548,-12.360744],[28.642417,-11.971569],[28.372253,-11.793647],[28.49607,-10.789884],[28.673682,-9.605925],[28.449871,-9.164918],[28.734867,-8.526559],[29.002912,-8.407032],[30.346086,-8.238257],[30.740015,-8.340007],[31.157751,-8.594579],[31.556348,-8.762049],[32.191865,-8.930359],[32.759375,-9.230599]]]}},
{"type":"Feature","id":"ZWE","properties":{"name":"Zimbabwe"},"geometry":{"type":"Polygon","coordinates":[[[31.191409,-22.25151],[30.659865,-22.151567],[30.322883,-22.271612],[29.839037,-22.102216],[29.432188,-22.091313],[28.794656,-21.639454],[28.02137,-21.485975],[27.727228,-20.851802],[27.724747,-20.499059],[27.296505,-20.39152],[26.164791,-19.293086],[25.850391,-18.714413],[25.649163,-18.536026],[25.264226,-17.73654],[26.381935,-17.846042],[26.706773,-17.961229],[27.044427,-17.938026],[27.598243,-17.290831],[28.467906,-16.4684],[28.825869,-16.389749],[28.947463,-16.043051],[29.516834,-15.644678],[30.274256,-15.507787],[30.338955,-15.880839],[31.173064,-15.860944],[31.636498,-16.07199],[31.852041,-16.319417],[32.328239,-16.392074],[32.847639,-16.713398],[32.849861,-17.979057],[32.654886,-18.67209],[32.611994,-19.419383],[32.772708,-19.715592],[32.659743,-20.30429],[32.508693,-20.395292],[32.244988,-21.116489],[31.191409,-22.25151]]]}} {"type":"Feature","id":"ZWE","properties":{"name":"Zimbabwe"},"geometry":{"type":"Polygon","coordinates":[[[31.191409,-22.25151],[30.659865,-22.151567],[30.322883,-22.271612],[29.839037,-22.102216],[29.432188,-22.091313],[28.794656,-21.639454],[28.02137,-21.485975],[27.727228,-20.851802],[27.724747,-20.499059],[27.296505,-20.39152],[26.164791,-19.293086],[25.850391,-18.714413],[25.649163,-18.536026],[25.264226,-17.73654],[26.381935,-17.846042],[26.706773,-17.961229],[27.044427,-17.938026],[27.598243,-17.290831],[28.467906,-16.4684],[28.825869,-16.389749],[28.947463,-16.043051],[29.516834,-15.644678],[30.274256,-15.507787],[30.338955,-15.880839],[31.173064,-15.860944],[31.636498,-16.07199],[31.852041,-16.319417],[32.328239,-16.392074],[32.847639,-16.713398],[32.849861,-17.979057],[32.654886,-18.67209],[32.611994,-19.419383],[32.772708,-19.715592],[32.659743,-20.30429],[32.508693,-20.395292],[32.244988,-21.116489],[31.191409,-22.25151]]]}}
]} ]}
`; `

View File

@ -1,8 +1,8 @@
import { env } from "next-runtime-env"; import { env } from "next-runtime-env"
export default function getEnv(key: string) { export default function getEnv(key: string) {
if (key.startsWith("NEXT_PUBLIC_")) { if (key.startsWith("NEXT_PUBLIC_")) {
return env(key); return env(key)
} }
return process.env[key]; return process.env[key]
} }

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,4 @@
export const countryCoordinates: Record< export const countryCoordinates: Record<string, { lat: number; lng: number; name: string }> = {
string,
{ lat: number; lng: number; name: string }
> = {
// 亚洲 // 亚洲
AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗 AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗
AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚 AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚
@ -208,4 +205,4 @@ export const countryCoordinates: Record<
EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉 EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉
ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚 ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚
ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦 ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦
}; }

View File

@ -1,5 +1,5 @@
import React from "react"; 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 {
if ( if (
@ -48,24 +48,24 @@ export function GetFontLogoClass(platform: string): string {
"zorin", "zorin",
].indexOf(platform) > -1 ].indexOf(platform) > -1
) { ) {
return platform; return platform
} }
if (platform == "darwin") { if (platform == "darwin") {
return "apple"; return "apple"
} }
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) { if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "tux"; return "tux"
} }
if (platform == "amazon") { if (platform == "amazon") {
return "redhat"; return "redhat"
} }
if (platform == "arch") { if (platform == "arch") {
return "archlinux"; return "archlinux"
} }
if (platform.toLowerCase().includes("opensuse")) { if (platform.toLowerCase().includes("opensuse")) {
return "opensuse"; return "opensuse"
} }
return "tux"; return "tux"
} }
export function GetOsName(platform: string): string { export function GetOsName(platform: string): string {
@ -111,39 +111,33 @@ export function GetOsName(platform: string): string {
"zorin", "zorin",
].indexOf(platform) > -1 ].indexOf(platform) > -1
) { ) {
return platform.charAt(0).toUpperCase() + platform.slice(1); return platform.charAt(0).toUpperCase() + platform.slice(1)
} }
if (platform == "darwin") { if (platform == "darwin") {
return "macOS"; return "macOS"
} }
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) { if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "Linux"; return "Linux"
} }
if (platform == "amazon") { if (platform == "amazon") {
return "Redhat"; return "Redhat"
} }
if (platform == "arch") { if (platform == "arch") {
return "Archlinux"; return "Archlinux"
} }
if (platform.toLowerCase().includes("opensuse")) { if (platform.toLowerCase().includes("opensuse")) {
return "Opensuse"; return "Opensuse"
} }
return "Linux"; return "Linux"
} }
export function MageMicrosoftWindows(props: SVGProps<SVGSVGElement>) { export function MageMicrosoftWindows(props: SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path <path
fill="currentColor" fill="currentColor"
d="M2.75 7.189V2.865c0-.102 0-.115.115-.115h8.622c.128 0 .14 0 .14.128V11.5c0 .128 0 .128-.14.128H2.865c-.102 0-.115 0-.115-.116zM7.189 21.25H2.865c-.102 0-.115 0-.115-.116V12.59c0-.128 0-.128.128-.128h8.635c.102 0 .115 0 .115.115v8.57c0 .09 0 .103-.116.103zM21.25 7.189v4.31c0 .116 0 .116-.116.116h-8.557c-.102 0-.128 0-.128-.115V2.865c0-.09 0-.102.115-.102h8.48c.206 0 .206 0 .206.205zm-8.763 9.661v-4.273c0-.09 0-.115.103-.09h8.621c.026 0 0 .09 0 .142v8.518a.06.06 0 0 1-.017.06a.06.06 0 0 1-.06.017H12.54s-.09 0-.077-.09V16.85z" d="M2.75 7.189V2.865c0-.102 0-.115.115-.115h8.622c.128 0 .14 0 .14.128V11.5c0 .128 0 .128-.14.128H2.865c-.102 0-.115 0-.115-.116zM7.189 21.25H2.865c-.102 0-.115 0-.115-.116V12.59c0-.128 0-.128.128-.128h8.635c.102 0 .115 0 .115.115v8.57c0 .09 0 .103-.116.103zM21.25 7.189v4.31c0 .116 0 .116-.116.116h-8.557c-.102 0-.128 0-.128-.115V2.865c0-.09 0-.102.115-.102h8.48c.206 0 .206 0 .206.205zm-8.763 9.661v-4.273c0-.09 0-.115.103-.09h8.621c.026 0 0 .09 0 .142v8.518a.06.06 0 0 1-.017.06a.06.06 0 0 1-.06.017H12.54s-.09 0-.077-.09V16.85z"
></path> ></path>
</svg> </svg>
); )
} }

File diff suppressed because one or more lines are too long

View File

@ -1,28 +1,24 @@
"use client"; "use client"
import React, { ReactNode, createContext, useContext, useState } from "react"; import React, { ReactNode, createContext, useContext, useState } from "react"
interface FilterContextType { interface FilterContextType {
filter: boolean; filter: boolean
setFilter: (filter: boolean) => void; setFilter: (filter: boolean) => void
} }
const FilterContext = createContext<FilterContextType | undefined>(undefined); const FilterContext = createContext<FilterContextType | undefined>(undefined)
export function FilterProvider({ children }: { children: ReactNode }) { export function FilterProvider({ children }: { children: ReactNode }) {
const [filter, setFilter] = useState<boolean>(false); const [filter, setFilter] = useState<boolean>(false)
return ( return <FilterContext.Provider value={{ filter, setFilter }}>{children}</FilterContext.Provider>
<FilterContext.Provider value={{ filter, setFilter }}>
{children}
</FilterContext.Provider>
);
} }
export function useFilter() { export function useFilter() {
const context = useContext(FilterContext); const context = useContext(FilterContext)
if (context === undefined) { if (context === undefined) {
throw new Error("useFilter must be used within a FilterProvider"); throw new Error("useFilter must be used within a FilterProvider")
} }
return context; return context
} }

View File

@ -1,21 +1,21 @@
"use server"; "use server"
import { NezhaAPI, ServerApi } from "@/app/types/nezha-api"; import { NezhaAPI, ServerApi } from "@/app/types/nezha-api"
import { MakeOptional } from "@/app/types/utils"; import { MakeOptional } from "@/app/types/utils"
import getEnv from "@/lib/env-entry"; import getEnv from "@/lib/env-entry"
import { unstable_noStore as noStore } from "next/cache"; import { unstable_noStore as noStore } from "next/cache"
export async function GetNezhaData() { export async function GetNezhaData() {
noStore(); noStore()
let nezhaBaseUrl = getEnv("NezhaBaseUrl"); let nezhaBaseUrl = getEnv("NezhaBaseUrl")
if (!nezhaBaseUrl) { if (!nezhaBaseUrl) {
console.error("NezhaBaseUrl is not set"); console.error("NezhaBaseUrl is not set")
throw new Error("NezhaBaseUrl is not set"); throw new Error("NezhaBaseUrl is not set")
} }
// Remove trailing slash // Remove trailing slash
nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, ""); nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, "")
try { try {
const response = await fetch(`${nezhaBaseUrl}/api/v1/server/details`, { const response = await fetch(`${nezhaBaseUrl}/api/v1/server/details`, {
@ -25,20 +25,20 @@ export async function GetNezhaData() {
next: { next: {
revalidate: 0, revalidate: 0,
}, },
}); })
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text()
throw new Error(`Failed to fetch data: ${response.status} ${errorText}`); throw new Error(`Failed to fetch data: ${response.status} ${errorText}`)
} }
const resData = await response.json(); const resData = await response.json()
if (!resData.result) { if (!resData.result) {
throw new Error("NezhaData fetch failed: 'result' field is missing"); throw new Error("NezhaData fetch failed: 'result' field is missing")
} }
const nezhaData = resData.result as NezhaAPI[]; const nezhaData = resData.result as NezhaAPI[]
const data: ServerApi = { const data: ServerApi = {
live_servers: 0, live_servers: 0,
offline_servers: 0, offline_servers: 0,
@ -47,102 +47,95 @@ export async function GetNezhaData() {
total_in_speed: 0, total_in_speed: 0,
total_out_speed: 0, total_out_speed: 0,
result: [], result: [],
}; }
const forceShowAllServers = getEnv("ForceShowAllServers") === "true"; const forceShowAllServers = getEnv("ForceShowAllServers") === "true"
const nezhaDataFiltered = forceShowAllServers const nezhaDataFiltered = forceShowAllServers
? nezhaData ? nezhaData
: nezhaData.filter((element) => !element.hide_for_guest); : nezhaData.filter((element) => !element.hide_for_guest)
const timestamp = Date.now() / 1000; const timestamp = Date.now() / 1000
data.result = nezhaDataFiltered.map( data.result = nezhaDataFiltered.map(
(element: MakeOptional<NezhaAPI, "ipv4" | "ipv6" | "valid_ip">) => { (element: MakeOptional<NezhaAPI, "ipv4" | "ipv6" | "valid_ip">) => {
const isOnline = timestamp - element.last_active <= 300; const isOnline = timestamp - element.last_active <= 300
element.online_status = isOnline; element.online_status = isOnline
if (isOnline) { if (isOnline) {
data.live_servers += 1; data.live_servers += 1
} else { } else {
data.offline_servers += 1; data.offline_servers += 1
} }
data.total_out_bandwidth += element.status.NetOutTransfer; data.total_out_bandwidth += element.status.NetOutTransfer
data.total_in_bandwidth += element.status.NetInTransfer; data.total_in_bandwidth += element.status.NetInTransfer
data.total_in_speed += element.status.NetInSpeed; data.total_in_speed += element.status.NetInSpeed
data.total_out_speed += element.status.NetOutSpeed; data.total_out_speed += element.status.NetOutSpeed
// Remove unwanted properties // Remove unwanted properties
delete element.ipv4; delete element.ipv4
delete element.ipv6; delete element.ipv6
delete element.valid_ip; delete element.valid_ip
return element; return element
}, },
); )
return data; return data
} catch (error) { } catch (error) {
console.error("GetNezhaData error:", error); console.error("GetNezhaData error:", error)
throw error; // Rethrow the error to be caught by the caller throw error // Rethrow the error to be caught by the caller
} }
} }
export async function GetServerMonitor({ server_id }: { server_id: number }) { export async function GetServerMonitor({ server_id }: { server_id: number }) {
let nezhaBaseUrl = getEnv("NezhaBaseUrl"); let nezhaBaseUrl = getEnv("NezhaBaseUrl")
if (!nezhaBaseUrl) { if (!nezhaBaseUrl) {
console.error("NezhaBaseUrl is not set"); console.error("NezhaBaseUrl is not set")
throw new Error("NezhaBaseUrl is not set"); throw new Error("NezhaBaseUrl is not set")
} }
// Remove trailing slash // Remove trailing slash
nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, ""); nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, "")
try { try {
const response = await fetch( const response = await fetch(`${nezhaBaseUrl}/api/v1/monitor/${server_id}`, {
`${nezhaBaseUrl}/api/v1/monitor/${server_id}`, headers: {
{ Authorization: getEnv("NezhaAuth") as string,
headers: {
Authorization: getEnv("NezhaAuth") as string,
},
next: {
revalidate: 0,
},
}, },
); next: {
revalidate: 0,
},
})
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text()
throw new Error(`Failed to fetch data: ${response.status} ${errorText}`); throw new Error(`Failed to fetch data: ${response.status} ${errorText}`)
} }
const resData = await response.json(); const resData = await response.json()
const monitorData = resData.result; const monitorData = resData.result
if (!monitorData) { if (!monitorData) {
console.error("MonitorData fetch failed:", resData); console.error("MonitorData fetch failed:", resData)
throw new Error("MonitorData fetch failed: 'result' field is missing"); throw new Error("MonitorData fetch failed: 'result' field is missing")
} }
return monitorData; return monitorData
} catch (error) { } catch (error) {
console.error("GetServerMonitor error:", error); console.error("GetServerMonitor error:", error)
throw error; throw error
} }
} }
export async function GetServerIP({ export async function GetServerIP({ server_id }: { server_id: number }): Promise<string> {
server_id, let nezhaBaseUrl = getEnv("NezhaBaseUrl")
}: {
server_id: number;
}): Promise<string> {
let nezhaBaseUrl = getEnv("NezhaBaseUrl");
if (!nezhaBaseUrl) { if (!nezhaBaseUrl) {
console.error("NezhaBaseUrl is not set"); console.error("NezhaBaseUrl is not set")
throw new Error("NezhaBaseUrl is not set"); throw new Error("NezhaBaseUrl is not set")
} }
// Remove trailing slash // Remove trailing slash
nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, ""); nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, "")
try { try {
const response = await fetch(`${nezhaBaseUrl}/api/v1/server/details`, { const response = await fetch(`${nezhaBaseUrl}/api/v1/server/details`, {
@ -152,89 +145,80 @@ export async function GetServerIP({
next: { next: {
revalidate: 0, revalidate: 0,
}, },
}); })
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text()
throw new Error(`Failed to fetch data: ${response.status} ${errorText}`); throw new Error(`Failed to fetch data: ${response.status} ${errorText}`)
} }
const resData = await response.json(); const resData = await response.json()
if (!resData.result) { if (!resData.result) {
throw new Error("NezhaData fetch failed: 'result' field is missing"); throw new Error("NezhaData fetch failed: 'result' field is missing")
} }
const nezhaData = resData.result as NezhaAPI[]; const nezhaData = resData.result as NezhaAPI[]
// Find the server with the given ID // Find the server with the given ID
const server = nezhaData.find((element) => element.id === server_id); const server = nezhaData.find((element) => element.id === server_id)
if (!server) { if (!server) {
throw new Error(`Server with ID ${server_id} not found`); throw new Error(`Server with ID ${server_id} not found`)
} }
return server?.valid_ip || server?.ipv4 || server?.ipv6 || ""; return server?.valid_ip || server?.ipv4 || server?.ipv6 || ""
} catch (error) { } catch (error) {
console.error("GetNezhaData error:", error); console.error("GetNezhaData error:", error)
throw error; // Rethrow the error to be caught by the caller throw error // Rethrow the error to be caught by the caller
} }
} }
export async function GetServerDetail({ server_id }: { server_id: number }) { export async function GetServerDetail({ server_id }: { server_id: number }) {
let nezhaBaseUrl = getEnv("NezhaBaseUrl"); let nezhaBaseUrl = getEnv("NezhaBaseUrl")
if (!nezhaBaseUrl) { if (!nezhaBaseUrl) {
console.error("NezhaBaseUrl is not set"); console.error("NezhaBaseUrl is not set")
throw new Error("NezhaBaseUrl is not set"); throw new Error("NezhaBaseUrl is not set")
} }
// Remove trailing slash // Remove trailing slash
nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, ""); nezhaBaseUrl = nezhaBaseUrl.replace(/\/$/, "")
try { try {
const response = await fetch( const response = await fetch(`${nezhaBaseUrl}/api/v1/server/details?id=${server_id}`, {
`${nezhaBaseUrl}/api/v1/server/details?id=${server_id}`, headers: {
{ Authorization: getEnv("NezhaAuth") as string,
headers: {
Authorization: getEnv("NezhaAuth") as string,
},
next: {
revalidate: 0,
},
}, },
); next: {
revalidate: 0,
},
})
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text()
throw new Error(`Failed to fetch data: ${response.status} ${errorText}`); throw new Error(`Failed to fetch data: ${response.status} ${errorText}`)
} }
const resData = await response.json(); const resData = await response.json()
const detailDataList = resData.result; const detailDataList = resData.result
if ( if (!detailDataList || !Array.isArray(detailDataList) || detailDataList.length === 0) {
!detailDataList || console.error("MonitorData fetch failed:", resData)
!Array.isArray(detailDataList) || throw new Error("MonitorData fetch failed: 'result' field is missing or empty")
detailDataList.length === 0
) {
console.error("MonitorData fetch failed:", resData);
throw new Error(
"MonitorData fetch failed: 'result' field is missing or empty",
);
} }
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 <= 300; element.online_status = timestamp - element.last_active <= 300
delete element.ipv4; delete element.ipv4
delete element.ipv6; delete element.ipv6
delete element.valid_ip; delete element.valid_ip
return element; return element
})[0]; })[0]
return detailData; return detailData
} catch (error) { } catch (error) {
console.error("GetServerDetail error:", error); console.error("GetServerDetail error:", error)
throw error; // Rethrow the error to be handled by the caller throw error // Rethrow the error to be handled by the caller
} }
} }

View File

@ -1,30 +1,26 @@
"use client"; "use client"
import React, { ReactNode, createContext, useContext, useState } from "react"; import React, { ReactNode, createContext, useContext, useState } from "react"
type Status = "all" | "online" | "offline"; type Status = "all" | "online" | "offline"
interface StatusContextType { interface StatusContextType {
status: Status; status: Status
setStatus: (status: Status) => void; setStatus: (status: Status) => void
} }
const StatusContext = createContext<StatusContextType | undefined>(undefined); const StatusContext = createContext<StatusContextType | undefined>(undefined)
export function StatusProvider({ children }: { children: ReactNode }) { export function StatusProvider({ children }: { children: ReactNode }) {
const [status, setStatus] = useState<Status>("all"); const [status, setStatus] = useState<Status>("all")
return ( return <StatusContext.Provider value={{ status, setStatus }}>{children}</StatusContext.Provider>
<StatusContext.Provider value={{ status, setStatus }}>
{children}
</StatusContext.Provider>
);
} }
export function useStatus() { export function useStatus() {
const context = useContext(StatusContext); const context = useContext(StatusContext)
if (context === undefined) { if (context === undefined) {
throw new Error("useStatus must be used within a StatusProvider"); throw new Error("useStatus must be used within a StatusProvider")
} }
return context; return context
} }

View File

@ -1,9 +1,9 @@
import { 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"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs))
} }
export function formatNezhaInfo(serverInfo: NezhaAPISafe) { export function formatNezhaInfo(serverInfo: NezhaAPISafe) {
@ -21,98 +21,86 @@ export function formatNezhaInfo(serverInfo: NezhaAPISafe) {
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,
country_code: serverInfo.host.CountryCode, country_code: serverInfo.host.CountryCode,
}; }
} }
export function formatBytes(bytes: number, decimals: number = 2) { export function formatBytes(bytes: number, decimals: number = 2) {
if (!+bytes) return "0 Bytes"; if (!+bytes) return "0 Bytes"
const k = 1024; const k = 1024
const dm = decimals < 0 ? 0 : decimals; const dm = decimals < 0 ? 0 : decimals
const sizes = [ const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
"Bytes",
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
} }
export function getDaysBetweenDates(date1: string, date2: string): number { export function getDaysBetweenDates(date1: string, date2: string): number {
const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数 const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
const firstDate = new Date(date1); const firstDate = new Date(date1)
const secondDate = new Date(date2); const secondDate = new Date(date2)
// 计算两个日期之间的天数差异 // 计算两个日期之间的天数差异
return Math.round( return Math.round(Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay))
Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay),
);
} }
export const fetcher = (url: string) => export const fetcher = (url: string) =>
fetch(url) fetch(url)
.then((res) => { .then((res) => {
if (!res.ok) { if (!res.ok) {
throw new Error(res.statusText); throw new Error(res.statusText)
} }
return res.json(); return res.json()
}) })
.then((data) => data.data) .then((data) => data.data)
.catch((err) => { .catch((err) => {
console.error(err); console.error(err)
throw err; throw err
}); })
export const nezhaFetcher = async (url: string) => { export const nezhaFetcher = async (url: string) => {
const res = await fetch(url); const res = await fetch(url)
if (!res.ok) { if (!res.ok) {
const error = new Error("An error occurred while fetching the data."); const error = new Error("An error occurred while fetching the data.")
// @ts-expect-error - res.json() returns a Promise<any> // @ts-expect-error - res.json() returns a Promise<any>
error.info = await res.json(); error.info = await res.json()
// @ts-expect-error - res.status is a number // @ts-expect-error - res.status is a number
error.status = res.status; error.status = res.status
throw error; throw error
} }
return res.json(); return res.json()
}; }
export function formatRelativeTime(timestamp: number): string { export function formatRelativeTime(timestamp: number): string {
const now = Date.now(); const now = Date.now()
const diff = now - timestamp; const diff = now - timestamp
const hours = Math.floor(diff / (1000 * 60 * 60)); const hours = Math.floor(diff / (1000 * 60 * 60))
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((diff % (1000 * 60)) / 1000); const seconds = Math.floor((diff % (1000 * 60)) / 1000)
if (hours > 24) { if (hours > 24) {
const days = Math.floor(hours / 24); const days = Math.floor(hours / 24)
return `${days}d`; return `${days}d`
} else if (hours > 0) { } else if (hours > 0) {
return `${hours}h`; return `${hours}h`
} else if (minutes > 0) { } else if (minutes > 0) {
return `${minutes}m`; return `${minutes}m`
} else if (seconds >= 0) { } else if (seconds >= 0) {
return `${seconds}s`; return `${seconds}s`
} }
return "0s"; return "0s"
} }
export function formatTime(timestamp: number): string { export function formatTime(timestamp: number): string {
const date = new Date(timestamp); const date = new Date(timestamp)
const year = date.getFullYear(); const year = date.getFullYear()
const month = date.getMonth() + 1; const month = date.getMonth() + 1
const day = date.getDate(); const day = date.getDate()
const hours = date.getHours().toString().padStart(2, "0"); const hours = date.getHours().toString().padStart(2, "0")
const minutes = date.getMinutes().toString().padStart(2, "0"); const minutes = date.getMinutes().toString().padStart(2, "0")
const seconds = date.getSeconds().toString().padStart(2, "0"); const seconds = date.getSeconds().toString().padStart(2, "0")
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
} }

View File

@ -1,13 +1,13 @@
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"; import { env } from "next-runtime-env"
const bundleAnalyzer = withBundleAnalyzer({ const bundleAnalyzer = withBundleAnalyzer({
enabled: process.env.ANALYZE === "true", enabled: process.env.ANALYZE === "true",
}); })
const withNextIntl = createNextIntlPlugin(); const withNextIntl = createNextIntlPlugin()
const withPWA = withPWAInit({ const withPWA = withPWAInit({
dest: "public", dest: "public",
@ -18,7 +18,7 @@ const withPWA = withPWAInit({
workboxOptions: { workboxOptions: {
disableDevLogs: true, disableDevLogs: true,
}, },
}); })
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
@ -40,5 +40,5 @@ const nextConfig = {
fullUrl: true, fullUrl: true,
}, },
}, },
}; }
export default bundleAnalyzer(withPWA(withNextIntl(nextConfig))); export default bundleAnalyzer(withPWA(withNextIntl(nextConfig)))

View File

@ -5,7 +5,9 @@
"scripts": { "scripts": {
"dev": "next dev -p 3040", "dev": "next dev -p 3040",
"start": "node .next/standalone/server.js", "start": "node .next/standalone/server.js",
"lint": "next lint", "lint": "eslint",
"lint:fix": "eslint --fix",
"format": "prettier --write .",
"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", "build-dev": "next build",
"start-dev": "next start" "start-dev": "next start"

View File

@ -2,4 +2,4 @@ module.exports = {
plugins: { plugins: {
"@tailwindcss/postcss": {}, "@tailwindcss/postcss": {},
}, },
}; }

View File

@ -1,11 +0,0 @@
// prettier.config.js
module.exports = {
importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
endOfLine: "auto",
plugins: [
"prettier-plugin-tailwindcss",
"@trivago/prettier-plugin-sort-imports",
],
};

View File

@ -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));