mirror of
https://github.com/hamster1963/nezha-dash.git
synced 2025-04-24 21:10:45 +08:00
Compare commits
7 Commits
bf959fbb9a
...
7277e44f85
Author | SHA1 | Date | |
---|---|---|---|
|
7277e44f85 | ||
|
649a3546f1 | ||
|
5e2cad71db | ||
|
07afc44eb7 | ||
|
1713189333 | ||
|
3ce131419f | ||
|
010cfce1c4 |
@ -20,6 +20,77 @@ const ServerGlobal = dynamic(() => import("./Global"), {
|
|||||||
loading: () => <GlobalLoading />,
|
loading: () => <GlobalLoading />,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sortServersByDisplayIndex = (servers: any[]) => {
|
||||||
|
return servers.sort((a, b) => {
|
||||||
|
const displayIndexDiff = (b.display_index || 0) - (a.display_index || 0)
|
||||||
|
return displayIndexDiff !== 0 ? displayIndexDiff : a.id - b.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterServersByStatus = (servers: any[], status: string) => {
|
||||||
|
return status === "all"
|
||||||
|
? servers
|
||||||
|
: servers.filter((server) => [status].includes(server.online_status ? "online" : "offline"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterServersByTag = (servers: any[], tag: string, defaultTag: string) => {
|
||||||
|
return tag === defaultTag ? servers : servers.filter((server) => server.tag === tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortServersByNetwork = (servers: any[]) => {
|
||||||
|
return [...servers].sort((a, b) => {
|
||||||
|
if (!a.online_status && b.online_status) return 1
|
||||||
|
if (a.online_status && !b.online_status) return -1
|
||||||
|
if (!a.online_status && !b.online_status) return 0
|
||||||
|
return b.status.NetInSpeed + b.status.NetOutSpeed - (a.status.NetInSpeed + a.status.NetOutSpeed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTagCounts = (servers: any[]) => {
|
||||||
|
return servers.reduce((acc: Record<string, number>, server) => {
|
||||||
|
if (server.tag) {
|
||||||
|
acc[server.tag] = (acc[server.tag] || 0) + 1
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadingState = ({ t }: { t: any }) => (
|
||||||
|
<div className="flex flex-col items-center min-h-96 justify-center ">
|
||||||
|
<div className="font-semibold flex items-center gap-2 text-sm">
|
||||||
|
<Loader visible={true} />
|
||||||
|
{t("connecting")}...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const ErrorState = ({ error, t }: { error: Error; t: any }) => (
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<p className="text-sm font-medium opacity-40">{error.message}</p>
|
||||||
|
<p className="text-sm font-medium opacity-40">{t("error_message")}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const ServerList = ({ servers, inline, containerRef }: { servers: any[]; inline: string; containerRef: any }) => {
|
||||||
|
if (inline === "1") {
|
||||||
|
return (
|
||||||
|
<section ref={containerRef} className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden">
|
||||||
|
{servers.map((serverInfo) => (
|
||||||
|
<ServerCardInline key={serverInfo.id} serverInfo={serverInfo} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={containerRef} className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||||
|
{servers.map((serverInfo) => (
|
||||||
|
<ServerCard key={serverInfo.id} serverInfo={serverInfo} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function ServerListClient() {
|
export default function ServerListClient() {
|
||||||
const { status } = useStatus()
|
const { status } = useStatus()
|
||||||
const { filter } = useFilter()
|
const { filter } = useFilter()
|
||||||
@ -36,12 +107,9 @@ export default function ServerListClient() {
|
|||||||
if (inlineState !== null) {
|
if (inlineState !== null) {
|
||||||
setInline(inlineState)
|
setInline(inlineState)
|
||||||
}
|
}
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const savedTag = sessionStorage.getItem("selectedTag") || defaultTag
|
const savedTag = sessionStorage.getItem("selectedTag") || defaultTag
|
||||||
setTag(savedTag)
|
setTag(savedTag)
|
||||||
|
|
||||||
restoreScrollPosition()
|
restoreScrollPosition()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -71,73 +139,30 @@ export default function ServerListClient() {
|
|||||||
|
|
||||||
const { data, error } = useServerData()
|
const { data, error } = useServerData()
|
||||||
|
|
||||||
if (error)
|
if (error) return <ErrorState error={error} t={t} />
|
||||||
return (
|
if (!data?.result) return <LoadingState t={t} />
|
||||||
<div className="flex flex-col items-center justify-center">
|
|
||||||
<p className="text-sm font-medium opacity-40">{error.message}</p>
|
|
||||||
<p className="text-sm font-medium opacity-40">{t("error_message")}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!data?.result)
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center min-h-96 justify-center ">
|
|
||||||
<div className="font-semibold flex items-center gap-2 text-sm">
|
|
||||||
<Loader visible={true} />
|
|
||||||
{t("connecting")}...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const { result } = data
|
const { result } = data
|
||||||
const sortedServers = result.sort((a, b) => {
|
const sortedServers = sortServersByDisplayIndex(result)
|
||||||
const displayIndexDiff = (b.display_index || 0) - (a.display_index || 0)
|
const filteredServersByStatus = filterServersByStatus(sortedServers, status)
|
||||||
if (displayIndexDiff !== 0) return displayIndexDiff
|
|
||||||
return a.id - b.id
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredServersByStatus =
|
|
||||||
status === "all"
|
|
||||||
? sortedServers
|
|
||||||
: sortedServers.filter((server) =>
|
|
||||||
[status].includes(server.online_status ? "online" : "offline"),
|
|
||||||
)
|
|
||||||
|
|
||||||
const allTag = filteredServersByStatus.map((server) => server.tag).filter(Boolean)
|
const allTag = filteredServersByStatus.map((server) => server.tag).filter(Boolean)
|
||||||
const uniqueTags = [...new Set(allTag)]
|
const uniqueTags = [...new Set(allTag)]
|
||||||
uniqueTags.unshift(defaultTag)
|
uniqueTags.unshift(defaultTag)
|
||||||
|
|
||||||
const filteredServers =
|
let filteredServers = filterServersByTag(filteredServersByStatus, tag, defaultTag)
|
||||||
tag === defaultTag
|
|
||||||
? filteredServersByStatus
|
|
||||||
: filteredServersByStatus.filter((server) => server.tag === tag)
|
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
filteredServers.sort((a, b) => {
|
filteredServers = sortServersByNetwork(filteredServers)
|
||||||
if (!a.online_status && b.online_status) return 1
|
|
||||||
if (a.online_status && !b.online_status) return -1
|
|
||||||
if (!a.online_status && !b.online_status) return 0
|
|
||||||
return (
|
|
||||||
b.status.NetInSpeed + b.status.NetOutSpeed - (a.status.NetInSpeed + a.status.NetOutSpeed)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagCountMap: Record<string, number> = {}
|
const tagCountMap = getTagCounts(filteredServersByStatus)
|
||||||
for (const server of filteredServersByStatus) {
|
|
||||||
if (server.tag) {
|
|
||||||
tagCountMap[server.tag] = (tagCountMap[server.tag] || 0) + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="flex items-center gap-2 w-full overflow-hidden">
|
<section className="flex items-center gap-2 w-full overflow-hidden">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => setShowMap(!showMap)}
|
||||||
setShowMap(!showMap)
|
|
||||||
}}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]",
|
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]",
|
||||||
{
|
{
|
||||||
@ -150,8 +175,9 @@ export default function ServerListClient() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setInline(inline === "0" ? "1" : "0")
|
const newInline = inline === "0" ? "1" : "0"
|
||||||
localStorage.setItem("inline", inline === "0" ? "1" : "0")
|
setInline(newInline)
|
||||||
|
localStorage.setItem("inline", newInline)
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]",
|
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]",
|
||||||
@ -172,24 +198,7 @@ export default function ServerListClient() {
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
{showMap && <ServerGlobal />}
|
{showMap && <ServerGlobal />}
|
||||||
{inline === "1" && (
|
<ServerList servers={filteredServers} inline={inline} containerRef={containerRef} />
|
||||||
<section
|
|
||||||
ref={containerRef}
|
|
||||||
className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden"
|
|
||||||
>
|
|
||||||
{filteredServers.map((serverInfo) => (
|
|
||||||
<ServerCardInline key={serverInfo.id} serverInfo={serverInfo} />
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{inline === "0" && (
|
|
||||||
<section ref={containerRef} className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
|
||||||
{filteredServers.map((serverInfo) => (
|
|
||||||
<ServerCard key={serverInfo.id} serverInfo={serverInfo} />
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,38 +1,53 @@
|
|||||||
import pack from "@/package.json"
|
import pack from "@/package.json"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
|
|
||||||
|
const GITHUB_URL = "https://github.com/hamster1963/nezha-dash"
|
||||||
|
const PERSONAL_URL = "https://buycoffee.top"
|
||||||
|
|
||||||
|
type LinkProps = {
|
||||||
|
href: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const FooterLink = ({ href, children }: LinkProps) => (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
className="cursor-pointer font-normal underline decoration-yellow-500 hover:decoration-yellow-600 transition-colors decoration-2 underline-offset-2 dark:decoration-yellow-500/60 dark:hover:decoration-yellow-500/80"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
|
||||||
|
const baseTextStyles = "text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50"
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const t = useTranslations("Footer")
|
const t = useTranslations("Footer")
|
||||||
const version = pack.version
|
const version = pack.version
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="mx-auto w-full max-w-5xl flex items-center justify-between">
|
<footer className="mx-auto w-full max-w-5xl flex items-center justify-between">
|
||||||
<section className="flex flex-col">
|
<section className="flex flex-col">
|
||||||
<p className="mt-3 flex gap-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
|
<p className={`mt-3 flex gap-1 ${baseTextStyles}`}>
|
||||||
{t("p_146-598_Findthecodeon")}{" "}
|
{t("p_146-598_Findthecodeon")}{" "}
|
||||||
<a
|
<FooterLink href={GITHUB_URL}>
|
||||||
href="https://github.com/hamster1963/nezha-dash"
|
|
||||||
target="_blank"
|
|
||||||
className="cursor-pointer font-normal underline decoration-yellow-500 hover:decoration-yellow-600 transition-colors decoration-2 underline-offset-2 dark:decoration-yellow-500/60 dark:hover:decoration-yellow-500/80"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{t("a_303-585_GitHub")}
|
{t("a_303-585_GitHub")}
|
||||||
</a>
|
</FooterLink>
|
||||||
<a
|
<FooterLink href={`${GITHUB_URL}/releases/tag/v${version}`}>
|
||||||
href={`https://github.com/hamster1963/nezha-dash/releases/tag/v${version}`}
|
|
||||||
target="_blank"
|
|
||||||
className="cursor-pointer font-normal underline decoration-yellow-500 hover:decoration-yellow-600 transition-colors decoration-2 underline-offset-2 dark:decoration-yellow-500/60 dark:hover:decoration-yellow-500/80"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
v{version}
|
v{version}
|
||||||
</a>
|
</FooterLink>
|
||||||
</p>
|
</p>
|
||||||
<section className="mt-1 flex items-center gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
|
<section className={`mt-1 flex items-center gap-2 ${baseTextStyles}`}>
|
||||||
{t("section_607-869_2020")}
|
{t("section_607-869_2020")}
|
||||||
{new Date().getFullYear()}{" "}
|
{currentYear}{" "}
|
||||||
<a href={"https://buycoffee.top"}>{t("a_800-850_Hamster1963")}</a>
|
<FooterLink href={PERSONAL_URL}>
|
||||||
|
{t("a_800-850_Hamster1963")}
|
||||||
|
</FooterLink>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
<p className="mt-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
|
<p className={`mt-1 ${baseTextStyles}`}>
|
||||||
<kbd className="pointer-events-none mx-1 inline-flex h-4 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
<kbd className="pointer-events-none mx-1 inline-flex h-4 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||||
<span className="text-xs">⌘</span>K
|
<span className="text-xs">⌘</span>K
|
||||||
</kbd>
|
</kbd>
|
||||||
|
@ -9,7 +9,94 @@ import NumberFlow, { NumberFlowGroup } from "@number-flow/react"
|
|||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import React from "react"
|
import { memo, useCallback, useEffect, useState } from "react"
|
||||||
|
|
||||||
|
interface TimeState {
|
||||||
|
hh: number
|
||||||
|
mm: number
|
||||||
|
ss: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomLink {
|
||||||
|
link: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCurrentTime = () => {
|
||||||
|
const [time, setTime] = useState<TimeState>({
|
||||||
|
hh: DateTime.now().setLocale("en-US").hour,
|
||||||
|
mm: DateTime.now().setLocale("en-US").minute,
|
||||||
|
ss: DateTime.now().setLocale("en-US").second,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
const now = DateTime.now().setLocale("en-US")
|
||||||
|
setTime({
|
||||||
|
hh: now.hour,
|
||||||
|
mm: now.minute,
|
||||||
|
ss: now.second,
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return time
|
||||||
|
}
|
||||||
|
|
||||||
|
const Links = memo(function Links() {
|
||||||
|
const linksEnv = getEnv("NEXT_PUBLIC_Links")
|
||||||
|
const links: CustomLink[] | null = linksEnv ? JSON.parse(linksEnv) : null
|
||||||
|
|
||||||
|
if (!links) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{links.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.link}
|
||||||
|
href={link.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
|
||||||
|
>
|
||||||
|
{link.name}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const Overview = memo(function Overview() {
|
||||||
|
const t = useTranslations("Overview")
|
||||||
|
const time = useCurrentTime()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={"mt-10 flex flex-col md:mt-16"}>
|
||||||
|
<p className="text-base font-semibold">{t("p_2277-2331_Overview")}</p>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<p className="text-sm font-medium opacity-50">{t("p_2390-2457_wherethetimeis")}</p>
|
||||||
|
<NumberFlowGroup>
|
||||||
|
<div
|
||||||
|
style={{ fontVariantNumeric: "tabular-nums" }}
|
||||||
|
className="flex text-sm font-medium mt-0.5"
|
||||||
|
>
|
||||||
|
<NumberFlow trend={1} value={time.hh} format={{ minimumIntegerDigits: 2 }} />
|
||||||
|
<NumberFlow
|
||||||
|
prefix=":"
|
||||||
|
trend={1}
|
||||||
|
value={time.mm}
|
||||||
|
digits={{ 1: { max: 5 } }}
|
||||||
|
format={{ minimumIntegerDigits: 2 }}
|
||||||
|
/>
|
||||||
|
<p className="mt-[0.5px]">:{time.ss.toString().padStart(2, "0")}</p>
|
||||||
|
</div>
|
||||||
|
</NumberFlowGroup>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
const t = useTranslations("Header")
|
const t = useTranslations("Header")
|
||||||
@ -19,14 +106,16 @@ function Header() {
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleLogoClick = useCallback(() => {
|
||||||
|
sessionStorage.removeItem("selectedTag")
|
||||||
|
router.push("/")
|
||||||
|
}, [router])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-5xl">
|
<div className="mx-auto w-full max-w-5xl">
|
||||||
<section className="flex items-center justify-between">
|
<section className="flex items-center justify-between">
|
||||||
<section
|
<section
|
||||||
onClick={() => {
|
onClick={handleLogoClick}
|
||||||
sessionStorage.removeItem("selectedTag")
|
|
||||||
router.push("/")
|
|
||||||
}}
|
|
||||||
className="flex cursor-pointer items-center text-base font-medium hover:opacity-50 transition-opacity duration-300"
|
className="flex cursor-pointer items-center text-base font-medium hover:opacity-50 transition-opacity duration-300"
|
||||||
>
|
>
|
||||||
<div className="mr-1 flex flex-row items-center justify-start">
|
<div className="mr-1 flex flex-row items-center justify-start">
|
||||||
@ -67,80 +156,4 @@ function Header() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type links = {
|
|
||||||
link: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function Links() {
|
|
||||||
const linksEnv = getEnv("NEXT_PUBLIC_Links")
|
|
||||||
|
|
||||||
const links: links[] | null = linksEnv ? JSON.parse(linksEnv) : null
|
|
||||||
|
|
||||||
if (!links) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{links.map((link) => {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={link.link}
|
|
||||||
href={link.link}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
|
|
||||||
>
|
|
||||||
{link.name}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Overview() {
|
|
||||||
const t = useTranslations("Overview")
|
|
||||||
const [time, setTime] = React.useState({
|
|
||||||
hh: DateTime.now().setLocale("en-US").hour,
|
|
||||||
mm: DateTime.now().setLocale("en-US").minute,
|
|
||||||
ss: DateTime.now().setLocale("en-US").second,
|
|
||||||
})
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
setTime({
|
|
||||||
hh: DateTime.now().setLocale("en-US").hour,
|
|
||||||
mm: DateTime.now().setLocale("en-US").minute,
|
|
||||||
ss: DateTime.now().setLocale("en-US").second,
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
return () => clearInterval(timer)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className={"mt-10 flex flex-col md:mt-16"}>
|
|
||||||
<p className="text-base font-semibold">{t("p_2277-2331_Overview")}</p>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<p className="text-sm font-medium opacity-50">{t("p_2390-2457_wherethetimeis")}</p>
|
|
||||||
<NumberFlowGroup>
|
|
||||||
<div
|
|
||||||
style={{ fontVariantNumeric: "tabular-nums" }}
|
|
||||||
className="flex text-sm font-medium mt-0.5"
|
|
||||||
>
|
|
||||||
<NumberFlow trend={1} value={time.hh} format={{ minimumIntegerDigits: 2 }} />
|
|
||||||
<NumberFlow
|
|
||||||
prefix=":"
|
|
||||||
trend={1}
|
|
||||||
value={time.mm}
|
|
||||||
digits={{ 1: { max: 5 } }}
|
|
||||||
format={{ minimumIntegerDigits: 2 }}
|
|
||||||
/>
|
|
||||||
<p className="mt-[0.5px]">:{time.ss.toString().padStart(2, "0")}</p>
|
|
||||||
</div>
|
|
||||||
</NumberFlowGroup>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default Header
|
export default Header
|
||||||
|
@ -9,27 +9,45 @@ import { Separator } from "@/components/ui/separator"
|
|||||||
import getEnv from "@/lib/env-entry"
|
import getEnv from "@/lib/env-entry"
|
||||||
import { use, useState } from "react"
|
import { use, useState } from "react"
|
||||||
|
|
||||||
export default function Page(props: { params: Promise<{ id: string }> }) {
|
type PageProps = {
|
||||||
const params = use(props.params)
|
params: Promise<{ id: string }>
|
||||||
const tabs = ["Detail", "Network"]
|
}
|
||||||
const [currentTab, setCurrentTab] = useState(tabs[0])
|
|
||||||
|
type TabType = "Detail" | "Network"
|
||||||
|
|
||||||
|
export default function Page({ params }: PageProps) {
|
||||||
|
const { id } = use(params)
|
||||||
|
const serverId = Number(id)
|
||||||
|
const tabs: TabType[] = ["Detail", "Network"]
|
||||||
|
const [currentTab, setCurrentTab] = useState<TabType>(tabs[0])
|
||||||
|
|
||||||
|
const tabContent = {
|
||||||
|
Detail: <ServerDetailChartClient server_id={serverId} show={currentTab === "Detail"} />,
|
||||||
|
Network: (
|
||||||
|
<>
|
||||||
|
{getEnv("NEXT_PUBLIC_ShowIpInfo") && <ServerIPInfo server_id={serverId} />}
|
||||||
|
<NetworkChartClient server_id={serverId} show={currentTab === "Network"} />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto grid w-full max-w-5xl gap-2">
|
<main className="mx-auto grid w-full max-w-5xl gap-2">
|
||||||
<ServerDetailClient server_id={Number(params.id)} />
|
<ServerDetailClient server_id={serverId} />
|
||||||
<section className="flex items-center my-2 w-full">
|
|
||||||
|
<nav className="flex items-center my-2 w-full">
|
||||||
<Separator className="flex-1" />
|
<Separator className="flex-1" />
|
||||||
<div className="flex justify-center w-full max-w-[200px]">
|
<div className="flex justify-center w-full max-w-[200px]">
|
||||||
<TabSwitch tabs={tabs} currentTab={currentTab} setCurrentTab={setCurrentTab} />
|
<TabSwitch
|
||||||
|
tabs={tabs}
|
||||||
|
currentTab={currentTab}
|
||||||
|
setCurrentTab={(tab: string) => setCurrentTab(tab as TabType)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="flex-1" />
|
<Separator className="flex-1" />
|
||||||
</section>
|
</nav>
|
||||||
<div style={{ display: currentTab === tabs[0] ? "block" : "none" }}>
|
|
||||||
<ServerDetailChartClient server_id={Number(params.id)} show={currentTab === tabs[0]} />
|
{tabContent[currentTab]}
|
||||||
</div>
|
</main>
|
||||||
<div style={{ display: currentTab === tabs[1] ? "block" : "none" }}>
|
|
||||||
{getEnv("NEXT_PUBLIC_ShowIpInfo") && <ServerIPInfo server_id={Number(params.id)} />}
|
|
||||||
<NetworkChartClient server_id={Number(params.id)} show={currentTab === tabs[1]} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,9 @@ const withPWA = withPWAInit({
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
experimental: {
|
experimental: {
|
||||||
|
webpackBuildWorker: true,
|
||||||
|
parallelServerBuildTraces: true,
|
||||||
|
parallelServerCompiles: true,
|
||||||
inlineCss: true,
|
inlineCss: true,
|
||||||
reactCompiler: true,
|
reactCompiler: true,
|
||||||
serverActions: {
|
serverActions: {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nezha-dash",
|
"name": "nezha-dash",
|
||||||
"version": "2.6.2",
|
"version": "2.6.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3040",
|
"dev": "next dev -p 3040",
|
||||||
@ -54,7 +54,7 @@
|
|||||||
"react-wrap-balancer": "^1.1.1",
|
"react-wrap-balancer": "^1.1.1",
|
||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"swr": "^2.3.0",
|
"swr": "^2.3.1",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user