Compare commits

..

7 Commits

7 changed files with 252 additions and 194 deletions

View File

@ -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>
)}
</> </>
) )
} }

View File

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

View File

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

View File

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

BIN
bun.lockb

Binary file not shown.

View File

@ -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: {

View File

@ -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"
}, },