mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 09:31:55 +08:00
feat: CustomMobileBackgroundImage
This commit is contained in:
parent
e0bf568965
commit
ae8e3ea144
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"printWidth": 100,
|
"printWidth": 150,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
|
"importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
|
||||||
|
14
index.html
14
index.html
@ -80,9 +80,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (theme === "system") {
|
if (theme === "system") {
|
||||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
||||||
? "dark"
|
|
||||||
: "light"
|
|
||||||
setTheme(systemTheme)
|
setTheme(systemTheme)
|
||||||
|
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
||||||
@ -110,14 +108,8 @@
|
|||||||
<link rel="icon" type="image/png" href="/apple-touch-icon.png" />
|
<link rel="icon" type="image/png" href="/apple-touch-icon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>哪吒监控 Nezha Monitoring</title>
|
<title>哪吒监控 Nezha Monitoring</title>
|
||||||
<link
|
<link rel="stylesheet" href="https://fastly.jsdelivr.net/gh/lipis/flag-icons@7.0.0/css/flag-icons.min.css" />
|
||||||
rel="stylesheet"
|
<link rel="stylesheet" href="https://fastly.jsdelivr.net/npm/font-logos@1/assets/font-logos.css" />
|
||||||
href="https://fastly.jsdelivr.net/gh/lipis/flag-icons@7.0.0/css/flag-icons.min.css"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://fastly.jsdelivr.net/npm/font-logos@1/assets/font-logos.css"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
16
src/App.tsx
16
src/App.tsx
@ -41,18 +41,30 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error ShowNetTransfer is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
|
|
||||||
|
const customMobileBackgroundImage =
|
||||||
|
// @ts-expect-error CustomMobileBackgroundImage is a global variable
|
||||||
|
(window.CustomMobileBackgroundImage as string) !== "" ? window.CustomMobileBackgroundImage : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router basename={import.meta.env.BASE_URL}>
|
<Router basename={import.meta.env.BASE_URL}>
|
||||||
{/* 固定定位的背景层 */}
|
{/* 固定定位的背景层 */}
|
||||||
{customBackgroundImage && (
|
{customBackgroundImage && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center"
|
className={cn("fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center", {
|
||||||
|
"hidden sm:block": customMobileBackgroundImage,
|
||||||
|
})}
|
||||||
style={{ backgroundImage: `url(${customBackgroundImage})` }}
|
style={{ backgroundImage: `url(${customBackgroundImage})` }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{customMobileBackgroundImage && (
|
||||||
|
<div
|
||||||
|
className={cn("fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center sm:hidden")}
|
||||||
|
style={{ backgroundImage: `url(${customMobileBackgroundImage})` }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={cn("flex min-h-screen w-full flex-col", {
|
className={cn("flex min-h-screen w-full flex-col", {
|
||||||
"bg-background": !customBackgroundImage,
|
"bg-background": !customBackgroundImage,
|
||||||
|
@ -8,10 +8,7 @@ interface CycleTransferStatsProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({
|
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({ cycleStats, className }) => {
|
||||||
cycleStats,
|
|
||||||
className,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-4">
|
<section className="grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-4">
|
||||||
{Object.entries(cycleStats).map(([cycleId, cycleData]) => {
|
{Object.entries(cycleStats).map(([cycleId, cycleData]) => {
|
||||||
|
@ -20,17 +20,10 @@ interface CycleTransferStatsClientProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> = ({
|
export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> = ({ name, from, to, max, serverStats, className }) => {
|
||||||
name,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
max,
|
|
||||||
serverStats,
|
|
||||||
className,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error ShowNetTransfer is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -48,9 +41,7 @@ export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> =
|
|||||||
return (
|
return (
|
||||||
<div key={serverId}>
|
<div key={serverId}>
|
||||||
<section className="flex justify-between items-center">
|
<section className="flex justify-between items-center">
|
||||||
<div className="bg-green-600 w-fit text-white px-1.5 py-0.5 rounded-full text-[10px]">
|
<div className="bg-green-600 w-fit text-white px-1.5 py-0.5 rounded-full text-[10px]">{name}</div>
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
<span className="text-stone-600 dark:text-stone-400 text-xs">
|
<span className="text-stone-600 dark:text-stone-400 text-xs">
|
||||||
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()}
|
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
@ -63,21 +54,12 @@ export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> =
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<p className="text-xs text-end w-10 font-medium">{progress.toFixed(0)}%</p>
|
<p className="text-xs text-end w-10 font-medium">{progress.toFixed(0)}%</p>
|
||||||
<AnimatedCircularProgressBar
|
<AnimatedCircularProgressBar className="size-4 text-[0px]" max={100} min={0} value={progress} primaryColor="hsl(var(--chart-5))" />
|
||||||
className="size-4 text-[0px]"
|
|
||||||
max={100}
|
|
||||||
min={0}
|
|
||||||
value={progress}
|
|
||||||
primaryColor="hsl(var(--chart-5))"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="w-full bg-neutral-100 dark:bg-neutral-800 rounded-full overflow-hidden h-2.5 mt-2">
|
<div className="w-full bg-neutral-100 dark:bg-neutral-800 rounded-full overflow-hidden h-2.5 mt-2">
|
||||||
<div
|
<div className="bg-green-600 h-2.5 rounded-full" style={{ width: `${Math.min(progress, 100)}%` }} />
|
||||||
className="bg-green-600 h-2.5 rounded-full"
|
|
||||||
style={{ width: `${Math.min(progress, 100)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="flex justify-between items-center mt-2">
|
<section className="flex justify-between items-center mt-2">
|
||||||
|
@ -29,9 +29,7 @@ const Footer: React.FC = () => {
|
|||||||
<a href={"https://github.com/hamster1963/nezha-dash"} target="_blank">
|
<a href={"https://github.com/hamster1963/nezha-dash"} target="_blank">
|
||||||
nezha-dash
|
nezha-dash
|
||||||
</a>
|
</a>
|
||||||
{import.meta.env.VITE_GIT_HASH && (
|
{import.meta.env.VITE_GIT_HASH && <span className="ml-1">({import.meta.env.VITE_GIT_HASH})</span>}
|
||||||
<span className="ml-1">({import.meta.env.VITE_GIT_HASH})</span>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
@ -27,9 +27,7 @@ export default function GlobalMap({ serverList, now }: { serverList: NezhaServer
|
|||||||
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: { properties: { iso_a3_eh: string } }) => feature.properties.iso_a3_eh !== "")
|
||||||
(feature: { properties: { iso_a3_eh: string } }) => feature.properties.iso_a3_eh !== "",
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col gap-4 mt-8">
|
<section className="flex flex-col gap-4 mt-8">
|
||||||
@ -68,15 +66,7 @@ interface InteractiveMapProps {
|
|||||||
now: number
|
now: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InteractiveMap({
|
export function InteractiveMap({ countries, serverCounts, width, height, filteredFeatures, nezhaServerList, now }: InteractiveMapProps) {
|
||||||
countries,
|
|
||||||
serverCounts,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
filteredFeatures,
|
|
||||||
nezhaServerList,
|
|
||||||
now,
|
|
||||||
}: InteractiveMapProps) {
|
|
||||||
const { setTooltipData } = useTooltip()
|
const { setTooltipData } = useTooltip()
|
||||||
|
|
||||||
const projection = geoEquirectangular()
|
const projection = geoEquirectangular()
|
||||||
@ -88,13 +78,7 @@ export function InteractiveMap({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full aspect-[2/1]" onMouseLeave={() => setTooltipData(null)}>
|
<div className="relative w-full aspect-[2/1]" onMouseLeave={() => setTooltipData(null)}>
|
||||||
<svg
|
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} xmlns="http://www.w3.org/2000/svg" className="w-full h-auto">
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
viewBox={`0 0 ${width} ${height}`}
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="w-full h-auto"
|
|
||||||
>
|
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
|
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
|
||||||
<circle cx="1" cy="1" r="0.5" fill="currentColor" />
|
<circle cx="1" cy="1" r="0.5" fill="currentColor" />
|
||||||
@ -102,14 +86,7 @@ export function InteractiveMap({
|
|||||||
</defs>
|
</defs>
|
||||||
<g>
|
<g>
|
||||||
{/* Background rect to handle mouse events in empty areas */}
|
{/* Background rect to handle mouse events in empty areas */}
|
||||||
<rect
|
<rect x="0" y="0" width={width} height={height} fill="transparent" onMouseEnter={() => setTooltipData(null)} />
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
fill="transparent"
|
|
||||||
onMouseEnter={() => setTooltipData(null)}
|
|
||||||
/>
|
|
||||||
{filteredFeatures.map((feature, index) => {
|
{filteredFeatures.map((feature, index) => {
|
||||||
const isHighlighted = countries.includes(feature.properties.iso_a2_eh)
|
const isHighlighted = countries.includes(feature.properties.iso_a2_eh)
|
||||||
|
|
||||||
@ -132,9 +109,7 @@ export function InteractiveMap({
|
|||||||
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
|
const countryServers = nezhaServerList
|
||||||
.filter(
|
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode)
|
||||||
(server: NezhaServer) => server.country_code?.toUpperCase() === countryCode,
|
|
||||||
)
|
|
||||||
.map((server: NezhaServer) => ({
|
.map((server: NezhaServer) => ({
|
||||||
name: server.name,
|
name: server.name,
|
||||||
status: formatNezhaInfo(now, server).online,
|
status: formatNezhaInfo(now, server).online,
|
||||||
@ -154,9 +129,7 @@ export function InteractiveMap({
|
|||||||
{/* 渲染不在 filteredFeatures 中的国家标记点 */}
|
{/* 渲染不在 filteredFeatures 中的国家标记点 */}
|
||||||
{countries.map((countryCode) => {
|
{countries.map((countryCode) => {
|
||||||
// 检查该国家是否已经在 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
|
||||||
@ -174,10 +147,7 @@ export function InteractiveMap({
|
|||||||
key={countryCode}
|
key={countryCode}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
const countryServers = nezhaServerList
|
const countryServers = nezhaServerList
|
||||||
.filter(
|
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode.toUpperCase())
|
||||||
(server: NezhaServer) =>
|
|
||||||
server.country_code?.toUpperCase() === countryCode.toUpperCase(),
|
|
||||||
)
|
|
||||||
.map((server: NezhaServer) => ({
|
.map((server: NezhaServer) => ({
|
||||||
name: server.name,
|
name: server.name,
|
||||||
status: formatNezhaInfo(now, server).online,
|
status: formatNezhaInfo(now, server).online,
|
||||||
|
@ -11,17 +11,14 @@ export default function GroupSwitch({
|
|||||||
setCurrentTab: (tab: string) => void
|
setCurrentTab: (tab: string) => void
|
||||||
}) {
|
}) {
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error ShowNetTransfer is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
return (
|
return (
|
||||||
<div className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]">
|
<div className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", {
|
||||||
"flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800",
|
"bg-stone-100/50 dark:bg-stone-800/50": customBackgroundImage,
|
||||||
{
|
})}
|
||||||
"bg-stone-100/50 dark:bg-stone-800/50": customBackgroundImage,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{tabs.map((tab: string) => (
|
{tabs.map((tab: string) => (
|
||||||
<div
|
<div
|
||||||
@ -29,9 +26,7 @@ export default function GroupSwitch({
|
|||||||
onClick={() => setCurrentTab(tab)}
|
onClick={() => setCurrentTab(tab)}
|
||||||
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",
|
||||||
currentTab === tab
|
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500",
|
||||||
? "text-black dark:text-white"
|
|
||||||
: "text-stone-400 dark:text-stone-500",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{currentTab === tab && (
|
{currentTab === tab && (
|
||||||
|
@ -47,10 +47,7 @@ function Header() {
|
|||||||
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={() => navigate("/")} className="cursor-pointer flex items-center text-base font-medium">
|
||||||
onClick={() => navigate("/")}
|
|
||||||
className="cursor-pointer flex items-center text-base font-medium"
|
|
||||||
>
|
|
||||||
<div className="mr-1 flex flex-row items-center justify-start">
|
<div className="mr-1 flex flex-row items-center justify-start">
|
||||||
<img
|
<img
|
||||||
width={40}
|
width={40}
|
||||||
@ -60,11 +57,7 @@ function Header() {
|
|||||||
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!"
|
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isLoading ? (
|
{isLoading ? <Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" /> : siteName || "NEZHA"}
|
||||||
<Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" />
|
|
||||||
) : (
|
|
||||||
siteName || "NEZHA"
|
|
||||||
)}
|
|
||||||
<Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" />
|
<Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" />
|
||||||
<p className="hidden text-sm font-medium opacity-40 md:block">{customDesc}</p>
|
<p className="hidden text-sm font-medium opacity-40 md:block">{customDesc}</p>
|
||||||
</section>
|
</section>
|
||||||
@ -125,9 +118,7 @@ function Overview() {
|
|||||||
}, [])
|
}, [])
|
||||||
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)
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} 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 { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
@ -15,7 +10,7 @@ export function LanguageSwitcher() {
|
|||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error ShowNetTransfer is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
|
|
||||||
const locale = i18n.languages[0]
|
const locale = i18n.languages[0]
|
||||||
@ -47,11 +42,7 @@ export function LanguageSwitcher() {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
||||||
{localeItems.map((item) => (
|
{localeItems.map((item) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem key={item.code} onSelect={(e) => handleSelect(e, item.code)} className={locale === item.code ? "bg-muted gap-3" : ""}>
|
||||||
key={item.code}
|
|
||||||
onSelect={(e) => handleSelect(e, item.code)}
|
|
||||||
className={locale === item.code ? "bg-muted gap-3" : ""}
|
|
||||||
>
|
|
||||||
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />}
|
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
|
@ -27,9 +27,7 @@ const MapTooltip = memo(function MapTooltip() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">
|
<p className="font-medium">{tooltipData.country === "China" ? "Mainland China" : tooltipData.country}</p>
|
||||||
{tooltipData.country === "China" ? "Mainland China" : tooltipData.country}
|
|
||||||
</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("map.Servers")}
|
{tooltipData.count} {t("map.Servers")}
|
||||||
</p>
|
</p>
|
||||||
@ -43,11 +41,7 @@ const MapTooltip = memo(function MapTooltip() {
|
|||||||
>
|
>
|
||||||
{tooltipData.servers.map((server, index: number) => (
|
{tooltipData.servers.map((server, index: number) => (
|
||||||
<div key={index} className="flex items-center gap-1.5 py-0.5">
|
<div key={index} className="flex items-center gap-1.5 py-0.5">
|
||||||
<span
|
<span className={`w-1.5 h-1.5 shrink-0 rounded-full ${server.status ? "bg-green-500" : "bg-red-500"}`}></span>
|
||||||
className={`w-1.5 h-1.5 shrink-0 rounded-full ${
|
|
||||||
server.status ? "bg-green-500" : "bg-red-500"
|
|
||||||
}`}
|
|
||||||
></span>
|
|
||||||
<span className="text-xs">{server.name}</span>
|
<span className="text-xs">{server.name}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -1,14 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import {
|
import { ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
|
||||||
ChartConfig,
|
|
||||||
ChartContainer,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
} from "@/components/ui/chart"
|
|
||||||
import { fetchMonitor } from "@/lib/nezha-api"
|
import { fetchMonitor } from "@/lib/nezha-api"
|
||||||
import { formatTime } from "@/lib/utils"
|
import { formatTime } from "@/lib/utils"
|
||||||
import { formatRelativeTime } from "@/lib/utils"
|
import { formatRelativeTime } from "@/lib/utils"
|
||||||
@ -128,9 +121,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
|||||||
onClick={() => handleButtonClick(key)}
|
onClick={() => handleButtonClick(key)}
|
||||||
>
|
>
|
||||||
<span className="whitespace-nowrap text-xs text-muted-foreground">{key}</span>
|
<span className="whitespace-nowrap text-xs text-muted-foreground">{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</span>
|
||||||
{chartData[key][chartData[key].length - 1].avg_delay.toFixed(2)}ms
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
)),
|
)),
|
||||||
[chartDataKey, activeChart, chartData, handleButtonClick],
|
[chartDataKey, activeChart, chartData, handleButtonClick],
|
||||||
@ -138,16 +129,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
|||||||
|
|
||||||
const chartLines = useMemo(() => {
|
const chartLines = useMemo(() => {
|
||||||
if (activeChart !== defaultChart) {
|
if (activeChart !== defaultChart) {
|
||||||
return (
|
return <Line isAnimationActive={false} strokeWidth={1} type="linear" dot={false} dataKey="avg_delay" stroke={getColorByIndex(activeChart)} />
|
||||||
<Line
|
|
||||||
isAnimationActive={false}
|
|
||||||
strokeWidth={1}
|
|
||||||
type="linear"
|
|
||||||
dot={false}
|
|
||||||
dataKey="avg_delay"
|
|
||||||
stroke={getColorByIndex(activeChart)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return chartDataKey.map((key) => (
|
return chartDataKey.map((key) => (
|
||||||
<Line
|
<Line
|
||||||
@ -168,9 +150,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
|||||||
return activeChart === defaultChart ? formattedData : chartData[activeChart]
|
return activeChart === defaultChart ? formattedData : chartData[activeChart]
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = (
|
const data = (activeChart === defaultChart ? formattedData : chartData[activeChart]) as ResultItem[]
|
||||||
activeChart === defaultChart ? formattedData : chartData[activeChart]
|
|
||||||
) as ResultItem[]
|
|
||||||
|
|
||||||
const windowSize = 11 // 增加窗口大小以获取更好的统计效果
|
const windowSize = 11 // 增加窗口大小以获取更好的统计效果
|
||||||
const alpha = 0.3 // EWMA平滑因子
|
const alpha = 0.3 // EWMA平滑因子
|
||||||
@ -219,9 +199,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
|||||||
|
|
||||||
if (activeChart === defaultChart) {
|
if (activeChart === defaultChart) {
|
||||||
chartDataKey.forEach((key) => {
|
chartDataKey.forEach((key) => {
|
||||||
const values = window
|
const values = window.map((w) => w[key]).filter((v) => v !== undefined && v !== null) as number[]
|
||||||
.map((w) => w[key])
|
|
||||||
.filter((v) => v !== undefined && v !== null) as number[]
|
|
||||||
|
|
||||||
if (values.length > 0) {
|
if (values.length > 0) {
|
||||||
const processed = processValues(values)
|
const processed = processValues(values)
|
||||||
@ -237,9 +215,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const values = window
|
const values = window.map((w) => w.avg_delay).filter((v) => v !== undefined && v !== null) as number[]
|
||||||
.map((w) => w.avg_delay)
|
|
||||||
.filter((v) => v !== undefined && v !== null) as number[]
|
|
||||||
|
|
||||||
if (values.length > 0) {
|
if (values.length > 0) {
|
||||||
const processed = processValues(values)
|
const processed = processValues(values)
|
||||||
@ -263,9 +239,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-col items-stretch space-y-0 p-0 sm:flex-row">
|
<CardHeader className="flex flex-col items-stretch space-y-0 p-0 sm:flex-row">
|
||||||
<div className="flex flex-none flex-col justify-center gap-1 border-b px-6 py-4">
|
<div className="flex flex-none flex-col justify-center gap-1 border-b px-6 py-4">
|
||||||
<CardTitle className="flex flex-none items-center gap-0.5 text-md">
|
<CardTitle className="flex flex-none items-center gap-0.5 text-md">{serverName}</CardTitle>
|
||||||
{serverName}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="text-xs">
|
<CardDescription className="text-xs">
|
||||||
{chartDataKey.length} {t("monitor.monitorCount")}
|
{chartDataKey.length} {t("monitor.monitorCount")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
@ -291,13 +265,7 @@ export const NetworkChartClient = React.memo(function NetworkChart({
|
|||||||
interval={"preserveStartEnd"}
|
interval={"preserveStartEnd"}
|
||||||
tickFormatter={(value) => formatRelativeTime(value)}
|
tickFormatter={(value) => formatRelativeTime(value)}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis tickLine={false} axisLine={false} tickMargin={15} minTickGap={20} tickFormatter={(value) => `${value}ms`} />
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
tickMargin={15}
|
|
||||||
minTickGap={20}
|
|
||||||
tickFormatter={(value) => `${value}ms`}
|
|
||||||
/>
|
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
content={
|
content={
|
||||||
|
@ -15,52 +15,31 @@ export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData })
|
|||||||
return (
|
return (
|
||||||
<section className="flex gap-1 items-center flex-wrap mt-0.5">
|
<section className="flex gap-1 items-center flex-wrap mt-0.5">
|
||||||
{parsedData.planDataMod.bandwidth !== "" && (
|
{parsedData.planDataMod.bandwidth !== "" && (
|
||||||
<p
|
<p className={cn("text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
|
||||||
className={cn(
|
|
||||||
"text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{parsedData.planDataMod.bandwidth}
|
{parsedData.planDataMod.bandwidth}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{parsedData.planDataMod.trafficVol !== "" && (
|
{parsedData.planDataMod.trafficVol !== "" && (
|
||||||
<p
|
<p className={cn("text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
|
||||||
className={cn(
|
|
||||||
"text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{parsedData.planDataMod.trafficVol}
|
{parsedData.planDataMod.trafficVol}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{parsedData.planDataMod.IPv4 === "1" && (
|
{parsedData.planDataMod.IPv4 === "1" && (
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn("text-[9px] bg-purple-600 text-purple-200 dark:bg-purple-800 dark:text-purple-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}
|
||||||
"text-[9px] bg-purple-600 text-purple-200 dark:bg-purple-800 dark:text-purple-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
IPv4
|
IPv4
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{parsedData.planDataMod.IPv6 === "1" && (
|
{parsedData.planDataMod.IPv6 === "1" && (
|
||||||
<p
|
<p className={cn("text-[9px] bg-pink-600 text-pink-200 dark:bg-pink-800 dark:text-pink-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
|
||||||
className={cn(
|
|
||||||
"text-[9px] bg-pink-600 text-pink-200 dark:bg-pink-800 dark:text-pink-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
IPv6
|
IPv6
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{parsedData.planDataMod.networkRoute && (
|
{parsedData.planDataMod.networkRoute && (
|
||||||
<p
|
<p className={cn("text-[9px] bg-blue-600 text-blue-200 dark:bg-blue-800 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
|
||||||
className={cn(
|
|
||||||
"text-[9px] bg-blue-600 text-blue-200 dark:bg-blue-800 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{parsedData.planDataMod.networkRoute.split(",").map((route, index) => {
|
{parsedData.planDataMod.networkRoute.split(",").map((route, index) => {
|
||||||
return (
|
return route + (index === parsedData.planDataMod!.networkRoute.split(",").length - 1 ? "" : "|")
|
||||||
route +
|
|
||||||
(index === parsedData.planDataMod!.networkRoute.split(",").length - 1 ? "" : "|")
|
|
||||||
)
|
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -68,9 +47,7 @@ export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData })
|
|||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn("text-[9px] bg-stone-600 text-stone-200 dark:bg-stone-800 dark:text-stone-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}
|
||||||
"text-[9px] bg-stone-600 text-stone-200 dark:bg-stone-800 dark:text-stone-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{extra}
|
{extra}
|
||||||
</p>
|
</p>
|
||||||
|
@ -2,13 +2,7 @@ import { cn } from "@/lib/utils"
|
|||||||
|
|
||||||
import { Progress } from "./ui/progress"
|
import { Progress } from "./ui/progress"
|
||||||
|
|
||||||
export default function RemainPercentBar({
|
export default function RemainPercentBar({ value, className }: { value: number; className?: string }) {
|
||||||
value,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
value: number
|
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<Progress
|
<Progress
|
||||||
aria-label={"Server Usage Bar"}
|
aria-label={"Server Usage Bar"}
|
||||||
|
@ -14,24 +14,12 @@ import { Card } from "./ui/card"
|
|||||||
export default function ServerCard({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) {
|
export default function ServerCard({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const {
|
const { name, country_code, online, cpu, up, down, mem, stg, net_in_transfer, net_out_transfer, public_note } = formatNezhaInfo(now, serverInfo)
|
||||||
name,
|
|
||||||
country_code,
|
|
||||||
online,
|
|
||||||
cpu,
|
|
||||||
up,
|
|
||||||
down,
|
|
||||||
mem,
|
|
||||||
stg,
|
|
||||||
net_in_transfer,
|
|
||||||
net_out_transfer,
|
|
||||||
public_note,
|
|
||||||
} = formatNezhaInfo(now, serverInfo)
|
|
||||||
|
|
||||||
const showFlag = true
|
const showFlag = true
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error ShowNetTransfer is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
|
|
||||||
// @ts-expect-error ShowNetTransfer is a global variable
|
// @ts-expect-error ShowNetTransfer is a global variable
|
||||||
@ -41,30 +29,18 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
|
|||||||
|
|
||||||
return online ? (
|
return online ? (
|
||||||
<Card
|
<Card
|
||||||
className={cn(
|
className={cn("flex flex-col items-center justify-start gap-3 p-3 md:px-5 lg:flex-row cursor-pointer hover:bg-accent/50 transition-colors", {
|
||||||
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 lg:flex-row cursor-pointer hover:bg-accent/50 transition-colors",
|
"bg-card/50": customBackgroundImage,
|
||||||
{
|
})}
|
||||||
"bg-card/50": customBackgroundImage,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
onClick={() => navigate(`/server/${serverInfo.id}`)}
|
onClick={() => navigate(`/server/${serverInfo.id}`)}
|
||||||
>
|
>
|
||||||
<section
|
<section className={cn("grid items-center gap-2 lg:w-40")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
|
||||||
className={cn("grid items-center gap-2 lg:w-40")}
|
|
||||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
|
||||||
>
|
|
||||||
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
||||||
<div
|
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
|
||||||
className={cn("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 flex flex-col">
|
<div className="relative flex flex-col">
|
||||||
<p
|
<p className={cn("break-all font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}>{name}</p>
|
||||||
className={cn("break-all font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</p>
|
|
||||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -88,21 +64,13 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
|
|||||||
<div className={"flex w-14 flex-col"}>
|
<div className={"flex w-14 flex-col"}>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
|
<p className="text-xs text-muted-foreground">{t("serverCard.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 >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`}
|
||||||
? `${(up / 1024).toFixed(2)}G/s`
|
|
||||||
: up >= 1
|
|
||||||
? `${up.toFixed(2)}M/s`
|
|
||||||
: `${(up * 1024).toFixed(2)}K/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("serverCard.download")}</p>
|
<p className="text-xs text-muted-foreground">{t("serverCard.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 >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`}
|
||||||
? `${(down / 1024).toFixed(2)}G/s`
|
|
||||||
: down >= 1
|
|
||||||
? `${down.toFixed(2)}M/s`
|
|
||||||
: `${(down * 1024).toFixed(2)}K/s`}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -136,25 +104,13 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
|
|||||||
)}
|
)}
|
||||||
onClick={() => navigate(`/server/${serverInfo.id}`, { replace: true })}
|
onClick={() => navigate(`/server/${serverInfo.id}`, { replace: true })}
|
||||||
>
|
>
|
||||||
<section
|
<section className={cn("grid items-center gap-2 lg:w-40")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
|
||||||
className={cn("grid items-center gap-2 lg:w-40")}
|
|
||||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
|
||||||
>
|
|
||||||
<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("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
|
||||||
className={cn("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 flex flex-col">
|
<div className="relative flex flex-col">
|
||||||
<p
|
<p className={cn("break-all font-bold tracking-tight max-w-[108px]", showFlag ? "text-xs" : "text-sm")}>{name}</p>
|
||||||
className={cn(
|
|
||||||
"break-all font-bold tracking-tight max-w-[108px]",
|
|
||||||
showFlag ? "text-xs" : "text-sm",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</p>
|
|
||||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -12,35 +12,18 @@ import BillingInfo from "./billingInfo"
|
|||||||
import { Card } from "./ui/card"
|
import { Card } from "./ui/card"
|
||||||
import { Separator } from "./ui/separator"
|
import { Separator } from "./ui/separator"
|
||||||
|
|
||||||
export default function ServerCardInline({
|
export default function ServerCardInline({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) {
|
||||||
now,
|
|
||||||
serverInfo,
|
|
||||||
}: {
|
|
||||||
now: number
|
|
||||||
serverInfo: NezhaServer
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const {
|
const { name, country_code, online, cpu, up, down, mem, stg, platform, uptime, net_in_transfer, net_out_transfer, public_note } = formatNezhaInfo(
|
||||||
name,
|
now,
|
||||||
country_code,
|
serverInfo,
|
||||||
online,
|
)
|
||||||
cpu,
|
|
||||||
up,
|
|
||||||
down,
|
|
||||||
mem,
|
|
||||||
stg,
|
|
||||||
platform,
|
|
||||||
uptime,
|
|
||||||
net_in_transfer,
|
|
||||||
net_out_transfer,
|
|
||||||
public_note,
|
|
||||||
} = formatNezhaInfo(now, serverInfo)
|
|
||||||
|
|
||||||
const showFlag = true
|
const showFlag = true
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error ShowNetTransfer is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
|
|
||||||
const parsedData = parsePublicNote(public_note)
|
const parsedData = parsePublicNote(public_note)
|
||||||
@ -56,28 +39,13 @@ export default function ServerCardInline({
|
|||||||
)}
|
)}
|
||||||
onClick={() => navigate(`/server/${serverInfo.id}`)}
|
onClick={() => navigate(`/server/${serverInfo.id}`)}
|
||||||
>
|
>
|
||||||
<section
|
<section className={cn("grid items-center gap-2 lg:w-36")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
|
||||||
className={cn("grid items-center gap-2 lg:w-36")}
|
|
||||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
|
||||||
>
|
|
||||||
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
||||||
<div
|
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
|
||||||
className={cn(
|
|
||||||
"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 flex flex-col">
|
<div className="relative w-28 flex flex-col">
|
||||||
<p
|
<p className={cn("break-all font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}>{name}</p>
|
||||||
className={cn(
|
|
||||||
"break-all font-bold tracking-tight",
|
|
||||||
showFlag ? "text-xs " : "text-sm",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</p>
|
|
||||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -94,9 +62,7 @@ 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("serverCard.system")}</p>
|
<p className="text-xs text-muted-foreground">{t("serverCard.system")}</p>
|
||||||
<div className="flex items-center text-[10.5px] font-semibold">
|
<div className="flex items-center text-[10.5px] font-semibold">{platform.includes("Windows") ? "Windows" : GetOsName(platform)}</div>
|
||||||
{platform.includes("Windows") ? "Windows" : GetOsName(platform)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-20 flex-col"}>
|
<div className={"flex w-20 flex-col"}>
|
||||||
@ -125,34 +91,22 @@ export default function ServerCardInline({
|
|||||||
<div className={"flex w-16 flex-col"}>
|
<div className={"flex w-16 flex-col"}>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
|
<p className="text-xs text-muted-foreground">{t("serverCard.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 >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`}
|
||||||
? `${(up / 1024).toFixed(2)}G/s`
|
|
||||||
: up >= 1
|
|
||||||
? `${up.toFixed(2)}M/s`
|
|
||||||
: `${(up * 1024).toFixed(2)}K/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("serverCard.download")}</p>
|
<p className="text-xs text-muted-foreground">{t("serverCard.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` : up >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`}
|
||||||
? `${(down / 1024).toFixed(2)}G/s`
|
|
||||||
: up >= 1
|
|
||||||
? `${down.toFixed(2)}M/s`
|
|
||||||
: `${(down * 1024).toFixed(2)}K/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">{t("serverCard.totalUpload")}</p>
|
<p className="text-xs text-muted-foreground">{t("serverCard.totalUpload")}</p>
|
||||||
<div className="flex items-center text-xs font-semibold">
|
<div className="flex items-center text-xs font-semibold">{formatBytes(net_out_transfer)}</div>
|
||||||
{formatBytes(net_out_transfer)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-20 flex-col"}>
|
<div className={"flex w-20 flex-col"}>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.totalDownload")}</p>
|
<p className="text-xs text-muted-foreground">{t("serverCard.totalDownload")}</p>
|
||||||
<div className="flex items-center text-xs font-semibold">
|
<div className="flex items-center text-xs font-semibold">{formatBytes(net_in_transfer)}</div>
|
||||||
{formatBytes(net_in_transfer)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||||
@ -169,25 +123,13 @@ export default function ServerCardInline({
|
|||||||
)}
|
)}
|
||||||
onClick={() => navigate(`/server/${serverInfo.id}`)}
|
onClick={() => navigate(`/server/${serverInfo.id}`)}
|
||||||
>
|
>
|
||||||
<section
|
<section className={cn("grid items-center gap-2 w-40")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
|
||||||
className={cn("grid items-center gap-2 w-40")}
|
|
||||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
|
||||||
>
|
|
||||||
<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("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
|
||||||
className={cn("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 flex flex-col">
|
<div className="relative flex flex-col">
|
||||||
<p
|
<p className={cn("break-all font-bold w-28 tracking-tight", showFlag ? "text-xs" : "text-sm")}>{name}</p>
|
||||||
className={cn(
|
|
||||||
"break-all font-bold w-28 tracking-tight",
|
|
||||||
showFlag ? "text-xs" : "text-sm",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</p>
|
|
||||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -81,13 +81,9 @@ export default function ServerDetailChart({ server_id }: { server_id: string })
|
|||||||
<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">
|
||||||
<CpuChart now={nezhaWsData.now} data={server} />
|
<CpuChart now={nezhaWsData.now} data={server} />
|
||||||
{gpuStats.length >= 1 && gpuList.length === gpuStats.length ? (
|
{gpuStats.length >= 1 && gpuList.length === gpuStats.length ? (
|
||||||
gpuList.map((gpu, index) => (
|
gpuList.map((gpu, index) => <GpuChart now={nezhaWsData.now} gpuStat={gpuStats[index]} gpuName={gpu} key={index} />)
|
||||||
<GpuChart now={nezhaWsData.now} gpuStat={gpuStats[index]} gpuName={gpu} key={index} />
|
|
||||||
))
|
|
||||||
) : gpuStats.length > 0 ? (
|
) : gpuStats.length > 0 ? (
|
||||||
gpuStats.map((gpu, index) => (
|
gpuStats.map((gpu, index) => <GpuChart now={nezhaWsData.now} gpuStat={gpu} gpuName={`#${index + 1}`} key={index} />)
|
||||||
<GpuChart now={nezhaWsData.now} gpuStat={gpu} gpuName={`#${index + 1}`} key={index} />
|
|
||||||
))
|
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
@ -139,13 +135,7 @@ function GpuChart({ now, gpuStat, gpuName }: { now: number; gpuStat: number; gpu
|
|||||||
</section>
|
</section>
|
||||||
<section className="flex items-center gap-2">
|
<section className="flex items-center gap-2">
|
||||||
<p className="text-xs text-end w-10 font-medium">{gpuStat.toFixed(2)}%</p>
|
<p className="text-xs text-end w-10 font-medium">{gpuStat.toFixed(2)}%</p>
|
||||||
<AnimatedCircularProgressBar
|
<AnimatedCircularProgressBar className="size-3 text-[0px]" max={100} min={0} value={gpuStat} primaryColor="hsl(var(--chart-3))" />
|
||||||
className="size-3 text-[0px]"
|
|
||||||
max={100}
|
|
||||||
min={0}
|
|
||||||
value={gpuStat}
|
|
||||||
primaryColor="hsl(var(--chart-3))"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
|
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
|
||||||
@ -168,22 +158,8 @@ function GpuChart({ now, gpuStat, gpuName }: { now: number; gpuStat: number; gpu
|
|||||||
interval="preserveStartEnd"
|
interval="preserveStartEnd"
|
||||||
tickFormatter={(value) => formatRelativeTime(value)}
|
tickFormatter={(value) => formatRelativeTime(value)}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis tickLine={false} axisLine={false} mirror={true} tickMargin={-15} domain={[0, 100]} tickFormatter={(value) => `${value}%`} />
|
||||||
tickLine={false}
|
<Area isAnimationActive={false} dataKey="gpu" type="step" fill="hsl(var(--chart-3))" fillOpacity={0.3} stroke="hsl(var(--chart-3))" />
|
||||||
axisLine={false}
|
|
||||||
mirror={true}
|
|
||||||
tickMargin={-15}
|
|
||||||
domain={[0, 100]}
|
|
||||||
tickFormatter={(value) => `${value}%`}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
isAnimationActive={false}
|
|
||||||
dataKey="gpu"
|
|
||||||
type="step"
|
|
||||||
fill="hsl(var(--chart-3))"
|
|
||||||
fillOpacity={0.3}
|
|
||||||
stroke="hsl(var(--chart-3))"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</section>
|
</section>
|
||||||
@ -230,13 +206,7 @@ function CpuChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
<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">{cpu.toFixed(2)}%</p>
|
<p className="text-xs text-end w-10 font-medium">{cpu.toFixed(2)}%</p>
|
||||||
<AnimatedCircularProgressBar
|
<AnimatedCircularProgressBar className="size-3 text-[0px]" max={100} min={0} value={cpu} primaryColor="hsl(var(--chart-1))" />
|
||||||
className="size-3 text-[0px]"
|
|
||||||
max={100}
|
|
||||||
min={0}
|
|
||||||
value={cpu}
|
|
||||||
primaryColor="hsl(var(--chart-1))"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
|
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
|
||||||
@ -259,22 +229,8 @@ function CpuChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
interval="preserveStartEnd"
|
interval="preserveStartEnd"
|
||||||
tickFormatter={(value) => formatRelativeTime(value)}
|
tickFormatter={(value) => formatRelativeTime(value)}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis tickLine={false} axisLine={false} mirror={true} tickMargin={-15} domain={[0, 100]} tickFormatter={(value) => `${value}%`} />
|
||||||
tickLine={false}
|
<Area isAnimationActive={false} dataKey="cpu" type="step" fill="hsl(var(--chart-1))" fillOpacity={0.3} stroke="hsl(var(--chart-1))" />
|
||||||
axisLine={false}
|
|
||||||
mirror={true}
|
|
||||||
tickMargin={-15}
|
|
||||||
domain={[0, 100]}
|
|
||||||
tickFormatter={(value) => `${value}%`}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
isAnimationActive={false}
|
|
||||||
dataKey="cpu"
|
|
||||||
type="step"
|
|
||||||
fill="hsl(var(--chart-1))"
|
|
||||||
fillOpacity={0.3}
|
|
||||||
stroke="hsl(var(--chart-1))"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</section>
|
</section>
|
||||||
@ -404,26 +360,14 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className=" text-xs text-muted-foreground">{t("serverDetailChart.mem")}</p>
|
<p className=" text-xs text-muted-foreground">{t("serverDetailChart.mem")}</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AnimatedCircularProgressBar
|
<AnimatedCircularProgressBar className="size-3 text-[0px]" max={100} min={0} value={mem} primaryColor="hsl(var(--chart-8))" />
|
||||||
className="size-3 text-[0px]"
|
|
||||||
max={100}
|
|
||||||
min={0}
|
|
||||||
value={mem}
|
|
||||||
primaryColor="hsl(var(--chart-8))"
|
|
||||||
/>
|
|
||||||
<p className="text-xs font-medium">{mem.toFixed(0)}%</p>
|
<p className="text-xs font-medium">{mem.toFixed(0)}%</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className=" text-xs text-muted-foreground">{t("serverDetailChart.swap")}</p>
|
<p className=" text-xs text-muted-foreground">{t("serverDetailChart.swap")}</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AnimatedCircularProgressBar
|
<AnimatedCircularProgressBar className="size-3 text-[0px]" max={100} min={0} value={swap} primaryColor="hsl(var(--chart-10))" />
|
||||||
className="size-3 text-[0px]"
|
|
||||||
max={100}
|
|
||||||
min={0}
|
|
||||||
value={swap}
|
|
||||||
primaryColor="hsl(var(--chart-10))"
|
|
||||||
/>
|
|
||||||
<p className="text-xs font-medium">{swap.toFixed(0)}%</p>
|
<p className="text-xs font-medium">{swap.toFixed(0)}%</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -463,22 +407,8 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
interval="preserveStartEnd"
|
interval="preserveStartEnd"
|
||||||
tickFormatter={(value) => formatRelativeTime(value)}
|
tickFormatter={(value) => formatRelativeTime(value)}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis tickLine={false} axisLine={false} mirror={true} tickMargin={-15} domain={[0, 100]} tickFormatter={(value) => `${value}%`} />
|
||||||
tickLine={false}
|
<Area isAnimationActive={false} dataKey="mem" type="step" fill="hsl(var(--chart-8))" fillOpacity={0.3} stroke="hsl(var(--chart-8))" />
|
||||||
axisLine={false}
|
|
||||||
mirror={true}
|
|
||||||
tickMargin={-15}
|
|
||||||
domain={[0, 100]}
|
|
||||||
tickFormatter={(value) => `${value}%`}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
isAnimationActive={false}
|
|
||||||
dataKey="mem"
|
|
||||||
type="step"
|
|
||||||
fill="hsl(var(--chart-8))"
|
|
||||||
fillOpacity={0.3}
|
|
||||||
stroke="hsl(var(--chart-8))"
|
|
||||||
/>
|
|
||||||
<Area
|
<Area
|
||||||
isAnimationActive={false}
|
isAnimationActive={false}
|
||||||
dataKey="swap"
|
dataKey="swap"
|
||||||
@ -535,13 +465,7 @@ function DiskChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
<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">{disk.toFixed(0)}%</p>
|
<p className="text-xs text-end w-10 font-medium">{disk.toFixed(0)}%</p>
|
||||||
<AnimatedCircularProgressBar
|
<AnimatedCircularProgressBar className="size-3 text-[0px]" max={100} min={0} value={disk} primaryColor="hsl(var(--chart-5))" />
|
||||||
className="size-3 text-[0px]"
|
|
||||||
max={100}
|
|
||||||
min={0}
|
|
||||||
value={disk}
|
|
||||||
primaryColor="hsl(var(--chart-5))"
|
|
||||||
/>
|
|
||||||
</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.state.disk_used)} / {formatBytes(data.host.disk_total)}
|
{formatBytes(data.state.disk_used)} / {formatBytes(data.host.disk_total)}
|
||||||
@ -568,22 +492,8 @@ function DiskChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
interval="preserveStartEnd"
|
interval="preserveStartEnd"
|
||||||
tickFormatter={(value) => formatRelativeTime(value)}
|
tickFormatter={(value) => formatRelativeTime(value)}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis tickLine={false} axisLine={false} mirror={true} tickMargin={-15} domain={[0, 100]} tickFormatter={(value) => `${value}%`} />
|
||||||
tickLine={false}
|
<Area isAnimationActive={false} dataKey="disk" type="step" fill="hsl(var(--chart-5))" fillOpacity={0.3} stroke="hsl(var(--chart-5))" />
|
||||||
axisLine={false}
|
|
||||||
mirror={true}
|
|
||||||
tickMargin={-15}
|
|
||||||
domain={[0, 100]}
|
|
||||||
tickFormatter={(value) => `${value}%`}
|
|
||||||
/>
|
|
||||||
<Area
|
|
||||||
isAnimationActive={false}
|
|
||||||
dataKey="disk"
|
|
||||||
type="step"
|
|
||||||
fill="hsl(var(--chart-5))"
|
|
||||||
fillOpacity={0.3}
|
|
||||||
stroke="hsl(var(--chart-5))"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</section>
|
</section>
|
||||||
@ -643,11 +553,7 @@ function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]"></span>
|
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]"></span>
|
||||||
<p className="text-xs font-medium">
|
<p className="text-xs font-medium">
|
||||||
{up >= 1024
|
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : up >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`}
|
||||||
? `${(up / 1024).toFixed(2)}G/s`
|
|
||||||
: up >= 1
|
|
||||||
? `${up.toFixed(2)}M/s`
|
|
||||||
: `${(up * 1024).toFixed(2)}K/s`}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -656,11 +562,7 @@ function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
<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">
|
<p className="text-xs font-medium">
|
||||||
{down >= 1024
|
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : down >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`}
|
||||||
? `${(down / 1024).toFixed(2)}G/s`
|
|
||||||
: down >= 1
|
|
||||||
? `${down.toFixed(2)}M/s`
|
|
||||||
: `${(down * 1024).toFixed(2)}K/s`}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -697,22 +599,8 @@ function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
domain={[1, maxDownload]}
|
domain={[1, maxDownload]}
|
||||||
tickFormatter={(value) => `${value.toFixed(0)}M/s`}
|
tickFormatter={(value) => `${value.toFixed(0)}M/s`}
|
||||||
/>
|
/>
|
||||||
<Line
|
<Line isAnimationActive={false} dataKey="upload" type="linear" stroke="hsl(var(--chart-1))" strokeWidth={1} dot={false} />
|
||||||
isAnimationActive={false}
|
<Line isAnimationActive={false} dataKey="download" type="linear" stroke="hsl(var(--chart-4))" strokeWidth={1} dot={false} />
|
||||||
dataKey="upload"
|
|
||||||
type="linear"
|
|
||||||
stroke="hsl(var(--chart-1))"
|
|
||||||
strokeWidth={1}
|
|
||||||
dot={false}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
isAnimationActive={false}
|
|
||||||
dataKey="download"
|
|
||||||
type="linear"
|
|
||||||
stroke="hsl(var(--chart-4))"
|
|
||||||
strokeWidth={1}
|
|
||||||
dot={false}
|
|
||||||
/>
|
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</section>
|
</section>
|
||||||
@ -796,30 +684,9 @@ function ConnectChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
interval="preserveStartEnd"
|
interval="preserveStartEnd"
|
||||||
tickFormatter={(value) => formatRelativeTime(value)}
|
tickFormatter={(value) => formatRelativeTime(value)}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis tickLine={false} axisLine={false} mirror={true} tickMargin={-15} type="number" interval="preserveStartEnd" />
|
||||||
tickLine={false}
|
<Line isAnimationActive={false} dataKey="tcp" type="linear" stroke="hsl(var(--chart-1))" strokeWidth={1} dot={false} />
|
||||||
axisLine={false}
|
<Line isAnimationActive={false} dataKey="udp" type="linear" stroke="hsl(var(--chart-4))" strokeWidth={1} dot={false} />
|
||||||
mirror={true}
|
|
||||||
tickMargin={-15}
|
|
||||||
type="number"
|
|
||||||
interval="preserveStartEnd"
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
isAnimationActive={false}
|
|
||||||
dataKey="tcp"
|
|
||||||
type="linear"
|
|
||||||
stroke="hsl(var(--chart-1))"
|
|
||||||
strokeWidth={1}
|
|
||||||
dot={false}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
isAnimationActive={false}
|
|
||||||
dataKey="udp"
|
|
||||||
type="linear"
|
|
||||||
stroke="hsl(var(--chart-4))"
|
|
||||||
strokeWidth={1}
|
|
||||||
dot={false}
|
|
||||||
/>
|
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</section>
|
</section>
|
||||||
|
@ -70,13 +70,10 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
|
|||||||
<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("serverDetail.status")}</p>
|
<p className="text-xs text-muted-foreground">{t("serverDetail.status")}</p>
|
||||||
<Badge
|
<Badge
|
||||||
className={cn(
|
className={cn("text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white", {
|
||||||
"text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white",
|
" bg-green-800": online,
|
||||||
{
|
" bg-red-600": !online,
|
||||||
" bg-green-800": online,
|
})}
|
||||||
" bg-red-600": !online,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{online ? t("serverDetail.online") : t("serverDetail.offline")}
|
{online ? t("serverDetail.online") : t("serverDetail.offline")}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -146,9 +143,7 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
|
|||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.region")}</p>
|
<p className="text-xs text-muted-foreground">{t("serverDetail.region")}</p>
|
||||||
<section className="flex items-start gap-1">
|
<section className="flex items-start gap-1">
|
||||||
<div className="text-xs text-start">{country_code?.toUpperCase()}</div>
|
<div className="text-xs text-start">{country_code?.toUpperCase()}</div>
|
||||||
{country_code && (
|
{country_code && <ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />}
|
||||||
<ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -235,15 +230,12 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
|
|||||||
<section className="flex flex-wrap gap-2 ml-1.5">
|
<section className="flex flex-wrap gap-2 ml-1.5">
|
||||||
<Accordion type="single" collapsible className="w-fit">
|
<Accordion type="single" collapsible className="w-fit">
|
||||||
<AccordionItem value="item-1" className="border-none">
|
<AccordionItem value="item-1" className="border-none">
|
||||||
<AccordionTrigger className="text-xs py-0 text-muted-foreground font-normal">
|
<AccordionTrigger className="text-xs py-0 text-muted-foreground font-normal">{t("serverDetail.temperature")}</AccordionTrigger>
|
||||||
{t("serverDetail.temperature")}
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="pb-0">
|
<AccordionContent className="pb-0">
|
||||||
<section className="flex items-start flex-wrap gap-2">
|
<section className="flex items-start flex-wrap gap-2">
|
||||||
{server?.state.temperatures.map((item, index) => (
|
{server?.state.temperatures.map((item, index) => (
|
||||||
<div className="text-xs flex items-center" key={index}>
|
<div className="text-xs flex items-center" key={index}>
|
||||||
<p className="font-semibold">{item.Name}</p>: {item.Temperature.toFixed(2)}{" "}
|
<p className="font-semibold">{item.Name}</p>: {item.Temperature.toFixed(2)} °C
|
||||||
°C
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
@ -259,9 +251,7 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
|
|||||||
<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">{t("serverDetail.lastActive")}</p>
|
<p className="text-xs text-muted-foreground">{t("serverDetail.lastActive")}</p>
|
||||||
<div className="text-xs">
|
<div className="text-xs">{last_active_time_string ? last_active_time_string : "N/A"}</div>
|
||||||
{last_active_time_string ? last_active_time_string : "N/A"}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -2,13 +2,7 @@ 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, className }: { country_code: string; className?: string }) {
|
||||||
country_code,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
country_code: string
|
|
||||||
className?: string
|
|
||||||
}) {
|
|
||||||
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false)
|
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -37,11 +31,7 @@ export default function ServerFlag({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={cn("text-[12px] text-muted-foreground", className)}>
|
<span className={cn("text-[12px] text-muted-foreground", className)}>
|
||||||
{!supportsEmojiFlags ? (
|
{!supportsEmojiFlags ? <span className={`fi fi-${country_code}`} /> : getUnicodeFlagIcon(country_code)}
|
||||||
<span className={`fi fi-${country_code}`} />
|
|
||||||
) : (
|
|
||||||
getUnicodeFlagIcon(country_code)
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -15,15 +15,7 @@ type ServerOverviewProps = {
|
|||||||
downSpeed: number
|
downSpeed: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ServerOverview({
|
export default function ServerOverview({ online, offline, total, up, down, upSpeed, downSpeed }: ServerOverviewProps) {
|
||||||
online,
|
|
||||||
offline,
|
|
||||||
total,
|
|
||||||
up,
|
|
||||||
down,
|
|
||||||
upSpeed,
|
|
||||||
downSpeed,
|
|
||||||
}: ServerOverviewProps) {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { status, setStatus } = useStatus()
|
const { status, setStatus } = useStatus()
|
||||||
|
|
||||||
@ -34,7 +26,7 @@ export default function ServerOverview({
|
|||||||
const customIllustration = window.CustomIllustration || "/animated-man.webp"
|
const customIllustration = window.CustomIllustration || "/animated-man.webp"
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error ShowNetTransfer is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -76,9 +68,7 @@ export default function ServerOverview({
|
|||||||
>
|
>
|
||||||
<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("serverOverview.onlineServers")}</p>
|
||||||
{t("serverOverview.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>
|
||||||
@ -106,9 +96,7 @@ export default function ServerOverview({
|
|||||||
>
|
>
|
||||||
<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("serverOverview.offlineServers")}</p>
|
||||||
{t("serverOverview.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>
|
||||||
@ -130,12 +118,8 @@ export default function ServerOverview({
|
|||||||
<p className="text-sm font-medium md:text-base">{t("serverOverview.network")}</p>
|
<p className="text-sm font-medium md:text-base">{t("serverOverview.network")}</p>
|
||||||
</div>
|
</div>
|
||||||
<section className="flex items-start flex-row z-10 pr-0 gap-1">
|
<section className="flex items-start flex-row z-10 pr-0 gap-1">
|
||||||
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">
|
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">↑{formatBytes(up)}</p>
|
||||||
↑{formatBytes(up)}
|
<p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">↓{formatBytes(down)}</p>
|
||||||
</p>
|
|
||||||
<p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">
|
|
||||||
↓{formatBytes(down)}
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
<section className="flex flex-col sm:flex-row -mr-1 sm:items-center items-start gap-1">
|
<section className="flex flex-col sm:flex-row -mr-1 sm:items-center items-start gap-1">
|
||||||
<p className="text-[11px] flex items-center text-nowrap font-semibold">
|
<p className="text-[11px] flex items-center text-nowrap font-semibold">
|
||||||
|
@ -26,14 +26,10 @@ export const ServiceTracker: React.FC = () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const totalUp = serviceData.up.reduce((a, b) => a + b, 0)
|
const totalUp = serviceData.up.reduce((a, b) => a + b, 0)
|
||||||
const totalChecks =
|
const totalChecks = serviceData.up.reduce((a, b) => a + b, 0) + serviceData.down.reduce((a, b) => a + b, 0)
|
||||||
serviceData.up.reduce((a, b) => a + b, 0) + serviceData.down.reduce((a, b) => a + b, 0)
|
|
||||||
const uptime = (totalUp / totalChecks) * 100
|
const uptime = (totalUp / totalChecks) * 100
|
||||||
|
|
||||||
const avgDelay =
|
const avgDelay = serviceData.delay.length > 0 ? serviceData.delay.reduce((a, b) => a + b, 0) / serviceData.delay.length : 0
|
||||||
serviceData.delay.length > 0
|
|
||||||
? serviceData.delay.reduce((a, b) => a + b, 0) / serviceData.delay.length
|
|
||||||
: 0
|
|
||||||
|
|
||||||
return { days, uptime, avgDelay }
|
return { days, uptime, avgDelay }
|
||||||
}
|
}
|
||||||
@ -67,15 +63,7 @@ export const ServiceTracker: React.FC = () => {
|
|||||||
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
|
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
|
||||||
{Object.entries(serviceData.data.services).map(([name, data]) => {
|
{Object.entries(serviceData.data.services).map(([name, data]) => {
|
||||||
const { days, uptime, avgDelay } = processServiceData(data)
|
const { days, uptime, avgDelay } = processServiceData(data)
|
||||||
return (
|
return <ServiceTrackerClient key={name} days={days} title={data.service_name} uptime={uptime} avgDelay={avgDelay} />
|
||||||
<ServiceTrackerClient
|
|
||||||
key={name}
|
|
||||||
days={days}
|
|
||||||
title={data.service_name}
|
|
||||||
uptime={uptime}
|
|
||||||
avgDelay={avgDelay}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
})}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
@ -15,16 +15,10 @@ interface ServiceTrackerProps {
|
|||||||
avgDelay?: number
|
avgDelay?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
|
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, className, title, uptime = 100, avgDelay = 0 }) => {
|
||||||
days,
|
|
||||||
className,
|
|
||||||
title,
|
|
||||||
uptime = 100,
|
|
||||||
avgDelay = 0,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error ShowNetTransfer is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -44,9 +38,7 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
|
|||||||
<span className="font-medium text-sm">{title}</span>
|
<span className="font-medium text-sm">{title}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-stone-600 dark:text-stone-400 font-medium text-sm">
|
<span className="text-stone-600 dark:text-stone-400 font-medium text-sm">{avgDelay.toFixed(0)}ms</span>
|
||||||
{avgDelay.toFixed(0)}ms
|
|
||||||
</span>
|
|
||||||
<Separator className="h-4 mx-0" orientation="vertical" />
|
<Separator className="h-4 mx-0" orientation="vertical" />
|
||||||
<span className="text-green-600 font-medium text-sm">
|
<span className="text-green-600 font-medium text-sm">
|
||||||
{uptime.toFixed(1)}% {t("serviceTracker.uptime")}
|
{uptime.toFixed(1)}% {t("serviceTracker.uptime")}
|
||||||
@ -58,10 +50,7 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
|
|||||||
{days.map((day, index) => (
|
{days.map((day, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn("flex-1 h-6 rounded-[5px] transition-colors", day.completed ? "bg-green-600" : "bg-red-500/60")}
|
||||||
"flex-1 h-6 rounded-[5px] transition-colors",
|
|
||||||
day.completed ? "bg-green-600" : "bg-red-500/60",
|
|
||||||
)}
|
|
||||||
title={day.date ? day.date.toLocaleDateString() : `Day ${index + 1}`}
|
title={day.date ? day.date.toLocaleDateString() : `Day ${index + 1}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -2,28 +2,17 @@ import { cn } from "@/lib/utils"
|
|||||||
import { m } from "framer-motion"
|
import { m } from "framer-motion"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
export default function TabSwitch({
|
export default function TabSwitch({ tabs, currentTab, setCurrentTab }: { tabs: string[]; currentTab: string; setCurrentTab: (tab: string) => void }) {
|
||||||
tabs,
|
|
||||||
currentTab,
|
|
||||||
setCurrentTab,
|
|
||||||
}: {
|
|
||||||
tabs: string[]
|
|
||||||
currentTab: string
|
|
||||||
setCurrentTab: (tab: string) => void
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error ShowNetTransfer is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
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
|
<div
|
||||||
className={cn(
|
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", {
|
||||||
"flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800",
|
"bg-stone-100/50 dark:bg-stone-800/50": customBackgroundImage,
|
||||||
{
|
})}
|
||||||
"bg-stone-100/50 dark:bg-stone-800/50": customBackgroundImage,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{tabs.map((tab: string) => (
|
{tabs.map((tab: string) => (
|
||||||
<div
|
<div
|
||||||
@ -31,9 +20,7 @@ export default function TabSwitch({
|
|||||||
onClick={() => setCurrentTab(tab)}
|
onClick={() => setCurrentTab(tab)}
|
||||||
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",
|
||||||
currentTab === tab
|
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500",
|
||||||
? "text-black dark:text-white"
|
|
||||||
: "text-stone-400 dark:text-stone-500",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{currentTab === tab && (
|
{currentTab === tab && (
|
||||||
|
@ -21,9 +21,7 @@ const initialState: ThemeProviderState = {
|
|||||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
||||||
|
|
||||||
export function ThemeProvider({ children, storageKey = "vite-ui-theme" }: ThemeProviderProps) {
|
export function ThemeProvider({ children, storageKey = "vite-ui-theme" }: ThemeProviderProps) {
|
||||||
const [theme, setTheme] = useState<Theme>(
|
const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || "system")
|
||||||
() => (localStorage.getItem(storageKey) as Theme) || "system",
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = window.document.documentElement
|
const root = window.document.documentElement
|
||||||
@ -31,9 +29,7 @@ export function ThemeProvider({ children, storageKey = "vite-ui-theme" }: ThemeP
|
|||||||
root.classList.remove("light", "dark")
|
root.classList.remove("light", "dark")
|
||||||
|
|
||||||
if (theme === "system") {
|
if (theme === "system") {
|
||||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
||||||
? "dark"
|
|
||||||
: "light"
|
|
||||||
|
|
||||||
root.classList.add(systemTheme)
|
root.classList.add(systemTheme)
|
||||||
const themeColor = systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
|
const themeColor = systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
import { Theme } from "@/components/ThemeProvider"
|
import { Theme } from "@/components/ThemeProvider"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} 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"
|
||||||
@ -18,7 +13,7 @@ export function ModeToggle() {
|
|||||||
const { setTheme, theme } = useTheme()
|
const { setTheme, theme } = useTheme()
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error ShowNetTransfer is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
|
|
||||||
const handleSelect = (e: Event, newTheme: Theme) => {
|
const handleSelect = (e: Event, newTheme: Theme) => {
|
||||||
@ -42,24 +37,15 @@ export function ModeToggle() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "light" })} onSelect={(e) => handleSelect(e, "light")}>
|
||||||
className={cn({ "gap-3 bg-muted": theme === "light" })}
|
|
||||||
onSelect={(e) => handleSelect(e, "light")}
|
|
||||||
>
|
|
||||||
{t("theme.light")}
|
{t("theme.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" })} onSelect={(e) => handleSelect(e, "dark")}>
|
||||||
className={cn({ "gap-3 bg-muted": theme === "dark" })}
|
|
||||||
onSelect={(e) => handleSelect(e, "dark")}
|
|
||||||
>
|
|
||||||
{t("theme.dark")}
|
{t("theme.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" })} onSelect={(e) => handleSelect(e, "system")}>
|
||||||
className={cn({ "gap-3 bg-muted": theme === "system" })}
|
|
||||||
onSelect={(e) => handleSelect(e, "system")}
|
|
||||||
>
|
|
||||||
{t("theme.system")}
|
{t("theme.system")}
|
||||||
{theme === "system" && <CheckCircleIcon className="size-4" />}
|
{theme === "system" && <CheckCircleIcon className="size-4" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -24,12 +24,8 @@ export default function BillingInfo({ parsedData }: { parsedData: PublicNoteData
|
|||||||
|
|
||||||
return daysLeftObject.days >= 0 ? (
|
return daysLeftObject.days >= 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className={cn("text-[10px] text-muted-foreground")}>
|
<div className={cn("text-[10px] text-muted-foreground")}>剩余时间: {isNeverExpire ? "永久" : daysLeftObject.days + "天"}</div>
|
||||||
剩余时间: {isNeverExpire ? "永久" : daysLeftObject.days + "天"}
|
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? (
|
||||||
</div>
|
|
||||||
{parsedData.billingDataMod.amount &&
|
|
||||||
parsedData.billingDataMod.amount !== "0" &&
|
|
||||||
parsedData.billingDataMod.amount !== "-1" ? (
|
|
||||||
<p className={cn("text-[10px] text-muted-foreground ")}>
|
<p className={cn("text-[10px] text-muted-foreground ")}>
|
||||||
价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
|
价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
|
||||||
</p>
|
</p>
|
||||||
@ -42,12 +38,8 @@ export default function BillingInfo({ parsedData }: { parsedData: PublicNoteData
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
|
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>已过期: {daysLeftObject.days * -1} 天</p>
|
||||||
已过期: {daysLeftObject.days * -1} 天
|
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? (
|
||||||
</p>
|
|
||||||
{parsedData.billingDataMod.amount &&
|
|
||||||
parsedData.billingDataMod.amount !== "0" &&
|
|
||||||
parsedData.billingDataMod.amount !== "-1" ? (
|
|
||||||
<p className={cn("text-[10px] text-muted-foreground ")}>
|
<p className={cn("text-[10px] text-muted-foreground ")}>
|
||||||
价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
|
价格: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
|
||||||
</p>
|
</p>
|
||||||
|
@ -8,9 +8,7 @@ const Accordion = AccordionPrimitive.Root
|
|||||||
const AccordionItem = React.forwardRef<
|
const AccordionItem = React.forwardRef<
|
||||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />)
|
||||||
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
|
|
||||||
))
|
|
||||||
AccordionItem.displayName = "AccordionItem"
|
AccordionItem.displayName = "AccordionItem"
|
||||||
|
|
||||||
const AccordionTrigger = React.forwardRef<
|
const AccordionTrigger = React.forwardRef<
|
||||||
|
@ -8,13 +8,7 @@ interface Props {
|
|||||||
primaryColor?: string
|
primaryColor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AnimatedCircularProgressBar({
|
export default function AnimatedCircularProgressBar({ max = 100, min = 0, value = 0, primaryColor, className }: Props) {
|
||||||
max = 100,
|
|
||||||
min = 0,
|
|
||||||
value = 0,
|
|
||||||
primaryColor,
|
|
||||||
className,
|
|
||||||
}: 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
|
||||||
@ -52,10 +46,8 @@ export default function AnimatedCircularProgressBar({
|
|||||||
{
|
{
|
||||||
"--stroke-percent": 90 - currentPercent,
|
"--stroke-percent": 90 - currentPercent,
|
||||||
"--offset-factor-secondary": "calc(1 - var(--offset-factor))",
|
"--offset-factor-secondary": "calc(1 - var(--offset-factor))",
|
||||||
strokeDasharray:
|
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
||||||
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
transform: "rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
|
||||||
transform:
|
|
||||||
"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: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
@ -77,13 +69,10 @@ export default function AnimatedCircularProgressBar({
|
|||||||
{
|
{
|
||||||
"--stroke-primary-color": primaryColor,
|
"--stroke-primary-color": primaryColor,
|
||||||
"--stroke-percent": currentPercent,
|
"--stroke-percent": currentPercent,
|
||||||
strokeDasharray:
|
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
||||||
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
transition: "var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
|
||||||
transition:
|
|
||||||
"var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
|
|
||||||
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: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,8 @@ const badgeVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
default: "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: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
destructive:
|
|
||||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
||||||
outline: "text-foreground",
|
outline: "text-foreground",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -21,9 +19,7 @@ const badgeVariants = cva(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
|
||||||
VariantProps<typeof badgeVariants> {}
|
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
@ -29,20 +29,14 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
||||||
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 <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||||
return (
|
})
|
||||||
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
Button.displayName = "Button"
|
Button.displayName = "Button"
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants }
|
||||||
|
@ -1,58 +1,38 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||||
({ className, ...props }, ref) => (
|
<div
|
||||||
<div
|
ref={ref}
|
||||||
ref={ref}
|
className={cn("rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none", className)}
|
||||||
className={cn(
|
{...props}
|
||||||
"rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
|
/>
|
||||||
className,
|
))
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)
|
|
||||||
Card.displayName = "Card"
|
Card.displayName = "Card"
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||||
({ className, ...props }, ref) => (
|
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
))
|
||||||
),
|
|
||||||
)
|
|
||||||
CardHeader.displayName = "CardHeader"
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => (
|
||||||
({ className, ...props }, ref) => (
|
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
|
||||||
<h3
|
))
|
||||||
ref={ref}
|
|
||||||
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)
|
|
||||||
CardTitle.displayName = "CardTitle"
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<
|
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(({ className, ...props }, ref) => (
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||||
))
|
))
|
||||||
CardDescription.displayName = "CardDescription"
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||||
({ className, ...props }, ref) => (
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
))
|
||||||
),
|
|
||||||
)
|
|
||||||
CardContent.displayName = "CardContent"
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
||||||
({ className, ...props }, ref) => (
|
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||||
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
))
|
||||||
),
|
|
||||||
)
|
|
||||||
CardFooter.displayName = "CardFooter"
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
|
@ -9,10 +9,7 @@ export type ChartConfig = {
|
|||||||
[k in string]: {
|
[k in string]: {
|
||||||
label?: React.ReactNode
|
label?: React.ReactNode
|
||||||
icon?: React.ComponentType
|
icon?: React.ComponentType
|
||||||
} & (
|
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> })
|
||||||
| { color?: string; theme?: never }
|
|
||||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartContextProps = {
|
type ChartContextProps = {
|
||||||
@ -130,15 +127,10 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
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" ? config[label as keyof typeof config]?.label || label : itemConfig?.label
|
||||||
!labelKey && typeof label === "string"
|
|
||||||
? config[label as keyof typeof config]?.label || label
|
|
||||||
: itemConfig?.label
|
|
||||||
|
|
||||||
if (labelFormatter) {
|
if (labelFormatter) {
|
||||||
return (
|
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
|
||||||
<div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@ -186,16 +178,12 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
) : (
|
) : (
|
||||||
!hideIndicator && (
|
!hideIndicator && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
|
||||||
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
"h-2.5 w-2.5": indicator === "dot",
|
||||||
{
|
"w-1": indicator === "line",
|
||||||
"h-2.5 w-2.5": indicator === "dot",
|
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
|
||||||
"w-1": indicator === "line",
|
"my-0.5": nestLabel && indicator === "dashed",
|
||||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
})}
|
||||||
indicator === "dashed",
|
|
||||||
"my-0.5": nestLabel && indicator === "dashed",
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--color-bg": indicatorColor,
|
"--color-bg": indicatorColor,
|
||||||
@ -205,23 +193,12 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<div
|
<div className={cn("flex flex-1 justify-between leading-none", nestLabel ? "items-end" : "items-center")}>
|
||||||
className={cn(
|
|
||||||
"flex flex-1 justify-between leading-none",
|
|
||||||
nestLabel ? "items-end" : "items-center",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
{nestLabel ? tooltipLabel : null}
|
{nestLabel ? tooltipLabel : null}
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
|
||||||
{itemConfig?.label || item.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{item.value && (
|
{item.value && <span className="font-mono font-medium tabular-nums text-foreground">{item.value.toLocaleString()}</span>}
|
||||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
|
||||||
{item.value.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -252,25 +229,13 @@ const ChartLegendContent = React.forwardRef<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={ref} className={cn("flex flex-wrap items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}>
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex flex-wrap items-center justify-center gap-4",
|
|
||||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
||||||
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} className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}>
|
||||||
key={item.value}
|
|
||||||
className={cn(
|
|
||||||
"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 />
|
||||||
) : (
|
) : (
|
||||||
@ -296,31 +261,17 @@ function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key:
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const payloadPayload =
|
const payloadPayload = "payload" in payload && typeof payload.payload === "object" && payload.payload !== null ? payload.payload : undefined
|
||||||
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
|
|
||||||
? payload.payload
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
let configLabelKey: string = key
|
let configLabelKey: string = key
|
||||||
|
|
||||||
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
|
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
|
||||||
configLabelKey = payload[key as keyof typeof payload] as string
|
configLabelKey = payload[key as keyof typeof payload] as string
|
||||||
} else if (
|
} else if (payloadPayload && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === "string") {
|
||||||
payloadPayload &&
|
|
||||||
key in payloadPayload &&
|
|
||||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
|
||||||
) {
|
|
||||||
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string
|
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string
|
||||||
}
|
}
|
||||||
|
|
||||||
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
|
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle }
|
||||||
ChartContainer,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartStyle,
|
|
||||||
}
|
|
||||||
|
@ -3,23 +3,22 @@ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|||||||
import { Check } from "lucide-react"
|
import { Check } from "lucide-react"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
const Checkbox = React.forwardRef<React.ElementRef<typeof CheckboxPrimitive.Root>, React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>>(
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
({ className, ...props }, ref) => (
|
||||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
<CheckboxPrimitive.Root
|
||||||
>(({ className, ...props }, ref) => (
|
ref={ref}
|
||||||
<CheckboxPrimitive.Root
|
className={cn(
|
||||||
ref={ref}
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
className={cn(
|
className,
|
||||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
)}
|
||||||
className,
|
{...props}
|
||||||
)}
|
>
|
||||||
{...props}
|
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
||||||
>
|
<Check className="h-4 w-4" />
|
||||||
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
</CheckboxPrimitive.Indicator>
|
||||||
<Check className="h-4 w-4" />
|
</CheckboxPrimitive.Root>
|
||||||
</CheckboxPrimitive.Indicator>
|
),
|
||||||
</CheckboxPrimitive.Root>
|
)
|
||||||
))
|
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||||
|
|
||||||
export { Checkbox }
|
export { Checkbox }
|
||||||
|
@ -56,46 +56,21 @@ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
|
|||||||
DialogHeader.displayName = "DialogHeader"
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
||||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
DialogFooter.displayName = "DialogFooter"
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
const DialogTitle = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Title>, React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>>(
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
({ className, ...props }, ref) => (
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
<DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
|
||||||
>(({ className, ...props }, ref) => (
|
),
|
||||||
<DialogPrimitive.Title
|
)
|
||||||
ref={ref}
|
|
||||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
const DialogDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => <DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />)
|
||||||
<DialogPrimitive.Description
|
|
||||||
ref={ref}
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
export {
|
export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription }
|
||||||
Dialog,
|
|
||||||
DialogPortal,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogClose,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogFooter,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
}
|
|
||||||
|
@ -138,24 +138,14 @@ const DropdownMenuLabel = React.forwardRef<
|
|||||||
inset?: boolean
|
inset?: boolean
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)} {...props} />
|
||||||
ref={ref}
|
|
||||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
|
||||||
{...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>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => <DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />)
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
@ -3,21 +3,19 @@ import * as React from "react"
|
|||||||
|
|
||||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
|
||||||
({ className, type, ...props }, ref) => {
|
return (
|
||||||
return (
|
<input
|
||||||
<input
|
type={type}
|
||||||
type={type}
|
className={cn(
|
||||||
className={cn(
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
className,
|
||||||
className,
|
)}
|
||||||
)}
|
ref={ref}
|
||||||
ref={ref}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
Input.displayName = "Input"
|
Input.displayName = "Input"
|
||||||
|
|
||||||
export { Input }
|
export { Input }
|
||||||
|
@ -3,16 +3,12 @@ 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> & VariantProps<typeof labelVariants>
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />)
|
||||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
|
||||||
))
|
|
||||||
Label.displayName = LabelPrimitive.Root.displayName
|
Label.displayName = LabelPrimitive.Root.displayName
|
||||||
|
|
||||||
export { Label }
|
export { Label }
|
||||||
|
@ -8,11 +8,7 @@ const Progress = React.forwardRef<
|
|||||||
indicatorClassName?: string
|
indicatorClassName?: string
|
||||||
}
|
}
|
||||||
>(({ className, value, indicatorClassName, ...props }, ref) => (
|
>(({ className, value, indicatorClassName, ...props }, ref) => (
|
||||||
<ProgressPrimitive.Root
|
<ProgressPrimitive.Root ref={ref} className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)} {...props}>
|
||||||
ref={ref}
|
|
||||||
className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ProgressPrimitive.Indicator
|
<ProgressPrimitive.Indicator
|
||||||
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
|
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
@ -2,22 +2,17 @@ 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.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>>(
|
||||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
|
||||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
<SeparatorPrimitive.Root
|
||||||
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
|
ref={ref}
|
||||||
<SeparatorPrimitive.Root
|
decorative={decorative}
|
||||||
ref={ref}
|
orientation={orientation}
|
||||||
decorative={decorative}
|
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
|
||||||
orientation={orientation}
|
{...props}
|
||||||
className={cn(
|
/>
|
||||||
"shrink-0 bg-border",
|
),
|
||||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
)
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||||
|
|
||||||
export { Separator }
|
export { Separator }
|
||||||
|
@ -2,25 +2,24 @@ 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.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>>(
|
||||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
({ className, ...props }, ref) => (
|
||||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
<SwitchPrimitives.Root
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SwitchPrimitives.Root
|
|
||||||
className={cn(
|
|
||||||
"peer inline-flex h-3 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
<SwitchPrimitives.Thumb
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none block h-2 w-2 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0",
|
"peer inline-flex h-3 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
|
className,
|
||||||
)}
|
)}
|
||||||
/>
|
{...props}
|
||||||
</SwitchPrimitives.Root>
|
ref={ref}
|
||||||
))
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-2 w-2 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
),
|
||||||
|
)
|
||||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||||
|
|
||||||
export { Switch }
|
export { Switch }
|
||||||
|
@ -1,88 +1,48 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(({ className, ...props }, ref) => (
|
||||||
({ className, ...props }, ref) => (
|
<div className="relative w-full overflow-auto">
|
||||||
<div className="relative w-full overflow-auto">
|
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
||||||
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
</div>
|
||||||
</div>
|
))
|
||||||
),
|
|
||||||
)
|
|
||||||
Table.displayName = "Table"
|
Table.displayName = "Table"
|
||||||
|
|
||||||
const TableHeader = React.forwardRef<
|
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
|
||||||
HTMLTableSectionElement,
|
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||||
))
|
))
|
||||||
TableHeader.displayName = "TableHeader"
|
TableHeader.displayName = "TableHeader"
|
||||||
|
|
||||||
const TableBody = React.forwardRef<
|
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
|
||||||
HTMLTableSectionElement,
|
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
||||||
))
|
))
|
||||||
TableBody.displayName = "TableBody"
|
TableBody.displayName = "TableBody"
|
||||||
|
|
||||||
const TableFooter = React.forwardRef<
|
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
|
||||||
HTMLTableSectionElement,
|
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} />
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<tfoot
|
|
||||||
ref={ref}
|
|
||||||
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
TableFooter.displayName = "TableFooter"
|
TableFooter.displayName = "TableFooter"
|
||||||
|
|
||||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(({ className, ...props }, ref) => (
|
||||||
({ className, ...props }, ref) => (
|
<tr ref={ref} className={cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", className)} {...props} />
|
||||||
<tr
|
))
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)
|
|
||||||
TableRow.displayName = "TableRow"
|
TableRow.displayName = "TableRow"
|
||||||
|
|
||||||
const TableHead = React.forwardRef<
|
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (
|
||||||
HTMLTableCellElement,
|
|
||||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<th
|
<th
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn("h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className)}
|
||||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
TableHead.displayName = "TableHead"
|
TableHead.displayName = "TableHead"
|
||||||
|
|
||||||
const TableCell = React.forwardRef<
|
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (
|
||||||
HTMLTableCellElement,
|
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
|
||||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<td
|
|
||||||
ref={ref}
|
|
||||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
TableCell.displayName = "TableCell"
|
TableCell.displayName = "TableCell"
|
||||||
|
|
||||||
const TableCaption = React.forwardRef<
|
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(({ className, ...props }, ref) => (
|
||||||
HTMLTableCaptionElement,
|
|
||||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
|
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
|
||||||
))
|
))
|
||||||
TableCaption.displayName = "TableCaption"
|
TableCaption.displayName = "TableCaption"
|
||||||
|
@ -1,31 +1,8 @@
|
|||||||
import { createContext } from "react"
|
import { createContext } from "react"
|
||||||
|
|
||||||
export type SortType =
|
export type SortType = "default" | "name" | "uptime" | "system" | "cpu" | "mem" | "stg" | "up" | "down" | "up total" | "down total"
|
||||||
| "default"
|
|
||||||
| "name"
|
|
||||||
| "uptime"
|
|
||||||
| "system"
|
|
||||||
| "cpu"
|
|
||||||
| "mem"
|
|
||||||
| "stg"
|
|
||||||
| "up"
|
|
||||||
| "down"
|
|
||||||
| "up total"
|
|
||||||
| "down total"
|
|
||||||
|
|
||||||
export const SORT_TYPES: SortType[] = [
|
export const SORT_TYPES: SortType[] = ["default", "name", "uptime", "system", "cpu", "mem", "stg", "up", "down", "up total", "down total"]
|
||||||
"default",
|
|
||||||
"name",
|
|
||||||
"uptime",
|
|
||||||
"system",
|
|
||||||
"cpu",
|
|
||||||
"mem",
|
|
||||||
"stg",
|
|
||||||
"up",
|
|
||||||
"down",
|
|
||||||
"up total",
|
|
||||||
"down total",
|
|
||||||
]
|
|
||||||
|
|
||||||
export type SortOrder = "asc" | "desc"
|
export type SortOrder = "asc" | "desc"
|
||||||
|
|
||||||
|
@ -6,9 +6,5 @@ export function SortProvider({ children }: { children: ReactNode }) {
|
|||||||
const [sortType, setSortType] = useState<SortType>("default")
|
const [sortType, setSortType] = useState<SortType>("default")
|
||||||
const [sortOrder, setSortOrder] = useState<SortOrder>("desc")
|
const [sortOrder, setSortOrder] = useState<SortOrder>("desc")
|
||||||
|
|
||||||
return (
|
return <SortContext.Provider value={{ sortType, setSortType, sortOrder, setSortOrder }}>{children}</SortContext.Provider>
|
||||||
<SortContext.Provider value={{ sortType, setSortType, sortOrder, setSortOrder }}>
|
|
||||||
{children}
|
|
||||||
</SortContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,5 @@ import { TooltipContext, TooltipData } from "./tooltip-context"
|
|||||||
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 }}>{children}</TooltipContext.Provider>
|
||||||
<TooltipContext.Provider value={{ tooltipData, setTooltipData }}>
|
|
||||||
{children}
|
|
||||||
</TooltipContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { LoginUserResponse, MonitorResponse, ServerGroupResponse, ServiceResponse, SettingResponse } from "@/types/nezha-api"
|
||||||
LoginUserResponse,
|
|
||||||
MonitorResponse,
|
|
||||||
ServerGroupResponse,
|
|
||||||
ServiceResponse,
|
|
||||||
SettingResponse,
|
|
||||||
} from "@/types/nezha-api"
|
|
||||||
|
|
||||||
export const fetchServerGroup = async (): Promise<ServerGroupResponse> => {
|
export const fetchServerGroup = async (): Promise<ServerGroupResponse> => {
|
||||||
const response = await fetch("/api/v1/server-group")
|
const response = await fetch("/api/v1/server-group")
|
||||||
|
@ -8,9 +8,7 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
|
export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
|
||||||
const lastActiveTime = serverInfo.last_active.startsWith("000")
|
const lastActiveTime = serverInfo.last_active.startsWith("000") ? 0 : parseISOTimestamp(serverInfo.last_active)
|
||||||
? 0
|
|
||||||
: parseISOTimestamp(serverInfo.last_active)
|
|
||||||
return {
|
return {
|
||||||
...serverInfo,
|
...serverInfo,
|
||||||
cpu: serverInfo.state.cpu || 0,
|
cpu: serverInfo.state.cpu || 0,
|
||||||
@ -47,12 +45,11 @@ export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDaysBetweenDatesWithAutoRenewal({
|
export function getDaysBetweenDatesWithAutoRenewal({ autoRenewal, cycle, startDate, endDate }: BillingData): {
|
||||||
autoRenewal,
|
days: number
|
||||||
cycle,
|
cycleLabel: string
|
||||||
startDate,
|
remainingPercentage: number
|
||||||
endDate,
|
} {
|
||||||
}: BillingData): { days: number; cycleLabel: string; remainingPercentage: number } {
|
|
||||||
let months = 1
|
let months = 1
|
||||||
// 套餐资费
|
// 套餐资费
|
||||||
let cycleLabel = cycle
|
let cycleLabel = cycle
|
||||||
@ -100,12 +97,9 @@ export function getDaysBetweenDatesWithAutoRenewal({
|
|||||||
days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()),
|
days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()),
|
||||||
cycleLabel: cycleLabel,
|
cycleLabel: cycleLabel,
|
||||||
remainingPercentage:
|
remainingPercentage:
|
||||||
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
|
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / dayjs(endDate).diff(startDate, "day") > 1
|
||||||
dayjs(endDate).diff(startDate, "day") >
|
|
||||||
1
|
|
||||||
? 1
|
? 1
|
||||||
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
|
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / dayjs(endDate).diff(startDate, "day"),
|
||||||
dayjs(endDate).diff(startDate, "day"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,8 +30,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
duration={1000}
|
duration={1000}
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
classNames: {
|
classNames: {
|
||||||
default:
|
default: "w-fit rounded-full px-2.5 py-1.5 bg-neutral-100 border border-neutral-200 backdrop-blur-xl shadow-none",
|
||||||
"w-fit rounded-full px-2.5 py-1.5 bg-neutral-100 border border-neutral-200 backdrop-blur-xl shadow-none",
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
position="top-center"
|
position="top-center"
|
||||||
|
@ -15,14 +15,7 @@ import { fetchServerGroup } from "@/lib/nezha-api"
|
|||||||
import { cn, formatNezhaInfo } from "@/lib/utils"
|
import { cn, formatNezhaInfo } from "@/lib/utils"
|
||||||
import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
||||||
import { ServerGroup } from "@/types/nezha-api"
|
import { ServerGroup } from "@/types/nezha-api"
|
||||||
import {
|
import { ArrowDownIcon, ArrowUpIcon, ArrowsUpDownIcon, ChartBarSquareIcon, MapIcon, ViewColumnsIcon } from "@heroicons/react/20/solid"
|
||||||
ArrowDownIcon,
|
|
||||||
ArrowUpIcon,
|
|
||||||
ArrowsUpDownIcon,
|
|
||||||
ChartBarSquareIcon,
|
|
||||||
MapIcon,
|
|
||||||
ViewColumnsIcon,
|
|
||||||
} from "@heroicons/react/20/solid"
|
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
@ -44,7 +37,7 @@ export default function Servers() {
|
|||||||
const [currentGroup, setCurrentGroup] = useState<string>("All")
|
const [currentGroup, setCurrentGroup] = useState<string>("All")
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error ShowNetTransfer is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -96,60 +89,40 @@ export default function Servers() {
|
|||||||
nezhaWsData?.servers?.filter((server) => {
|
nezhaWsData?.servers?.filter((server) => {
|
||||||
if (currentGroup === "All") return true
|
if (currentGroup === "All") return true
|
||||||
const group = groupData?.data?.find(
|
const group = groupData?.data?.find(
|
||||||
(g: ServerGroup) =>
|
(g: ServerGroup) => g.group.name === currentGroup && Array.isArray(g.servers) && g.servers.includes(server.id),
|
||||||
g.group.name === currentGroup &&
|
|
||||||
Array.isArray(g.servers) &&
|
|
||||||
g.servers.includes(server.id),
|
|
||||||
)
|
)
|
||||||
return !!group
|
return !!group
|
||||||
}) || []
|
}) || []
|
||||||
|
|
||||||
const totalServers = filteredServers.length || 0
|
const totalServers = filteredServers.length || 0
|
||||||
const onlineServers =
|
const onlineServers = filteredServers.filter((server) => formatNezhaInfo(nezhaWsData.now, server).online)?.length || 0
|
||||||
filteredServers.filter((server) => formatNezhaInfo(nezhaWsData.now, server).online)?.length || 0
|
const offlineServers = filteredServers.filter((server) => !formatNezhaInfo(nezhaWsData.now, server).online)?.length || 0
|
||||||
const offlineServers =
|
|
||||||
filteredServers.filter((server) => !formatNezhaInfo(nezhaWsData.now, server).online)?.length ||
|
|
||||||
0
|
|
||||||
const up =
|
const up =
|
||||||
filteredServers.reduce(
|
filteredServers.reduce(
|
||||||
(total, server) =>
|
(total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_out_transfer ?? 0) : total),
|
||||||
formatNezhaInfo(nezhaWsData.now, server).online
|
|
||||||
? total + (server.state?.net_out_transfer ?? 0)
|
|
||||||
: total,
|
|
||||||
0,
|
0,
|
||||||
) || 0
|
) || 0
|
||||||
const down =
|
const down =
|
||||||
filteredServers.reduce(
|
filteredServers.reduce(
|
||||||
(total, server) =>
|
(total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_in_transfer ?? 0) : total),
|
||||||
formatNezhaInfo(nezhaWsData.now, server).online
|
|
||||||
? total + (server.state?.net_in_transfer ?? 0)
|
|
||||||
: total,
|
|
||||||
0,
|
0,
|
||||||
) || 0
|
) || 0
|
||||||
|
|
||||||
const upSpeed =
|
const upSpeed =
|
||||||
filteredServers.reduce(
|
filteredServers.reduce(
|
||||||
(total, server) =>
|
(total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_out_speed ?? 0) : total),
|
||||||
formatNezhaInfo(nezhaWsData.now, server).online
|
|
||||||
? total + (server.state?.net_out_speed ?? 0)
|
|
||||||
: total,
|
|
||||||
0,
|
0,
|
||||||
) || 0
|
) || 0
|
||||||
const downSpeed =
|
const downSpeed =
|
||||||
filteredServers.reduce(
|
filteredServers.reduce(
|
||||||
(total, server) =>
|
(total, server) => (formatNezhaInfo(nezhaWsData.now, server).online ? total + (server.state?.net_in_speed ?? 0) : total),
|
||||||
formatNezhaInfo(nezhaWsData.now, server).online
|
|
||||||
? total + (server.state?.net_in_speed ?? 0)
|
|
||||||
: total,
|
|
||||||
0,
|
0,
|
||||||
) || 0
|
) || 0
|
||||||
|
|
||||||
filteredServers =
|
filteredServers =
|
||||||
status === "all"
|
status === "all"
|
||||||
? filteredServers
|
? filteredServers
|
||||||
: filteredServers.filter((server) =>
|
: filteredServers.filter((server) => [status].includes(formatNezhaInfo(nezhaWsData.now, server).online ? "online" : "offline"))
|
||||||
[status].includes(formatNezhaInfo(nezhaWsData.now, server).online ? "online" : "offline"),
|
|
||||||
)
|
|
||||||
|
|
||||||
filteredServers = filteredServers.sort((a, b) => {
|
filteredServers = filteredServers.sort((a, b) => {
|
||||||
const serverAInfo = formatNezhaInfo(nezhaWsData.now, a)
|
const serverAInfo = formatNezhaInfo(nezhaWsData.now, a)
|
||||||
@ -277,17 +250,14 @@ export default function Servers() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"rounded-[50px] flex items-center gap-1 dark:text-white border dark:border-none text-black cursor-pointer dark:[text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] dark:bg-stone-800 bg-stone-100 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
|
"rounded-[50px] flex items-center gap-1 dark:text-white border dark:border-none text-black cursor-pointer dark:[text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] dark:bg-stone-800 bg-stone-100 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
|
||||||
{
|
{
|
||||||
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] dark:bg-stone-700 bg-stone-200":
|
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] dark:bg-stone-700 bg-stone-200": settingsOpen,
|
||||||
settingsOpen,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dark:bg-stone-800/50 bg-stone-100/50 ": customBackgroundImage,
|
"dark:bg-stone-800/50 bg-stone-100/50 ": customBackgroundImage,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="text-[10px] font-bold whitespace-nowrap">
|
<p className="text-[10px] font-bold whitespace-nowrap">{sortType === "default" ? "Sort" : sortType.toUpperCase()}</p>
|
||||||
{sortType === "default" ? "Sort" : sortType.toUpperCase()}
|
|
||||||
</p>
|
|
||||||
{sortOrder === "asc" && sortType !== "default" ? (
|
{sortOrder === "asc" && sortType !== "default" ? (
|
||||||
<ArrowUpIcon className="size-[13px]" />
|
<ArrowUpIcon className="size-[13px]" />
|
||||||
) : sortOrder === "desc" && sortType !== "default" ? (
|
) : sortOrder === "desc" && sortType !== "default" ? (
|
||||||
@ -309,8 +279,7 @@ export default function Servers() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"rounded-[5px] text-[11px] w-fit px-1 py-0.5 cursor-pointer bg-transparent border transition-all dark:shadow-none ",
|
"rounded-[5px] text-[11px] w-fit px-1 py-0.5 cursor-pointer bg-transparent border transition-all dark:shadow-none ",
|
||||||
{
|
{
|
||||||
"bg-black text-white dark:bg-white dark:text-black shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]":
|
"bg-black text-white dark:bg-white dark:text-black shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]": sortType === type,
|
||||||
sortType === type,
|
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -330,8 +299,7 @@ export default function Servers() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"rounded-[5px] text-[11px] w-fit px-1 py-0.5 cursor-pointer bg-transparent border transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] dark:shadow-none",
|
"rounded-[5px] text-[11px] w-fit px-1 py-0.5 cursor-pointer bg-transparent border transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] dark:shadow-none",
|
||||||
{
|
{
|
||||||
"bg-black text-white dark:bg-white dark:text-black":
|
"bg-black text-white dark:bg-white dark:text-black": sortOrder === order && sortType !== "default",
|
||||||
sortOrder === order && sortType !== "default",
|
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -344,9 +312,7 @@ export default function Servers() {
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
{showMap === "1" && (
|
{showMap === "1" && <GlobalMap now={nezhaWsData.now} serverList={nezhaWsData?.servers || []} />}
|
||||||
<GlobalMap now={nezhaWsData.now} serverList={nezhaWsData?.servers || []} />
|
|
||||||
)}
|
|
||||||
{showServices === "1" && <ServiceTracker />}
|
{showServices === "1" && <ServiceTracker />}
|
||||||
{inline === "1" && (
|
{inline === "1" && (
|
||||||
<section className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden mt-6">
|
<section className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden mt-6">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user