mirror of
https://github.com/hamster1963/nezha-dash.git
synced 2025-04-24 21:10:45 +08:00
feat: pr lint
This commit is contained in:
parent
17946d5f0b
commit
55cc033cf5
56
.github/workflows/auto-fix-lint-format-commit.yml
vendored
Normal file
56
.github/workflows/auto-fix-lint-format-commit.yml
vendored
Normal 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
12
.prettierrc.mjs
Normal 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"],
|
||||||
|
}
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
};
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
import { handlers } from "@/auth";
|
import { handlers } from "@/auth"
|
||||||
|
|
||||||
export const { GET, POST } = handlers;
|
export const { GET, POST } = handlers
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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[]
|
||||||
}
|
}
|
||||||
|
@ -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
18
auth.ts
@ -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
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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 />
|
||||||
}
|
}
|
||||||
|
@ -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 />
|
||||||
}
|
}
|
||||||
|
@ -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"}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
export { domMax as default } from "framer-motion";
|
export { domMax as default } from "framer-motion"
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
@ -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 }
|
||||||
|
@ -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 }
|
||||||
|
@ -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,
|
|
||||||
};
|
|
||||||
|
@ -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,
|
||||||
};
|
}
|
||||||
|
@ -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,
|
||||||
};
|
}
|
||||||
|
@ -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 }
|
||||||
|
@ -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 }
|
||||||
|
@ -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,
|
||||||
};
|
}
|
||||||
|
@ -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 }
|
||||||
|
@ -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 }
|
||||||
|
@ -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 }
|
||||||
|
@ -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,
|
||||||
};
|
}
|
||||||
|
@ -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 }
|
||||||
|
@ -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 }
|
||||||
|
@ -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 }
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
|
@ -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]]]}}
|
||||||
]}
|
]}
|
||||||
`;
|
`
|
||||||
|
@ -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
@ -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" }, // 津巴布韦
|
||||||
};
|
}
|
||||||
|
@ -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
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
104
lib/utils.ts
104
lib/utils.ts
@ -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}`
|
||||||
}
|
}
|
||||||
|
@ -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)))
|
||||||
|
@ -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"
|
||||||
|
@ -2,4 +2,4 @@ module.exports = {
|
|||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
"@tailwindcss/postcss": {},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
@ -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",
|
|
||||||
],
|
|
||||||
};
|
|
@ -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));
|
||||||
|
Loading…
Reference in New Issue
Block a user