Compare commits

...

46 Commits
v2.7.2 ... main

Author SHA1 Message Date
hamster1963
e03e6232fb chore: update dependencies in package.json to latest versions 2025-04-23 17:04:27 +08:00
hamster1963
484266666d chore: bump version to 2.9.3 in package.json 2025-04-10 10:39:03 +08:00
hamster1963
fb1a74ec09 style: adjust padding and spacing in ThemeSwitcher component 2025-04-10 10:37:02 +08:00
hamster1963
812397235c chore: update dependencies in package.json to latest versions 2025-04-10 10:35:10 +08:00
hamster1963
3411783401 chore: bump version to 2.9.2 in package.json 2025-04-02 13:30:01 +08:00
hamster1963
1ab1559c20 feat: implement theme switcher with radio buttons and add localization for Traditional Chinese 2025-04-02 13:18:42 +08:00
hamster1963
0837393903 chore: update dependencies in package.json to latest versions 2025-04-02 11:36:46 +08:00
hamster1963
55cec2e055 chore: bump version to 2.9.1 in package.json 2025-03-26 09:04:29 +08:00
hamster1963
14d7f1e416 refactor: replace bcrypt with CryptoJS for password hashing and update dependencies 2025-03-26 09:04:06 +08:00
hamster1963
c267b489e4 chore: bump version to 2.9.0 in package.json 2025-03-25 17:38:44 +08:00
仓鼠
8a1ce73564
refactor: improve Switch component state management and initial rende… (#265)
* refactor: improve Switch component state management and initial render handling

* chore: auto-fix linting and formatting issues
2025-03-25 17:31:05 +08:00
hamster1963
6b273622df chore: update permissions to allow write access for Deploy and auto-fix workflows 2025-03-25 16:57:41 +08:00
hamster1963
80c4500822 chore: update dependencies in package.json 2025-03-25 16:23:45 +08:00
仓鼠
aa14f6045f
chore: update permissions for auto-fix lint and format workflow (#264) 2025-03-25 16:20:33 +08:00
仓鼠
4719c2210e
Potential fix for code scanning alert no. 4: Cache Poisoning via execution of untrusted code (#262)
* Potential fix for code scanning alert no. 4: Cache Poisoning via execution of untrusted code

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-03-25 16:15:09 +08:00
仓鼠
079ff0be32
chore: add permissions for contents read in Deploy.yml (#263)
* chore: add permissions for contents read in Deploy.yml

* chore: auto-fix linting and formatting issues
2025-03-25 16:14:04 +08:00
仓鼠
38ebfcee44
Merge pull request #261 from hamster1963/alert-autofix-7
Potential fix for code scanning alert no. 7: Use of password hash with insufficient computational effort
2025-03-25 16:07:01 +08:00
仓鼠
365ba91bff chore: add @types/bcrypt 2025-03-25 07:45:25 +00:00
hamster1963
865a5ba8ee chore: auto-fix linting and formatting issues 2025-03-25 07:31:50 +00:00
仓鼠
37adab9208
Potential fix for code scanning alert no. 7: Use of password hash with insufficient computational effort
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-03-25 15:31:27 +08:00
hamster1963
e5a4c2f410 chore: bump version to 2.8.3 2025-03-23 21:37:04 +08:00
hamster1963
ce21c9b774 chore: bump version to 2.8.2 2025-03-23 21:36:38 +08:00
hamster1963
fb1b412015 chore: update caniuse-lite, next, and devDependencies to latest versions 2025-03-23 21:36:01 +08:00
hamster1963
24a079624c chore: update caniuse-lite, cmdk, tailwindcss, and postcss to latest versions 2025-03-16 11:12:31 +08:00
hamster1963
b630505ca3 feat: enhance uptime display to show days and hours 2025-03-16 11:02:09 +08:00
hamster1963
d2ab62efdc docs: update demo site link in README 2025-03-14 10:43:25 +08:00
仓鼠
b5ac8251d9
Merge pull request #256 from hamster1963/boot-time
feat: add boot time display to server details and update localization
2025-03-12 09:34:58 +08:00
hamster1963
0b7a5be3c0 chore: update dependencies to latest versions 2025-03-12 09:31:24 +08:00
hamster1963
0f435ee7c1 feat: add boot time display to server details and update localization 2025-03-12 09:30:01 +08:00
hamster1963
ac15be6e71 chore: bump version to 2.8.1 2025-02-28 09:32:36 +08:00
hamster1963
b1bcd7227d feat: update dev script to use turbopack for improved performance 2025-02-28 09:32:20 +08:00
hamster1963
29b545706e fix: ensure unique keys for map features by including index in key prop 2025-02-28 09:32:15 +08:00
hamster1963
8bb5ea0cf7 feat: add loading skeleton for time display in overview section 2025-02-28 09:21:43 +08:00
hamster1963
28d1399b2f chore: update dependencies 2025-02-27 11:37:58 +08:00
hamster1963
b456cad3d5 feat: enhance accessibility and improve component styles across the application 2025-02-22 15:56:04 +08:00
hamster1963
2302a50484 chore: bump version to 2.8.0 2025-02-21 11:35:42 +08:00
hamster1963
6c3c18fa1e chore: update bun lockfile 2025-02-21 11:26:43 +08:00
hamster1963
fca3a9ab52 feat: add server ID and improve interactive map and tooltip functionality 2025-02-21 11:26:32 +08:00
hamster1963
5b8be03765 feat: update button styles in ServerListClient for improved accessibility and visual consistency 2025-02-21 11:05:45 +08:00
hamster1963
1dae136edb chore: update country-flag-icons, next-intl, tailwindcss, postcss dependencies 2025-02-20 09:47:33 +08:00
hamster1963
f49ca034cb feat: enhance network chart x-axis tick rendering with dynamic time interval logic 2025-02-19 17:45:52 +08:00
hamster1963
e8530956fa feat: replace requestAnimationFrame with setInterval for time updates 2025-02-19 15:06:18 +08:00
hamster1963
4171829a15 feat: optimize time display rendering with requestAnimationFrame 2025-02-19 14:26:22 +08:00
hamster1963
8fbe50fd8d chore: update project dependencies 2025-02-18 18:04:38 +08:00
hamster1963
e2a9357f18 feat: add AnimatedCount to server overview metrics 2025-02-18 18:04:19 +08:00
hamster1963
69ff5365d5 feat: replace NumberFlow with custom AnimatedCount component for time display 2025-02-18 17:57:18 +08:00
58 changed files with 693 additions and 400 deletions

View File

@ -1,4 +1,6 @@
name: Build and push Docker image name: Build and push Docker image
permissions:
contents: write
on: on:
push: push:

View File

@ -1,7 +1,10 @@
name: Auto Fix Lint and Format name: Auto Fix Lint and Format
permissions:
contents: write
pull-requests: write
on: on:
pull_request_target: pull_request:
types: [opened, synchronize] types: [opened, synchronize]
jobs: jobs:

View File

@ -20,7 +20,7 @@
- Cloudflare - Cloudflare
- Docker - Docker
[演示站点](https://nezha-cf.buycoffee.top) [演示站点](https://nezha-vercel.vercel.app)
[说明文档](https://nezhadash-docs.vercel.app) [说明文档](https://nezhadash-docs.vercel.app)
### 如何更新 ### 如何更新

View File

@ -47,8 +47,8 @@ export function NetworkChartClient({
return ( return (
<> <>
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40">{error.message}</p> <p className="font-medium text-sm opacity-40">{error.message}</p>
<p className="text-sm font-medium opacity-40">{t("chart_fetch_error_message")}</p> <p className="font-medium text-sm opacity-40">{t("chart_fetch_error_message")}</p>
</div> </div>
<NetworkChartLoading /> <NetworkChartLoading />
</> </>
@ -123,12 +123,12 @@ export const NetworkChart = React.memo(function NetworkChart({
key={key} key={key}
data-active={activeChart === key} data-active={activeChart === key}
className={ className={
"relative z-30 flex cursor-pointer grow basis-0 flex-col justify-center gap-1 border-b border-neutral-200 dark:border-neutral-800 px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6" "relative z-30 flex grow basis-0 cursor-pointer flex-col justify-center gap-1 border-neutral-200 border-b px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-t-0 sm:border-l sm:px-6 dark:border-neutral-800"
} }
onClick={() => handleButtonClick(key)} onClick={() => handleButtonClick(key)}
> >
<span className="whitespace-nowrap text-xs text-muted-foreground">{key}</span> <span className="whitespace-nowrap text-muted-foreground text-xs">{key}</span>
<span className="text-md font-bold leading-none sm:text-lg"> <span className="font-bold text-md leading-none sm:text-lg">
{chartData[key][chartData[key].length - 1].avg_delay.toFixed(2)}ms {chartData[key][chartData[key].length - 1].avg_delay.toFixed(2)}ms
</span> </span>
</button> </button>
@ -269,16 +269,16 @@ export const NetworkChart = React.memo(function NetworkChart({
<CardDescription className="text-xs"> <CardDescription className="text-xs">
{chartDataKey.length} {t("ServerMonitorCount")} {chartDataKey.length} {t("ServerMonitorCount")}
</CardDescription> </CardDescription>
<div className="flex items-center mt-0.5 space-x-2"> <div className="mt-0.5 flex items-center space-x-2">
<Switch id="Peak" checked={isPeakEnabled} onCheckedChange={setIsPeakEnabled} /> <Switch id="Peak" checked={isPeakEnabled} onCheckedChange={setIsPeakEnabled} />
<Label className="text-xs" htmlFor="Peak"> <Label className="text-xs" htmlFor="Peak">
Peak cut Peak cut
</Label> </Label>
</div> </div>
</div> </div>
<div className="flex flex-wrap w-full">{chartButtons}</div> <div className="flex w-full flex-wrap">{chartButtons}</div>
</CardHeader> </CardHeader>
<CardContent className="pr-2 pl-0 py-4 sm:pt-6 sm:pb-6 sm:pr-6 sm:pl-2"> <CardContent className="py-4 pr-2 pl-0 sm:pt-6 sm:pr-6 sm:pb-6 sm:pl-2">
<ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full"> <ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full">
<LineChart accessibilityLayer data={processedData} margin={{ left: 12, right: 12 }}> <LineChart accessibilityLayer data={processedData} margin={{ left: 12, right: 12 }}>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
@ -289,16 +289,34 @@ export const NetworkChart = React.memo(function NetworkChart({
axisLine={false} axisLine={false}
tickMargin={8} tickMargin={8}
minTickGap={80} minTickGap={80}
interval={0}
ticks={processedData ticks={processedData
.filter((item) => { .filter((item, index, array) => {
if (array.length < 6) {
return index === 0 || index === array.length - 1
}
// 计算数据的总时间跨度(毫秒)
const timeSpan = array[array.length - 1].created_at - array[0].created_at
const hours = timeSpan / (1000 * 60 * 60)
// 根据时间跨度调整显示间隔
if (hours <= 12) {
// 12小时内每60分钟显示一个刻度
return (
index === 0 ||
index === array.length - 1 ||
new Date(item.created_at).getMinutes() % 60 === 0
)
}
// 超过12小时每2小时显示一个刻度
const date = new Date(item.created_at) const date = new Date(item.created_at)
return date.getMinutes() === 0 && date.getHours() % 3 === 0 return date.getMinutes() === 0 && date.getHours() % 2 === 0
}) })
.map((item) => item.created_at)} .map((item) => item.created_at)}
tickFormatter={(value) => { tickFormatter={(value) => {
const date = new Date(value) const date = new Date(value)
return `${date.getHours()}:00` const minutes = date.getMinutes()
return minutes === 0 ? `${date.getHours()}:00` : `${date.getHours()}:${minutes}`
}} }}
/> />
<YAxis <YAxis

View File

@ -64,8 +64,8 @@ export default function ServerDetailChartClient({
return ( return (
<> <>
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40">{error.message}</p> <p className="font-medium text-sm opacity-40">{error.message}</p>
<p className="text-sm font-medium opacity-40">{t("chart_fetch_error_message")}</p> <p className="font-medium text-sm opacity-40">{t("chart_fetch_error_message")}</p>
</div> </div>
</> </>
) )
@ -73,7 +73,7 @@ export default function ServerDetailChartClient({
if (!data) return <ServerDetailChartLoading /> if (!data) return <ServerDetailChartLoading />
return ( return (
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3"> <section className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
<CpuChart data={data} history={history} /> <CpuChart data={data} history={history} />
<ProcessChart data={data} history={history} /> <ProcessChart data={data} history={history} />
<DiskChart data={data} history={history} /> <DiskChart data={data} history={history} />
@ -150,9 +150,9 @@ function CpuChart({
<CardContent className="px-6 py-3"> <CardContent className="px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-md font-medium">CPU</p> <p className="font-medium text-md">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(0)}%</p> <p className="w-10 text-end font-medium text-xs">{cpu.toFixed(0)}%</p>
<AnimatedCircularProgressBar <AnimatedCircularProgressBar
className="size-3 text-[0px]" className="size-3 text-[0px]"
max={100} max={100}
@ -273,9 +273,9 @@ function ProcessChart({
<CardContent className="px-6 py-3"> <CardContent className="px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-md font-medium">{t("Process")}</p> <p className="font-medium text-md">{t("Process")}</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">{process}</p> <p className="w-10 text-end font-medium text-xs">{process}</p>
</section> </section>
</div> </div>
<ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full"> <ChartContainer config={chartConfig} className="aspect-auto h-[130px] w-full">
@ -388,7 +388,7 @@ function MemChart({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<section className="flex items-center gap-4"> <section className="flex items-center gap-4">
<div className="flex flex-col"> <div className="flex flex-col">
<p className=" text-xs text-muted-foreground">{t("Mem")}</p> <p className=" text-muted-foreground text-xs">{t("Mem")}</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AnimatedCircularProgressBar <AnimatedCircularProgressBar
className="size-3 text-[0px]" className="size-3 text-[0px]"
@ -397,11 +397,11 @@ function MemChart({
value={mem} value={mem}
primaryColor="hsl(var(--chart-8))" primaryColor="hsl(var(--chart-8))"
/> />
<p className="text-xs font-medium">{mem.toFixed(0)}%</p> <p className="font-medium text-xs">{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("Swap")}</p> <p className=" text-muted-foreground text-xs">{t("Swap")}</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AnimatedCircularProgressBar <AnimatedCircularProgressBar
className="size-3 text-[0px]" className="size-3 text-[0px]"
@ -410,15 +410,15 @@ function MemChart({
value={swap} value={swap}
primaryColor="hsl(var(--chart-10))" primaryColor="hsl(var(--chart-10))"
/> />
<p className="text-xs font-medium">{swap.toFixed(0)}%</p> <p className="font-medium text-xs">{swap.toFixed(0)}%</p>
</div> </div>
</div> </div>
</section> </section>
<section className="flex flex-col items-end gap-0.5"> <section className="flex flex-col items-end gap-0.5">
<div className="flex text-[11px] font-medium items-center gap-2"> <div className="flex items-center gap-2 font-medium text-[11px]">
{formatBytes(data.status.MemUsed)} / {formatBytes(data.host.MemTotal)} {formatBytes(data.status.MemUsed)} / {formatBytes(data.host.MemTotal)}
</div> </div>
<div className="flex text-[11px] font-medium items-center gap-2"> <div className="flex items-center gap-2 font-medium text-[11px]">
swap: {formatBytes(data.status.SwapUsed)} swap: {formatBytes(data.status.SwapUsed)}
</div> </div>
</section> </section>
@ -542,10 +542,10 @@ function DiskChart({
<CardContent className="px-6 py-3"> <CardContent className="px-6 py-3">
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-md font-medium">{t("Disk")}</p> <p className="font-medium text-md">{t("Disk")}</p>
<section className="flex flex-col items-end gap-0.5"> <section className="flex flex-col items-end gap-0.5">
<section className="flex items-center gap-2"> <section className="flex items-center gap-2">
<p className="text-xs text-end w-10 font-medium">{disk.toFixed(0)}%</p> <p className="w-10 text-end font-medium text-xs">{disk.toFixed(0)}%</p>
<AnimatedCircularProgressBar <AnimatedCircularProgressBar
className="size-3 text-[0px]" className="size-3 text-[0px]"
max={100} max={100}
@ -554,7 +554,7 @@ function DiskChart({
primaryColor="hsl(var(--chart-5))" primaryColor="hsl(var(--chart-5))"
/> />
</section> </section>
<div className="flex text-[11px] font-medium items-center gap-2"> <div className="flex items-center gap-2 font-medium text-[11px]">
{formatBytes(data.status.DiskUsed)} / {formatBytes(data.host.DiskTotal)} {formatBytes(data.status.DiskUsed)} / {formatBytes(data.host.DiskTotal)}
</div> </div>
</section> </section>
@ -681,18 +681,18 @@ function NetworkChart({
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<div className="flex items-center"> <div className="flex items-center">
<section className="flex items-center gap-4"> <section className="flex items-center gap-4">
<div className="flex flex-col w-20"> <div className="flex w-20 flex-col">
<p className="text-xs text-muted-foreground">{t("Upload")}</p> <p className="text-muted-foreground text-xs">{t("Upload")}</p>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]" /> <span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]" />
<p className="text-xs font-medium">{up.toFixed(2)} M/s</p> <p className="font-medium text-xs">{up.toFixed(2)} M/s</p>
</div> </div>
</div> </div>
<div className="flex flex-col w-20"> <div className="flex w-20 flex-col">
<p className=" text-xs text-muted-foreground">{t("Download")}</p> <p className=" text-muted-foreground text-xs">{t("Download")}</p>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]" /> <span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]" />
<p className="text-xs font-medium">{down.toFixed(2)} M/s</p> <p className="font-medium text-xs">{down.toFixed(2)} M/s</p>
</div> </div>
</div> </div>
</section> </section>
@ -823,18 +823,18 @@ function ConnectChart({
<section className="flex flex-col gap-1"> <section className="flex flex-col gap-1">
<div className="flex items-center"> <div className="flex items-center">
<section className="flex items-center gap-4"> <section className="flex items-center gap-4">
<div className="flex flex-col w-12"> <div className="flex w-12 flex-col">
<p className="text-xs text-muted-foreground">TCP</p> <p className="text-muted-foreground text-xs">TCP</p>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]" /> <span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-1))]" />
<p className="text-xs font-medium">{tcp}</p> <p className="font-medium text-xs">{tcp}</p>
</div> </div>
</div> </div>
<div className="flex flex-col w-12"> <div className="flex w-12 flex-col">
<p className=" text-xs text-muted-foreground">UDP</p> <p className=" text-muted-foreground text-xs">UDP</p>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]" /> <span className="relative inline-flex size-1.5 rounded-full bg-[hsl(var(--chart-4))]" />
<p className="text-xs font-medium">{udp}</p> <p className="font-medium text-xs">{udp}</p>
</div> </div>
</div> </div>
</section> </section>

View File

@ -55,8 +55,8 @@ export default function ServerDetailClient({
return ( return (
<> <>
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40">{error.message}</p> <p className="font-medium text-sm opacity-40">{error.message}</p>
<p className="text-sm font-medium opacity-40">{t("detail_fetch_error_message")}</p> <p className="font-medium text-sm opacity-40">{t("detail_fetch_error_message")}</p>
</div> </div>
</> </>
) )
@ -83,25 +83,26 @@ export default function ServerDetailClient({
net_out_transfer, net_out_transfer,
net_in_transfer, net_in_transfer,
last_active_time_string, last_active_time_string,
boot_time_string,
} = formatNezhaInfo(serverData) } = formatNezhaInfo(serverData)
return ( return (
<div> <div>
<div <div
onClick={linkClick} onClick={linkClick}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl hover:opacity-50 transition-opacity duration-300" className="flex flex-none cursor-pointer items-center gap-0.5 break-all font-semibold text-xl leading-none tracking-tight transition-opacity duration-300 hover:opacity-50"
> >
<BackIcon /> <BackIcon />
{name} {name}
</div> </div>
<section className="flex flex-wrap gap-2 mt-3"> <section className="mt-3 flex flex-wrap gap-2">
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("status")}</p> <p className="text-muted-foreground text-xs">{t("status")}</p>
<Badge <Badge
className={cn( className={cn(
"text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white", "-mt-[0.3px] w-fit rounded-[6px] px-1 py-0 text-[9px] dark:text-white",
{ {
" bg-green-800": online, " bg-green-800": online,
" bg-red-600": !online, " bg-red-600": !online,
@ -113,76 +114,76 @@ export default function ServerDetailClient({
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Uptime")}</p> <p className="text-muted-foreground text-xs">{t("Uptime")}</p>
<div className="text-xs"> <div className="text-xs">
{" "} {" "}
{uptime / 86400 >= 1 {uptime / 86400 >= 1
? `${(uptime / 86400).toFixed(0)} ${t("Days")}` ? `${Math.floor(uptime / 86400)} ${t("Days")} ${Math.floor((uptime % 86400) / 3600)} ${t("Hours")}`
: `${(uptime / 3600).toFixed(0)} ${t("Hours")}`} : `${Math.floor(uptime / 3600)} ${t("Hours")}`}
</div> </div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
{version && ( {version && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Version")}</p> <p className="text-muted-foreground text-xs">{t("Version")}</p>
<div className="text-xs">{version} </div> <div className="text-xs">{version} </div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{arch && ( {arch && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Arch")}</p> <p className="text-muted-foreground text-xs">{t("Arch")}</p>
<div className="text-xs">{arch} </div> <div className="text-xs">{arch} </div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Mem")}</p> <p className="text-muted-foreground text-xs">{t("Mem")}</p>
<div className="text-xs">{formatBytes(mem_total)}</div> <div className="text-xs">{formatBytes(mem_total)}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Disk")}</p> <p className="text-muted-foreground text-xs">{t("Disk")}</p>
<div className="text-xs">{formatBytes(disk_total)}</div> <div className="text-xs">{formatBytes(disk_total)}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
{country_code && ( {country_code && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Region")}</p> <p className="text-muted-foreground text-xs">{t("Region")}</p>
<section className="flex items-start gap-1"> <section className="flex items-start gap-1">
<div className="text-xs text-start">{countries.getName(country_code, "en")}</div> <div className="text-start text-xs">{countries.getName(country_code, "en")}</div>
<ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} /> <ServerFlag className="-mt-[1px] text-[11px]" country_code={country_code} />
</section> </section>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
</section> </section>
<section className="flex flex-wrap gap-2 mt-1"> <section className="mt-1 flex flex-wrap gap-2">
{platform && ( {platform && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("System")}</p> <p className="text-muted-foreground text-xs">{t("System")}</p>
<div className="text-xs"> <div className="text-xs">
{" "} {" "}
@ -193,10 +194,10 @@ export default function ServerDetailClient({
</Card> </Card>
)} )}
{cpu_info && cpu_info.length > 0 && ( {cpu_info && cpu_info.length > 0 && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("CPU")}</p> <p className="text-muted-foreground text-xs">{t("CPU")}</p>
<div className="text-xs"> {cpu_info.join(", ")}</div> <div className="text-xs"> {cpu_info.join(", ")}</div>
</section> </section>
@ -204,31 +205,31 @@ export default function ServerDetailClient({
</Card> </Card>
)} )}
{gpu_info && gpu_info.length > 0 && ( {gpu_info && gpu_info.length > 0 && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"GPU"}</p> <p className="text-muted-foreground text-xs">{"GPU"}</p>
<div className="text-xs"> {gpu_info.join(", ")}</div> <div className="text-xs"> {gpu_info.join(", ")}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
</section> </section>
<section className="flex flex-wrap gap-2 mt-1"> <section className="mt-1 flex flex-wrap gap-2">
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Load")}</p> <p className="text-muted-foreground text-xs">{t("Load")}</p>
<div className="text-xs"> <div className="text-xs">
{load_1 || "0.00"} / {load_5 || "0.00"} / {load_15 || "0.00"} {load_1 || "0.00"} / {load_5 || "0.00"} / {load_15 || "0.00"}
</div> </div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Upload")}</p> <p className="text-muted-foreground text-xs">{t("Upload")}</p>
{net_out_transfer ? ( {net_out_transfer ? (
<div className="text-xs"> {formatBytes(net_out_transfer)} </div> <div className="text-xs"> {formatBytes(net_out_transfer)} </div>
) : ( ) : (
@ -237,10 +238,10 @@ export default function ServerDetailClient({
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("Download")}</p> <p className="text-muted-foreground text-xs">{t("Download")}</p>
{net_in_transfer ? ( {net_in_transfer ? (
<div className="text-xs"> {formatBytes(net_in_transfer)} </div> <div className="text-xs"> {formatBytes(net_in_transfer)} </div>
) : ( ) : (
@ -250,11 +251,19 @@ export default function ServerDetailClient({
</CardContent> </CardContent>
</Card> </Card>
</section> </section>
<section className="flex flex-wrap gap-2 mt-1"> <section className="mt-1 flex flex-wrap gap-2">
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("LastActive")}</p> <p className="text-muted-foreground text-xs">{t("BootTime")}</p>
<div className="text-xs">{boot_time_string ? boot_time_string : "N/A"}</div>
</section>
</CardContent>
</Card>
<Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-muted-foreground text-xs">{t("LastActive")}</p>
<div className="text-xs"> <div className="text-xs">
{last_active_time_string ? last_active_time_string : "N/A"} {last_active_time_string ? last_active_time_string : "N/A"}
</div> </div>

View File

@ -22,92 +22,92 @@ export default function ServerIPInfo({ server_id }: { server_id: number }) {
return ( return (
<> <>
<section className="flex flex-wrap gap-2 mb-4"> <section className="mb-4 flex flex-wrap gap-2">
{data.asn?.autonomous_system_organization && ( {data.asn?.autonomous_system_organization && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"ASN"}</p> <p className="text-muted-foreground text-xs">{"ASN"}</p>
<div className="text-xs">{data.asn.autonomous_system_organization}</div> <div className="text-xs">{data.asn.autonomous_system_organization}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{data.asn?.autonomous_system_number && ( {data.asn?.autonomous_system_number && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("asn_number")}</p> <p className="text-muted-foreground text-xs">{t("asn_number")}</p>
<div className="text-xs">AS{data.asn.autonomous_system_number}</div> <div className="text-xs">AS{data.asn.autonomous_system_number}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{data.city?.registered_country?.names.en && ( {data.city?.registered_country?.names.en && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("registered_country")}</p> <p className="text-muted-foreground text-xs">{t("registered_country")}</p>
<div className="text-xs">{data.city.registered_country?.names.en}</div> <div className="text-xs">{data.city.registered_country?.names.en}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{data.city?.country?.iso_code && ( {data.city?.country?.iso_code && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"ISO"}</p> <p className="text-muted-foreground text-xs">{"ISO"}</p>
<div className="text-xs">{data.city.country?.iso_code}</div> <div className="text-xs">{data.city.country?.iso_code}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{data.city?.city?.names.en && ( {data.city?.city?.names.en && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("city")}</p> <p className="text-muted-foreground text-xs">{t("city")}</p>
<div className="text-xs">{data.city.city?.names.en}</div> <div className="text-xs">{data.city.city?.names.en}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{data.city?.location?.longitude && ( {data.city?.location?.longitude && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("longitude")}</p> <p className="text-muted-foreground text-xs">{t("longitude")}</p>
<div className="text-xs">{data.city.location?.longitude}</div> <div className="text-xs">{data.city.location?.longitude}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{data.city?.location?.latitude && ( {data.city?.location?.latitude && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("latitude")}</p> <p className="text-muted-foreground text-xs">{t("latitude")}</p>
<div className="text-xs">{data.city.location?.latitude}</div> <div className="text-xs">{data.city.location?.latitude}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{data.city?.location?.time_zone && ( {data.city?.location?.time_zone && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("time_zone")}</p> <p className="text-muted-foreground text-xs">{t("time_zone")}</p>
<div className="text-xs">{data.city.location?.time_zone}</div> <div className="text-xs">{data.city.location?.time_zone}</div>
</section> </section>
</CardContent> </CardContent>
</Card> </Card>
)} )}
{data.city?.postal && ( {data.city?.postal && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] border-none bg-transparent shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("postal_code")}</p> <p className="text-muted-foreground text-xs">{t("postal_code")}</p>
<div className="text-xs">{data.city.postal?.code}</div> <div className="text-xs">{data.city.postal?.code}</div>
</section> </section>
</CardContent> </CardContent>

View File

@ -13,7 +13,7 @@ export default function ServerGlobal() {
if (error) if (error)
return ( return (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40">{error.message}</p> <p className="font-medium text-sm opacity-40">{error.message}</p>
</div> </div>
) )
@ -43,7 +43,7 @@ export default function ServerGlobal() {
) )
return ( return (
<section className="flex flex-col gap-4 mt-[3.2px]"> <section className="mt-[3.2px] flex flex-col gap-4">
<GlobalInfo countries={countryList} /> <GlobalInfo countries={countryList} />
<div className="w-full overflow-x-auto"> <div className="w-full overflow-x-auto">
<TooltipProvider> <TooltipProvider>

View File

@ -10,7 +10,7 @@ export default function GlobalInfo({ countries }: GlobalInfoProps) {
const t = useTranslations("Global") const t = useTranslations("Global")
return ( return (
<section className="flex items-center justify-between"> <section className="flex items-center justify-between">
<p className="text-sm font-medium opacity-40"> <p className="font-medium text-sm opacity-40">
{t("Distributions")} {countries.length} {t("Regions")} {t("Distributions")} {countries.length} {t("Regions")}
</p> </p>
</section> </section>

View File

@ -32,13 +32,13 @@ export function InteractiveMap({
const path = geoPath().projection(projection) const path = geoPath().projection(projection)
return ( return (
<div className="relative w-full aspect-[2/1]" onMouseLeave={() => setTooltipData(null)}> <div className="relative aspect-[2/1] w-full" onMouseLeave={() => setTooltipData(null)}>
<svg <svg
width={width} width={width}
height={height} height={height}
viewBox={`0 0 ${width} ${height}`} viewBox={`0 0 ${width} ${height}`}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto" className="h-auto w-full"
> >
<title>Interactive Map</title> <title>Interactive Map</title>
<defs> <defs>
@ -56,19 +56,19 @@ export function InteractiveMap({
fill="transparent" fill="transparent"
onMouseEnter={() => setTooltipData(null)} onMouseEnter={() => setTooltipData(null)}
/> />
{filteredFeatures.map((feature) => { {filteredFeatures.map((feature, index) => {
const isHighlighted = countries.includes(feature.properties.iso_a2_eh) const isHighlighted = countries.includes(feature.properties.iso_a2_eh)
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0 const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0
return ( return (
<path <path
key={feature.properties.iso_a2_eh} key={feature.properties.iso_a2_eh + String(index)}
d={path(feature) || ""} d={path(feature) || ""}
className={ className={
isHighlighted isHighlighted
? "fill-green-700 hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700 transition-all cursor-pointer" ? "cursor-pointer fill-green-700 transition-all hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700"
: "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]" : "fill-neutral-200/50 stroke-[0.5] stroke-neutral-300/40 dark:fill-neutral-800 dark:stroke-neutral-700"
} }
onMouseEnter={() => { onMouseEnter={() => {
if (!isHighlighted) { if (!isHighlighted) {
@ -82,6 +82,7 @@ export function InteractiveMap({
(server: any) => server.host.CountryCode?.toUpperCase() === countryCode, (server: any) => server.host.CountryCode?.toUpperCase() === countryCode,
) )
.map((server: any) => ({ .map((server: any) => ({
id: server.id,
name: server.name, name: server.name,
status: server.online_status, status: server.online_status,
})) }))
@ -122,6 +123,7 @@ export function InteractiveMap({
const countryServers = nezhaServerList.result const countryServers = nezhaServerList.result
.filter((server: any) => server.host.CountryCode?.toUpperCase() === countryCode) .filter((server: any) => server.host.CountryCode?.toUpperCase() === countryCode)
.map((server: any) => ({ .map((server: any) => ({
id: server.id,
name: server.name, name: server.name,
status: server.online_status, status: server.online_status,
})) }))
@ -138,7 +140,7 @@ export function InteractiveMap({
cx={x} cx={x}
cy={y} cy={y}
r={4} r={4}
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all" className="fill-sky-700 stroke-white transition-all hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700"
/> />
</g> </g>
) )

View File

@ -2,6 +2,7 @@
import { useTooltip } from "@/app/context/tooltip-context" import { useTooltip } from "@/app/context/tooltip-context"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import Link from "next/link"
import { memo } from "react" import { memo } from "react"
const MapTooltip = memo(function MapTooltip() { const MapTooltip = memo(function MapTooltip() {
@ -14,9 +15,13 @@ const MapTooltip = memo(function MapTooltip() {
return a.status === b.status ? 0 : a.status ? 1 : -1 return a.status === b.status ? 0 : a.status ? 1 : -1
}) })
const saveSession = () => {
sessionStorage.setItem("fromMainPage", "true")
}
return ( return (
<div <div
className="absolute hidden lg:block bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700 z-50 tooltip-animate" className="tooltip-animate absolute z-50 hidden rounded bg-white px-2 py-1 text-sm shadow-lg lg:block dark:border dark:border-neutral-700 dark:bg-neutral-800"
key={tooltipData.country} key={tooltipData.country}
style={{ style={{
left: tooltipData.centroid[0], left: tooltipData.centroid[0],
@ -31,26 +36,31 @@ const MapTooltip = memo(function MapTooltip() {
<p className="font-medium"> <p className="font-medium">
{tooltipData.country === "China" ? "Mainland China" : tooltipData.country} {tooltipData.country === "China" ? "Mainland China" : tooltipData.country}
</p> </p>
<p className="text-neutral-600 dark:text-neutral-400 mb-1"> <p className="mb-1 font-light text-neutral-600 text-xs dark:text-neutral-400">
{tooltipData.count} {t("Servers")} {tooltipData.count} {t("Servers")}
</p> </p>
</div> </div>
<div <div
className="border-t dark:border-neutral-700 pt-1" className="border-t pt-1 dark:border-neutral-700"
style={{ style={{
maxHeight: "200px", maxHeight: "200px",
overflowY: "auto", overflowY: "auto",
}} }}
> >
{sortedServers.map((server) => ( {sortedServers.map((server) => (
<div key={server.name} className="flex items-center gap-1.5 py-0.5"> <Link
onClick={saveSession}
href={`/server/${server.id}`}
key={server.name}
className="flex items-center gap-1.5 py-0.5 text-neutral-500 transition-colors hover:text-black dark:text-neutral-400 dark:hover:text-white"
>
<span <span
className={`w-1.5 h-1.5 shrink-0 rounded-full ${ className={`h-1.5 w-1.5 shrink-0 rounded-full ${
server.status ? "bg-green-500" : "bg-red-500" server.status ? "bg-green-500" : "bg-red-500"
}`} }`}
/> />
<span className="text-xs">{server.name}</span> <span className="text-xs">{server.name}</span>
</div> </Link>
))} ))}
</div> </div>
</div> </div>

View File

@ -56,8 +56,8 @@ const getTagCounts = (servers: any[]) => {
} }
const LoadingState = ({ t }: { t: any }) => ( const LoadingState = ({ t }: { t: any }) => (
<div className="flex flex-col items-center min-h-96 justify-center "> <div className="flex min-h-96 flex-col items-center justify-center ">
<div className="font-semibold flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 font-semibold text-sm">
<Loader visible={true} /> <Loader visible={true} />
{t("connecting")}... {t("connecting")}...
</div> </div>
@ -66,8 +66,8 @@ const LoadingState = ({ t }: { t: any }) => (
const ErrorState = ({ error, t }: { error: Error; t: any }) => ( const ErrorState = ({ error, t }: { error: Error; t: any }) => (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40">{error.message}</p> <p className="font-medium text-sm opacity-40">{error.message}</p>
<p className="text-sm font-medium opacity-40">{t("error_message")}</p> <p className="font-medium text-sm opacity-40">{t("error_message")}</p>
</div> </div>
) )
@ -80,7 +80,7 @@ const ServerList = ({
return ( return (
<section <section
ref={containerRef} ref={containerRef}
className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden" className="scrollbar-hidden flex flex-col gap-2 overflow-x-scroll"
> >
{servers.map((serverInfo) => ( {servers.map((serverInfo) => (
<ServerCardInline key={serverInfo.id} serverInfo={serverInfo} /> <ServerCardInline key={serverInfo.id} serverInfo={serverInfo} />
@ -115,6 +115,11 @@ export default function ServerListClient() {
setInline(inlineState) setInline(inlineState)
} }
const showMapState = localStorage.getItem("showMap")
if (showMapState !== null) {
setShowMap(showMapState === "true")
}
const savedTag = sessionStorage.getItem("selectedTag") || defaultTag const savedTag = sessionStorage.getItem("selectedTag") || defaultTag
setTag(savedTag) setTag(savedTag)
restoreScrollPosition() restoreScrollPosition()
@ -166,14 +171,19 @@ export default function ServerListClient() {
return ( return (
<> <>
<section className="flex items-center gap-2 w-full overflow-hidden"> <section className="flex w-full items-center gap-2 overflow-hidden">
<button <button
type="button" type="button"
onClick={() => setShowMap(!showMap)} onClick={() => {
const newShowMap = !showMap
setShowMap(newShowMap)
localStorage.setItem("showMap", String(newShowMap))
}}
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)]", "inset-shadow-2xs inset-shadow-white/20 flex cursor-pointer flex-col items-center gap-0 rounded-[50px] bg-blue-100 p-[10px] text-blue-600 transition-all dark:bg-blue-900 dark:text-blue-100 ",
{ {
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": showMap, "inset-shadow-black/20 bg-blue-600 text-white dark:bg-blue-100 dark:text-blue-600":
showMap,
}, },
)} )}
> >
@ -187,9 +197,10 @@ export default function ServerListClient() {
localStorage.setItem("inline", 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)]", "inset-shadow-2xs inset-shadow-white/20 flex cursor-pointer flex-col items-center gap-0 rounded-[50px] bg-blue-100 p-[10px] text-blue-600 transition-all dark:bg-blue-900 dark:text-blue-100 ",
{ {
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500": inline === "1", "inset-shadow-black/20 bg-blue-600 text-white dark:bg-blue-100 dark:text-blue-600":
inline === "1",
}, },
)} )}
> >

View File

@ -3,6 +3,7 @@
import { useFilter } from "@/app/context/network-filter-context" import { useFilter } from "@/app/context/network-filter-context"
import { useServerData } from "@/app/context/server-data-context" import { useServerData } from "@/app/context/server-data-context"
import { useStatus } from "@/app/context/status-context" import { useStatus } from "@/app/context/status-context"
import AnimateCountClient from "@/components/AnimatedCount"
import { Loader } from "@/components/loading/Loader" import { Loader } from "@/components/loading/Loader"
import { Card, CardContent } from "@/components/ui/card" import { Card, CardContent } from "@/components/ui/card"
import getEnv from "@/lib/env-entry" import getEnv from "@/lib/env-entry"
@ -23,10 +24,10 @@ export default function ServerOverviewClient() {
const errorInfo = error as any const errorInfo = error as any
return ( return (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40"> <p className="font-medium text-sm opacity-40">
Error status:{errorInfo?.status} {errorInfo.info?.cause ?? errorInfo?.message} Error status:{errorInfo?.status} {errorInfo.info?.cause ?? errorInfo?.message}
</p> </p>
<p className="text-sm font-medium opacity-40">{t("error_message")}</p> <p className="font-medium text-sm opacity-40">{t("error_message")}</p>
</div> </div>
) )
} }
@ -39,17 +40,19 @@ export default function ServerOverviewClient() {
setFilter(false) setFilter(false)
setStatus("all") setStatus("all")
}} }}
className={cn("cursor-pointer hover:border-blue-500 transition-all group")} className={cn("group cursor-pointer transition-all hover:border-blue-500")}
> >
<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">{t("p_816-881_Totalservers")}</p> <p className="font-medium text-sm md:text-base">{t("p_816-881_Totalservers")}</p>
<div className="flex items-center gap-2"> <div className="flex min-h-[28px] items-center gap-2">
<span className="relative flex h-2 w-2"> <span className="relative flex h-2 w-2">
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500" /> <span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500" />
</span> </span>
{data?.result ? ( {data?.result ? (
<div className="text-lg font-semibold">{data?.result.length}</div> <div className="font-semibold text-lg">
<AnimateCountClient count={data?.result.length} />
</div>
) : ( ) : (
<div className="flex h-7 items-center"> <div className="flex h-7 items-center">
<Loader visible={true} /> <Loader visible={true} />
@ -65,22 +68,24 @@ export default function ServerOverviewClient() {
setStatus("online") setStatus("online")
}} }}
className={cn( className={cn(
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all", "cursor-pointer ring-1 ring-transparent transition-all hover:ring-green-500",
{ {
"ring-green-500 ring-2 border-transparent": status === "online", "border-transparent ring-2 ring-green-500": status === "online",
}, },
)} )}
> >
<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">{t("p_1610-1676_Onlineservers")}</p> <p className="font-medium text-sm md:text-base">{t("p_1610-1676_Onlineservers")}</p>
<div className="flex items-center gap-2"> <div className="flex min-h-[28px] 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 className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500" /> <span className="relative inline-flex h-2 w-2 rounded-full bg-green-500" />
</span> </span>
{data?.result ? ( {data?.result ? (
<div className="text-lg font-semibold">{data?.live_servers}</div> <div className="font-semibold text-lg">
<AnimateCountClient count={data?.live_servers} />
</div>
) : ( ) : (
<div className="flex h-7 items-center"> <div className="flex h-7 items-center">
<Loader visible={true} /> <Loader visible={true} />
@ -96,22 +101,24 @@ export default function ServerOverviewClient() {
setStatus("offline") setStatus("offline")
}} }}
className={cn( className={cn(
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all", "cursor-pointer ring-1 ring-transparent transition-all hover:ring-red-500",
{ {
"ring-red-500 ring-2 border-transparent": status === "offline", "border-transparent ring-2 ring-red-500": status === "offline",
}, },
)} )}
> >
<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">{t("p_2532-2599_Offlineservers")}</p> <p className="font-medium text-sm md:text-base">{t("p_2532-2599_Offlineservers")}</p>
<div className="flex items-center gap-2"> <div className="flex min-h-[28px] 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 className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500" /> <span className="relative inline-flex h-2 w-2 rounded-full bg-red-500" />
</span> </span>
{data?.result ? ( {data?.result ? (
<div className="text-lg font-semibold">{data?.offline_servers}</div> <div className="font-semibold text-lg">
<AnimateCountClient count={data?.offline_servers} />
</div>
) : ( ) : (
<div className="flex h-7 items-center"> <div className="flex h-7 items-center">
<Loader visible={true} /> <Loader visible={true} />
@ -127,34 +134,34 @@ export default function ServerOverviewClient() {
setFilter(true) setFilter(true)
}} }}
className={cn( className={cn(
"cursor-pointer hover:ring-purple-500 ring-1 ring-transparent transition-all group", "group cursor-pointer ring-1 ring-transparent transition-all hover:ring-purple-500",
{ {
"ring-purple-500 ring-2 border-transparent": filter === true, "border-transparent ring-2 ring-purple-500": filter === true,
}, },
)} )}
> >
<CardContent className="flex h-full items-center relative px-6 py-3"> <CardContent className="relative flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1 w-full"> <section className="flex w-full flex-col gap-1">
<div className="flex items-center w-full justify-between"> <div className="flex w-full items-center justify-between">
<p className="text-sm font-medium md:text-base">{t("network")}</p> <p className="font-medium text-sm md:text-base">{t("network")}</p>
</div> </div>
{data?.result ? ( {data?.result ? (
<> <>
<section className="flex flex-row flex-wrap items-start pr-0 gap-1"> <section className="flex flex-row flex-wrap items-start gap-1 pr-0">
<p className="text-[12px] text-blue-800 dark:text-blue-400 text-nowrap font-medium"> <p className="text-nowrap font-medium text-[12px] text-blue-800 dark:text-blue-400">
{formatBytes(data?.total_out_bandwidth)} {formatBytes(data?.total_out_bandwidth)}
</p> </p>
<p className="text-[12px] text-purple-800 dark:text-purple-400 text-nowrap font-medium"> <p className="text-nowrap font-medium text-[12px] text-purple-800 dark:text-purple-400">
{formatBytes(data?.total_in_bandwidth)} {formatBytes(data?.total_in_bandwidth)}
</p> </p>
</section> </section>
<section className="flex flex-row flex-wrap -mr-1 sm:items-center items-start gap-1"> <section className="-mr-1 flex flex-row flex-wrap items-start gap-1 sm:items-center">
<p className="text-[11px] flex items-center text-nowrap font-semibold"> <p className="flex items-center text-nowrap font-semibold text-[11px]">
<ArrowUpCircleIcon className="size-3 mr-0.5 sm:mb-[1px]" /> <ArrowUpCircleIcon className="mr-0.5 size-3 sm:mb-[1px]" />
{formatBytes(data?.total_out_speed)}/s {formatBytes(data?.total_out_speed)}/s
</p> </p>
<p className="text-[11px] flex items-center text-nowrap font-semibold"> <p className="flex items-center text-nowrap font-semibold text-[11px]">
<ArrowDownCircleIcon className="size-3 mr-0.5" /> <ArrowDownCircleIcon className="mr-0.5 size-3" />
{formatBytes(data?.total_in_speed)}/s {formatBytes(data?.total_in_speed)}/s
</p> </p>
</section> </section>
@ -167,7 +174,7 @@ export default function ServerOverviewClient() {
</section> </section>
{!disableCartoon && ( {!disableCartoon && (
<Image <Image
className="absolute right-3 top-[-85px] z-50 w-20 scale-90 group-hover:opacity-50 md:scale-100 transition-all" className="absolute top-[-85px] right-3 z-50 w-20 scale-90 transition-all group-hover:opacity-50 md:scale-100"
alt={"Hamster1963"} alt={"Hamster1963"}
src={blogMan} src={blogMan}
priority priority
@ -179,7 +186,7 @@ export default function ServerOverviewClient() {
</section> </section>
{data?.result === undefined && !isLoading && ( {data?.result === undefined && !isLoading && (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40">{t("error_message")}</p> <p className="font-medium text-sm opacity-40">{t("error_message")}</p>
</div> </div>
)} )}
</> </>

View File

@ -15,7 +15,7 @@ const FooterLink = ({ href, children }: LinkProps) => (
<a <a
href={href} href={href}
target="_blank" 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" className="cursor-pointer font-normal underline decoration-2 decoration-yellow-500 underline-offset-2 transition-colors hover:decoration-yellow-600 dark:decoration-yellow-500/60 dark:hover:decoration-yellow-500/80"
rel="noreferrer" rel="noreferrer"
> >
{children} {children}
@ -36,7 +36,7 @@ export default function Footer() {
}, []) }, [])
return ( return (
<footer className="mx-auto w-full max-w-5xl flex items-center justify-between"> <footer className="mx-auto flex w-full max-w-5xl items-center justify-between">
<section className="flex flex-col"> <section className="flex flex-col">
<p className={`mt-3 flex gap-1 ${baseTextStyles}`}> <p className={`mt-3 flex gap-1 ${baseTextStyles}`}>
{t("p_146-598_Findthecodeon")}{" "} {t("p_146-598_Findthecodeon")}{" "}
@ -49,7 +49,7 @@ export default function Footer() {
</section> </section>
</section> </section>
<p className={`mt-1 ${baseTextStyles}`}> <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-medium font-mono text-[10px] text-muted-foreground opacity-100">
{isMac ? <span className="text-xs"></span> : "Ctrl "}K {isMac ? <span className="text-xs"></span> : "Ctrl "}K
</kbd> </kbd>
</p> </p>

View File

@ -1,11 +1,12 @@
"use client" "use client"
import AnimateCountClient from "@/components/AnimatedCount"
import { LanguageSwitcher } from "@/components/LanguageSwitcher" import { LanguageSwitcher } from "@/components/LanguageSwitcher"
import { ModeToggle } from "@/components/ThemeSwitcher" import { ModeToggle } from "@/components/ThemeSwitcher"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { Skeleton } from "@/components/ui/skeleton"
import getEnv from "@/lib/env-entry" import getEnv from "@/lib/env-entry"
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"
@ -30,7 +31,7 @@ const useCurrentTime = () => {
}) })
useEffect(() => { useEffect(() => {
const timer = setInterval(() => { const intervalId = setInterval(() => {
const now = DateTime.now().setLocale("en-US") const now = DateTime.now().setLocale("en-US")
setTime({ setTime({
hh: now.hour, hh: now.hour,
@ -39,7 +40,7 @@ const useCurrentTime = () => {
}) })
}, 1000) }, 1000)
return () => clearInterval(timer) return () => clearInterval(intervalId)
}, []) }, [])
return time return time
@ -59,7 +60,7 @@ const Links = memo(function Links() {
href={link.link} href={link.link}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100" className="flex items-center gap-1 font-medium text-sm opacity-50 transition-opacity hover:opacity-100"
> >
{link.name} {link.name}
</a> </a>
@ -71,28 +72,30 @@ const Links = memo(function Links() {
const Overview = memo(function Overview() { const Overview = memo(function Overview() {
const t = useTranslations("Overview") const t = useTranslations("Overview")
const time = useCurrentTime() const time = useCurrentTime()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
return ( return (
<section className={"mt-10 flex flex-col md:mt-16"}> <section className={"mt-10 flex flex-col md:mt-16"}>
<p className="text-base font-semibold">{t("p_2277-2331_Overview")}</p> <p className="font-semibold text-base">{t("p_2277-2331_Overview")}</p>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1">
<p className="text-sm font-medium opacity-50">{t("p_2390-2457_wherethetimeis")}</p> <p className="font-medium text-sm opacity-50">{t("p_2390-2457_wherethetimeis")}</p>
<NumberFlowGroup> {mounted ? (
<div <div className="flex items-center font-medium text-sm">
style={{ fontVariantNumeric: "tabular-nums" }} <AnimateCountClient count={time.hh} minDigits={2} />
className="flex text-sm font-medium mt-0.5" <span className="mb-[1px] font-medium text-sm opacity-50">:</span>
> <AnimateCountClient count={time.mm} minDigits={2} />
<NumberFlow trend={1} value={time.hh} format={{ minimumIntegerDigits: 2 }} /> <span className="mb-[1px] font-medium text-sm opacity-50">:</span>
<NumberFlow <span className="font-medium text-sm">
prefix=":" <AnimateCountClient count={time.ss} minDigits={2} />
trend={1} </span>
value={time.mm}
digits={{ 1: { max: 5 } }}
format={{ minimumIntegerDigits: 2 }}
/>
<p className="mt-[0.5px]">:{time.ss.toString().padStart(2, "0")}</p>
</div> </div>
</NumberFlowGroup> ) : (
<Skeleton className="h-[21px] w-16 animate-none rounded-[5px] bg-muted-foreground/10" />
)}
</div> </div>
</section> </section>
) )
@ -116,7 +119,7 @@ function Header() {
<section className="flex items-center justify-between"> <section className="flex items-center justify-between">
<section <section
onClick={handleLogoClick} onClick={handleLogoClick}
className="flex cursor-pointer items-center text-base font-medium hover:opacity-50 transition-opacity duration-300" className="flex cursor-pointer items-center font-medium text-base transition-opacity duration-300 hover:opacity-50"
> >
<div className="mr-1 flex flex-row items-center justify-start"> <div className="mr-1 flex flex-row items-center justify-start">
<img <img
@ -124,19 +127,19 @@ function Header() {
height={40} height={40}
alt="apple-touch-icon" alt="apple-touch-icon"
src={customLogo ? customLogo : "/apple-touch-icon.png"} src={customLogo ? customLogo : "/apple-touch-icon.png"}
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0! dark:hidden" className="relative m-0! h-6 w-6 border-2 border-transparent object-cover object-top p-0! dark:hidden"
/> />
<img <img
width={40} width={40}
height={40} height={40}
alt="apple-touch-icon" alt="apple-touch-icon"
src={customLogo ? customLogo : "/apple-touch-icon-dark.png"} src={customLogo ? customLogo : "/apple-touch-icon-dark.png"}
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0! hidden dark:block" className="relative m-0! hidden h-6 w-6 border-2 border-transparent object-cover object-top p-0! dark:block"
/> />
</div> </div>
{customTitle ? customTitle : "NezhaDash"} {customTitle ? customTitle : "NezhaDash"}
<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"> <p className="hidden font-medium text-sm opacity-40 md:block">
{customDescription ? customDescription : t("p_1079-1199_Simpleandbeautifuldashbo")} {customDescription ? customDescription : t("p_1079-1199_Simpleandbeautifuldashbo")}
</p> </p>
</section> </section>
@ -148,7 +151,7 @@ function Header() {
<ModeToggle /> <ModeToggle />
</section> </section>
</section> </section>
<div className="w-full flex justify-end sm:hidden mt-1"> <div className="mt-1 flex w-full justify-end sm:hidden">
<Links /> <Links />
</div> </div>
<Overview /> <Overview />

View File

@ -35,9 +35,9 @@ export default function Page({ params }: PageProps) {
<main 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={serverId} /> <ServerDetailClient server_id={serverId} />
<nav className="flex items-center my-2 w-full"> <nav className="my-2 flex w-full items-center">
<Separator className="flex-1" /> <Separator className="flex-1" />
<div className="flex justify-center w-full max-w-[200px]"> <div className="flex w-full max-w-[200px] justify-center">
<TabSwitch <TabSwitch
tabs={tabs} tabs={tabs}
currentTab={currentTab} currentTab={currentTab}

View File

@ -7,6 +7,7 @@ export interface TooltipData {
country: string country: string
count: number count: number
servers: Array<{ servers: Array<{
id: string
name: string name: string
status: boolean status: boolean
}> }>

View File

View File

@ -9,10 +9,10 @@ export default function NotFoundPage() {
<div className="flex min-h-screen w-full flex-col"> <div className="flex min-h-screen w-full flex-col">
<main className="flex min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 bg-background p-4 md:p-10 md:pt-8"> <main className="flex min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 bg-background p-4 md:p-10 md:pt-8">
<Header /> <Header />
<section className="flex flex-col items-center flex-1 justify-center gap-2"> <section className="flex flex-1 flex-col items-center justify-center gap-2">
<p className="text-sm font-semibold">{t("h1_490-590_404NotFound")}</p> <p className="font-semibold text-sm">{t("h1_490-590_404NotFound")}</p>
<Link href="/" className="flex items-center gap-1"> <Link href="/" className="flex items-center gap-1">
<p className="text-sm font-medium opacity-40">{t("h1_490-590_404NotFoundBack")}</p> <p className="font-medium text-sm opacity-40">{t("h1_490-590_404NotFoundBack")}</p>
</Link> </Link>
</section> </section>
<Footer /> <Footer />

View File

@ -17,8 +17,12 @@
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true, "recommended": true,
"nursery": {
"useSortedClasses": "error"
},
"a11y": { "a11y": {
"useKeyWithClickEvents": "off" "useKeyWithClickEvents": "off",
"noLabelWithoutControl": "off"
}, },
"security": { "security": {
"noDangerouslySetInnerHtml": "off" "noDangerouslySetInnerHtml": "off"

BIN
bun.lockb

Binary file not shown.

View File

@ -0,0 +1,96 @@
import { cn } from "@/lib/utils"
import { useEffect, useState } from "react"
export default function AnimateCountClient({
count,
className,
minDigits,
}: {
count: number
className?: string
minDigits?: number
}) {
const [previousCount, setPreviousCount] = useState(count)
useEffect(() => {
if (count !== previousCount) {
setTimeout(() => {
setPreviousCount(count)
}, 300)
}
}, [count])
return (
<AnimateCount
key={count}
preCount={previousCount}
className={cn("inline-flex items-center leading-none", className)}
minDigits={minDigits}
data-issues-count-animation
>
{count}
</AnimateCount>
)
}
export function AnimateCount({
children: count,
className,
preCount,
minDigits = 1,
...props
}: {
children: number
className?: string
preCount?: number
minDigits?: number
}) {
const currentDigits = count.toString().split("")
const previousDigits = (
preCount !== undefined ? preCount.toString() : count - 1 >= 0 ? (count - 1).toString() : "0"
).split("")
// Ensure both numbers meet the minimum length requirement and maintain the same length for animation
const maxLength = Math.max(previousDigits.length, currentDigits.length, minDigits)
while (previousDigits.length < maxLength) {
previousDigits.unshift("0")
}
while (currentDigits.length < maxLength) {
currentDigits.unshift("0")
}
return (
<div {...props} className={cn("flex h-[1em] items-center", className)}>
{currentDigits.map((digit, index) => {
const hasChanged = digit !== previousDigits[index]
return (
<div
key={`${index}-${digit}`}
className={cn("relative flex h-full min-w-[0.6em] items-center text-center", {
"min-w-[0.2em]": digit === ".",
})}
>
<div
aria-hidden
data-issues-count-exit
className={cn(
"absolute inset-0 flex items-center justify-center",
hasChanged ? "animate" : "opacity-0",
)}
>
{previousDigits[index]}
</div>
<div
data-issues-count-enter
className={cn(
"absolute inset-0 flex items-center justify-center",
hasChanged && "animate",
)}
>
{digit}
</div>
</div>
)
})}
</div>
)
}

View File

@ -103,9 +103,9 @@ export function DashCommand() {
}} }}
> >
{server.online_status ? ( {server.online_status ? (
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center" /> <span className="h-2 w-2 shrink-0 self-center rounded-full bg-green-500" />
) : ( ) : (
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" /> <span className="h-2 w-2 shrink-0 self-center rounded-full bg-red-500" />
)} )}
<span>{server.name}</span> <span>{server.name}</span>
</CommandItem> </CommandItem>

View File

@ -27,7 +27,7 @@ export function LanguageSwitcher() {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="rounded-full px-[9px] bg-white dark:bg-black cursor-pointer hover:bg-accent/50 dark:hover:bg-accent/50" className="cursor-pointer rounded-full bg-white px-[9px] hover:bg-accent/50 dark:bg-black dark:hover:bg-accent/50"
> >
<LanguageIcon className="size-4" /> <LanguageIcon className="size-4" />
<span className="sr-only">Change language</span> <span className="sr-only">Change language</span>
@ -40,7 +40,7 @@ export function LanguageSwitcher() {
onSelect={(e) => handleSelect(e, item.code)} onSelect={(e) => handleSelect(e, item.code)}
className={cn( className={cn(
{ {
"bg-muted gap-3 font-semibold": locale === item.code, "gap-3 bg-muted font-semibold": locale === item.code,
}, },
{ {
"rounded-t-[5px]": index === localeItems.length - 1, "rounded-t-[5px]": index === localeItems.length - 1,

View File

@ -30,7 +30,7 @@ export default function ServerCard({
<Link onClick={saveSession} href={`/server/${id}`} prefetch={true}> <Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
<Card <Card
className={cn( className={cn(
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 cursor-pointer hover:border-stone-300 dark:hover:border-stone-700 hover:shadow-md", "flex cursor-pointer flex-col items-center justify-start gap-3 p-3 hover:border-stone-300 hover:shadow-md md:px-5 dark:hover:border-stone-700",
{ {
"flex-col": fixedTopServerName, "flex-col": fixedTopServerName,
"lg:flex-row": !fixedTopServerName, "lg:flex-row": !fixedTopServerName,
@ -43,7 +43,7 @@ export default function ServerCard({
})} })}
style={{ gridTemplateColumns: "auto auto 1fr" }} style={{ gridTemplateColumns: "auto auto 1fr" }}
> >
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center" /> <span className="h-2 w-2 shrink-0 self-center rounded-full bg-green-500" />
<div <div
className={cn( className={cn(
"flex items-center justify-center", "flex items-center justify-center",
@ -70,8 +70,8 @@ export default function ServerCard({
})} })}
> >
{fixedTopServerName && ( {fixedTopServerName && (
<div className={"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"}> <div className={"col-span-1 hidden items-center gap-2 lg:flex lg:flex-row"}>
<div className="text-xs font-semibold"> <div className="font-semibold text-xs">
{host.Platform.includes("Windows") ? ( {host.Platform.includes("Windows") ? (
<MageMicrosoftWindows className="size-[10px]" /> <MageMicrosoftWindows className="size-[10px]" />
) : ( ) : (
@ -79,37 +79,37 @@ export default function ServerCard({
)} )}
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("System")}</p> <p className="text-muted-foreground text-xs">{t("System")}</p>
<div className="flex items-center text-[10.5px] font-semibold"> <div className="flex items-center font-semibold text-[10.5px]">
{host.Platform.includes("Windows") ? "Windows" : GetOsName(host.Platform)} {host.Platform.includes("Windows") ? "Windows" : GetOsName(host.Platform)}
</div> </div>
</div> </div>
</div> </div>
)} )}
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("CPU")}</p> <p className="text-muted-foreground text-xs">{t("CPU")}</p>
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div> <div className="flex items-center font-semibold text-xs">{cpu.toFixed(2)}%</div>
<ServerUsageBar value={cpu} /> <ServerUsageBar value={cpu} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("Mem")}</p> <p className="text-muted-foreground text-xs">{t("Mem")}</p>
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div> <div className="flex items-center font-semibold text-xs">{mem.toFixed(2)}%</div>
<ServerUsageBar value={mem} /> <ServerUsageBar value={mem} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("STG")}</p> <p className="text-muted-foreground text-xs">{t("STG")}</p>
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div> <div className="flex items-center font-semibold text-xs">{stg.toFixed(2)}%</div>
<ServerUsageBar value={stg} /> <ServerUsageBar value={stg} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("Upload")}</p> <p className="text-muted-foreground text-xs">{t("Upload")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center font-semibold text-xs">
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`} {up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`}
</div> </div>
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("Download")}</p> <p className="text-muted-foreground text-xs">{t("Download")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center font-semibold text-xs">
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`} {down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`}
</div> </div>
</div> </div>
@ -118,13 +118,13 @@ export default function ServerCard({
<section className={"flex items-center justify-between gap-1"}> <section className={"flex items-center justify-between gap-1"}>
<Badge <Badge
variant="secondary" variant="secondary"
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] border-muted-50 shadow-md shadow-neutral-200/30 dark:shadow-none" className="flex-1 items-center justify-center text-nowrap rounded-[8px] border-muted-50 text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none"
> >
{t("Upload")}:{formatBytes(serverInfo.status.NetOutTransfer)} {t("Upload")}:{formatBytes(serverInfo.status.NetOutTransfer)}
</Badge> </Badge>
<Badge <Badge
variant="outline" variant="outline"
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none" className="flex-1 items-center justify-center text-nowrap rounded-[8px] text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none"
> >
{t("Download")}:{formatBytes(serverInfo.status.NetInTransfer)} {t("Download")}:{formatBytes(serverInfo.status.NetInTransfer)}
</Badge> </Badge>
@ -137,8 +137,8 @@ export default function ServerCard({
<Link onClick={saveSession} href={`/server/${id}`} prefetch={true}> <Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
<Card <Card
className={cn( className={cn(
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 cursor-pointer hover:border-stone-300 dark:hover:border-stone-700 hover:shadow-md", "flex cursor-pointer flex-col items-center justify-start gap-3 p-3 hover:border-stone-300 hover:shadow-md md:px-5 dark:hover:border-stone-700",
showNetTransfer ? "lg:min-h-[91px] min-h-[123px]" : "lg:min-h-[61px] min-h-[93px]", showNetTransfer ? "min-h-[123px] lg:min-h-[91px]" : "min-h-[93px] lg:min-h-[61px]",
{ {
"flex-col": fixedTopServerName, "flex-col": fixedTopServerName,
"lg:flex-row": !fixedTopServerName, "lg:flex-row": !fixedTopServerName,
@ -151,7 +151,7 @@ export default function ServerCard({
})} })}
style={{ gridTemplateColumns: "auto auto 1fr" }} style={{ gridTemplateColumns: "auto auto 1fr" }}
> >
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" /> <span className="h-2 w-2 shrink-0 self-center rounded-full bg-red-500" />
<div <div
className={cn( className={cn(
"flex items-center justify-center", "flex items-center justify-center",

View File

@ -29,14 +29,14 @@ export default function ServerCardInline({
<Link onClick={saveSession} href={`/server/${id}`} prefetch={true}> <Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
<Card <Card
className={cn( className={cn(
"flex items-center lg:flex-row justify-start gap-3 p-3 md:px-5 cursor-pointer hover:border-stone-300 dark:hover:border-stone-700 hover:shadow-md min-w-[900px] w-full", "flex w-full min-w-[900px] cursor-pointer items-center justify-start gap-3 p-3 hover:border-stone-300 hover:shadow-md md:px-5 lg:flex-row dark:hover:border-stone-700",
)} )}
> >
<section <section
className={cn("grid items-center gap-2 lg:w-36")} className={cn("grid items-center gap-2 lg:w-36")}
style={{ gridTemplateColumns: "auto auto 1fr" }} style={{ gridTemplateColumns: "auto auto 1fr" }}
> >
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center" /> <span className="h-2 w-2 shrink-0 self-center rounded-full bg-green-500" />
<div <div
className={cn( className={cn(
"flex items-center justify-center", "flex items-center justify-center",
@ -56,11 +56,11 @@ export default function ServerCardInline({
</p> </p>
</div> </div>
</section> </section>
<Separator orientation="vertical" className="h-8 mx-0 ml-2" /> <Separator orientation="vertical" className="mx-0 ml-2 h-8" />
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}> <section className={cn("grid flex-1 grid-cols-9 items-center gap-3")}>
<div className={"items-center flex flex-row gap-2 whitespace-nowrap"}> <div className={"flex flex-row items-center gap-2 whitespace-nowrap"}>
<div className="text-xs font-semibold"> <div className="font-semibold text-xs">
{host.Platform.includes("Windows") ? ( {host.Platform.includes("Windows") ? (
<MageMicrosoftWindows className="size-[10px]" /> <MageMicrosoftWindows className="size-[10px]" />
) : ( ) : (
@ -68,54 +68,54 @@ 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("System")}</p> <p className="text-muted-foreground text-xs">{t("System")}</p>
<div className="flex items-center text-[10.5px] font-semibold"> <div className="flex items-center font-semibold text-[10.5px]">
{host.Platform.includes("Windows") ? "Windows" : GetOsName(host.Platform)} {host.Platform.includes("Windows") ? "Windows" : GetOsName(host.Platform)}
</div> </div>
</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("Uptime")}</p> <p className="text-muted-foreground text-xs">{t("Uptime")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center font-semibold text-xs">
{(serverInfo?.status.Uptime / 86400).toFixed(0)} {"Days"} {(serverInfo?.status.Uptime / 86400).toFixed(0)} {"Days"}
</div> </div>
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("CPU")}</p> <p className="text-muted-foreground text-xs">{t("CPU")}</p>
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div> <div className="flex items-center font-semibold text-xs">{cpu.toFixed(2)}%</div>
<ServerUsageBar value={cpu} /> <ServerUsageBar value={cpu} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("Mem")}</p> <p className="text-muted-foreground text-xs">{t("Mem")}</p>
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div> <div className="flex items-center font-semibold text-xs">{mem.toFixed(2)}%</div>
<ServerUsageBar value={mem} /> <ServerUsageBar value={mem} />
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("STG")}</p> <p className="text-muted-foreground text-xs">{t("STG")}</p>
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div> <div className="flex items-center font-semibold text-xs">{stg.toFixed(2)}%</div>
<ServerUsageBar value={stg} /> <ServerUsageBar value={stg} />
</div> </div>
<div className={"flex w-16 flex-col"}> <div className={"flex w-16 flex-col"}>
<p className="text-xs text-muted-foreground">{t("Upload")}</p> <p className="text-muted-foreground text-xs">{t("Upload")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center font-semibold text-xs">
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`} {up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : `${up.toFixed(2)}M/s`}
</div> </div>
</div> </div>
<div className={"flex w-16 flex-col"}> <div className={"flex w-16 flex-col"}>
<p className="text-xs text-muted-foreground">{t("Download")}</p> <p className="text-muted-foreground text-xs">{t("Download")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center font-semibold text-xs">
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`} {down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : `${down.toFixed(2)}M/s`}
</div> </div>
</div> </div>
<div className={"flex w-20 flex-col"}> <div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">{t("TotalUpload")}</p> <p className="text-muted-foreground text-xs">{t("TotalUpload")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center font-semibold text-xs">
{formatBytes(serverInfo.status.NetOutTransfer)} {formatBytes(serverInfo.status.NetOutTransfer)}
</div> </div>
</div> </div>
<div className={"flex w-20 flex-col"}> <div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">{t("TotalDownload")}</p> <p className="text-muted-foreground text-xs">{t("TotalDownload")}</p>
<div className="flex items-center text-xs font-semibold"> <div className="flex items-center font-semibold text-xs">
{formatBytes(serverInfo.status.NetInTransfer)} {formatBytes(serverInfo.status.NetInTransfer)}
</div> </div>
</div> </div>
@ -127,14 +127,14 @@ export default function ServerCardInline({
<Link onClick={saveSession} href={`/server/${id}`} prefetch={true}> <Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
<Card <Card
className={cn( className={cn(
"flex items-center justify-start gap-3 p-3 md:px-5 hover:border-stone-300 dark:hover:border-stone-700 hover:shadow-md min-h-[61px] min-w-[900px] flex-row", "flex min-h-[61px] min-w-[900px] flex-row items-center justify-start gap-3 p-3 hover:border-stone-300 hover:shadow-md md:px-5 dark:hover:border-stone-700",
)} )}
> >
<section <section
className={cn("grid items-center gap-2 lg:w-40")} className={cn("grid items-center gap-2 lg:w-40")}
style={{ gridTemplateColumns: "auto auto 1fr" }} style={{ gridTemplateColumns: "auto auto 1fr" }}
> >
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" /> <span className="h-2 w-2 shrink-0 self-center rounded-full bg-red-500" />
<div <div
className={cn( className={cn(
"flex items-center justify-center", "flex items-center justify-center",

View File

@ -49,26 +49,26 @@ export function SignIn() {
} }
return ( return (
<form <form
className="flex flex-col flex-1 items-center justify-center gap-4 p-4 " className="flex flex-1 flex-col items-center justify-center gap-4 p-4 "
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<input type="hidden" name="csrfToken" value={csrfToken} /> <input type="hidden" name="csrfToken" value={csrfToken} />
<section className="flex flex-col items-start gap-2"> <section className="flex flex-col items-start gap-2">
<label className="flex flex-col items-start gap-1 "> <label className="flex flex-col items-start gap-1 ">
{errorState && <p className="text-red-500 text-sm font-semibold">{t("ErrorMessage")}</p>} {errorState && <p className="font-semibold text-red-500 text-sm">{t("ErrorMessage")}</p>}
{successState && ( {successState && (
<p className="text-green-500 text-sm font-semibold">{t("SuccessMessage")}</p> <p className="font-semibold text-green-500 text-sm">{t("SuccessMessage")}</p>
)} )}
<p className="text-base font-semibold">{t("SignInMessage")}</p> <p className="font-semibold text-base">{t("SignInMessage")}</p>
<input <input
className="px-1 border-[1px] rounded-[5px] border-stone-300 dark:border-stone-800" className="rounded-[5px] border-[1px] border-stone-300 px-1 dark:border-stone-800"
name="password" name="password"
type="password" type="password"
/> />
</label> </label>
<button <button
type="submit" type="submit"
className="px-1.5 py-0.5 w-fit flex items-center gap-1 text-sm font-semibold border-stone-300 dark:border-stone-800 rounded-[8px] border bg-card hover:brightness-95 transition-all text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none" className="flex w-fit items-center gap-1 rounded-[8px] border border-stone-300 bg-card px-1.5 py-0.5 font-semibold text-card-foreground text-sm shadow-lg shadow-neutral-200/40 transition-all hover:brightness-95 dark:border-stone-800 dark:shadow-none"
disabled={loading} disabled={loading}
> >
{t("Submit")} {t("Submit")}

View File

@ -20,10 +20,8 @@ export default function Switch({
const tagRefs = useRef(allTag.map(() => createRef<HTMLDivElement>())) const tagRefs = useRef(allTag.map(() => createRef<HTMLDivElement>()))
const t = useTranslations("ServerListClient") const t = useTranslations("ServerListClient")
const locale = useLocale() const locale = useLocale()
const [indicator, setIndicator] = useState<{ x: number; w: number }>({ const [indicator, setIndicator] = useState<{ x: number; w: number } | null>(null)
x: 0, const [isFirstRender, setIsFirstRender] = useState(true)
w: 0,
})
useEffect(() => { useEffect(() => {
const savedTag = sessionStorage.getItem("selectedTag") const savedTag = sessionStorage.getItem("selectedTag")
@ -59,7 +57,13 @@ export default function Switch({
w: currentTagElement.offsetWidth, w: currentTagElement.offsetWidth,
}) })
} }
}, [nowTag, locale])
if (isFirstRender) {
setTimeout(() => {
setIsFirstRender(false)
}, 50)
}
}, [nowTag, locale, allTag, isFirstRender])
useEffect(() => { useEffect(() => {
const currentTagElement = tagRefs.current[allTag.indexOf(nowTag)]?.current const currentTagElement = tagRefs.current[allTag.indexOf(nowTag)]?.current
@ -84,14 +88,14 @@ export default function Switch({
className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]" className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]"
> >
<div className="relative flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800"> <div className="relative flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800">
{indicator.w > 0 && ( {indicator && (
<div <div
className="absolute top-[3px] left-0 z-10 h-[35px] bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5" className="absolute top-[3px] left-0 z-10 h-[35px] bg-white shadow-black/5 shadow-lg dark:bg-stone-700 dark:shadow-white/5"
style={{ style={{
borderRadius: 24, borderRadius: 24,
width: `${indicator.w}px`, width: `${indicator.w}px`,
transform: `translateX(${indicator.x}px)`, transform: `translateX(${indicator.x}px)`,
transition: "all 0.5s cubic-bezier(0.4, 0, 0.2, 1)", transition: isFirstRender ? "none" : "all 0.5s cubic-bezier(0.4, 0, 0.2, 1)",
}} }}
/> />
)} )}
@ -104,18 +108,18 @@ export default function Switch({
sessionStorage.setItem("selectedTag", tag) sessionStorage.setItem("selectedTag", tag)
}} }}
className={cn( className={cn(
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600]", "relative cursor-pointer rounded-3xl px-2.5 py-[8px] font-[600] text-[13px]",
"transition-all duration-500 ease-in-out text-stone-400 dark:text-stone-500 hover:text-stone-950 hover:dark:text-stone-50", "text-stone-400 transition-all duration-500 ease-in-out hover:text-stone-950 dark:text-stone-500 hover:dark:text-stone-50",
{ {
"text-stone-950 dark:text-stone-50": nowTag === tag, "text-stone-950 dark:text-stone-50": nowTag === tag,
}, },
)} )}
> >
<div className="relative z-20 flex items-center gap-1"> <div className="relative z-20 flex items-center gap-1">
<div className="whitespace-nowrap flex items-center gap-2"> <div className="flex items-center gap-2 whitespace-nowrap">
{tag === "defaultTag" ? t("defaultTag") : tag}{" "} {tag === "defaultTag" ? t("defaultTag") : tag}{" "}
{getEnv("NEXT_PUBLIC_ShowTagCount") === "true" && tag !== "defaultTag" && ( {getEnv("NEXT_PUBLIC_ShowTagCount") === "true" && tag !== "defaultTag" && (
<div className="w-fit px-1.5 rounded-full bg-muted">{tagCountMap[tag]}</div> <div className="w-fit rounded-full bg-muted px-1.5">{tagCountMap[tag]}</div>
)} )}
</div> </div>
</div> </div>

View File

@ -40,7 +40,7 @@ export default function TabSwitch({
<div className="relative flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800"> <div className="relative flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800">
{indicator.w > 0 && ( {indicator.w > 0 && (
<div <div
className="absolute top-[3px] left-0 z-10 h-[35px] bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5" className="absolute top-[3px] left-0 z-10 h-[35px] bg-white shadow-black/5 shadow-lg dark:bg-stone-700 dark:shadow-white/5"
style={{ style={{
borderRadius: 24, borderRadius: 24,
width: `${indicator.w}px`, width: `${indicator.w}px`,
@ -57,8 +57,8 @@ 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]", "relative cursor-pointer rounded-3xl px-2.5 py-[8px] font-[600] text-[13px]",
"transition-all duration-500 ease-in-out text-stone-400 dark:text-stone-500 hover:text-stone-950 hover:dark:text-stone-50", "text-stone-400 transition-all duration-500 ease-in-out hover:text-stone-950 dark:text-stone-500 hover:dark:text-stone-50",
{ {
"text-stone-950 dark:text-stone-50": currentTab === tab, "text-stone-950 dark:text-stone-50": currentTab === tab,
}, },

View File

@ -4,23 +4,28 @@ import { Button } from "@/components/ui/button"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils" import { CheckIcon, MinusIcon, Moon, Sun } from "lucide-react"
import { CheckCircleIcon } from "@heroicons/react/20/solid"
import { Moon, Sun } from "lucide-react"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { useId } from "react"
import { RadioGroup, RadioGroupItem } from "./ui/radio-group"
const items = [
{ value: "light", label: "Light", image: "/ui-light.png" },
{ value: "dark", label: "Dark", image: "/ui-dark.png" },
{ value: "system", label: "System", image: "/ui-system.png" },
]
export function ModeToggle() { export function ModeToggle() {
const { setTheme, theme } = useTheme() const { setTheme, theme } = useTheme()
const t = useTranslations("ThemeSwitcher") const t = useTranslations("ThemeSwitcher")
const handleSelect = (e: Event, newTheme: string) => { const handleSelect = (newTheme: string) => {
e.preventDefault()
setTheme(newTheme) setTheme(newTheme)
} }
const id = useId()
return ( return (
<DropdownMenu> <DropdownMenu>
@ -28,38 +33,47 @@ export function ModeToggle() {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="rounded-full px-[9px] bg-white dark:bg-black cursor-pointer hover:bg-accent/50 dark:hover:bg-accent/50" className="cursor-pointer rounded-full bg-white px-[9px] hover:bg-accent/50 dark:bg-black dark:hover:bg-accent/50"
> >
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Sun className="dark:-rotate-90 h-4 w-4 rotate-0 scale-100 transition-all dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span> <span className="sr-only">Toggle theme</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="flex flex-col gap-0.5" align="end"> <DropdownMenuContent className="px-2 pt-2 pb-1.5" align="end">
<DropdownMenuItem <fieldset className="space-y-4">
className={cn("rounded-b-[5px]", { <RadioGroup className="flex gap-2" defaultValue={theme} onValueChange={handleSelect}>
"gap-3 bg-muted font-semibold": theme === "light", {items.map((item) => (
})} <label key={`${id}-${item.value}`}>
onSelect={(e) => handleSelect(e, "light")} <RadioGroupItem
> id={`${id}-${item.value}`}
{t("Light")} {theme === "light" && <CheckCircleIcon className="size-4" />} value={item.value}
</DropdownMenuItem> className="peer sr-only after:absolute after:inset-0"
<DropdownMenuItem />
className={cn("rounded-[5px]", { <img
"gap-3 bg-muted font-semibold": theme === "dark", src={item.image}
})} alt={item.label}
onSelect={(e) => handleSelect(e, "dark")} width={88}
> height={70}
{t("Dark")} {theme === "dark" && <CheckCircleIcon className="size-4" />} className="relative cursor-pointer overflow-hidden rounded-[8px] border border-neutral-300 shadow-xs outline-none transition-[color,box-shadow] peer-focus-visible:ring-[3px] peer-focus-visible:ring-ring/50 peer-data-disabled:cursor-not-allowed peer-data-[state=checked]:bg-accent peer-data-disabled:opacity-50 dark:border-neutral-700"
</DropdownMenuItem> />
<DropdownMenuItem <span className="group mt-2 flex items-center gap-1 peer-data-[state=unchecked]:text-muted-foreground/70">
className={cn("rounded-t-[5px]", { <CheckIcon
"gap-3 bg-muted font-semibold": theme === "system", size={16}
})} className="group-peer-data-[state=unchecked]:hidden"
onSelect={(e) => handleSelect(e, "system")} aria-hidden="true"
> />
{t("System")} {theme === "system" && <CheckCircleIcon className="size-4" />} <MinusIcon
</DropdownMenuItem> size={16}
className="group-peer-data-[state=checked]:hidden"
aria-hidden="true"
/>
<span className="font-medium text-xs">{t(item.label)}</span>
</span>
</label>
))}
</RadioGroup>
</fieldset>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) )

View File

@ -6,7 +6,7 @@ import { useTranslations } from "next-intl"
export default function GlobalLoading() { export default function GlobalLoading() {
const t = useTranslations("Global") const t = useTranslations("Global")
return ( return (
<section className="flex flex-col gap-4 mt-[3.2px]"> <section className="mt-[3.2px] flex flex-col gap-4">
<div className="flex min-h-40 flex-col items-center justify-center font-medium text-sm"> <div className="flex min-h-40 flex-col items-center justify-center font-medium text-sm">
{t("Loading")} {t("Loading")}
<Loader visible={true} /> <Loader visible={true} />

View File

@ -11,7 +11,7 @@ export default function NetworkChartLoading() {
</CardTitle> </CardTitle>
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted" /> <div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted" />
</div> </div>
<div className="hidden pr-4 pt-4 sm:block"> <div className="hidden pt-4 pr-4 sm:block">
<Loader visible={true} /> <Loader visible={true} />
</div> </div>
</CardHeader> </CardHeader>

View File

@ -5,13 +5,13 @@ import { useRouter } from "next/navigation"
export function ServerDetailChartLoading() { export function ServerDetailChartLoading() {
return ( return (
<div> <div>
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3"> <section className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none" /> <Skeleton className="h-[182px] w-full animate-none rounded-[5px] bg-muted-foreground/10" />
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none" /> <Skeleton className="h-[182px] w-full animate-none rounded-[5px] bg-muted-foreground/10" />
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none" /> <Skeleton className="h-[182px] w-full animate-none rounded-[5px] bg-muted-foreground/10" />
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none" /> <Skeleton className="h-[182px] w-full animate-none rounded-[5px] bg-muted-foreground/10" />
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none" /> <Skeleton className="h-[182px] w-full animate-none rounded-[5px] bg-muted-foreground/10" />
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none" /> <Skeleton className="h-[182px] w-full animate-none rounded-[5px] bg-muted-foreground/10" />
</section> </section>
</div> </div>
) )
@ -26,12 +26,12 @@ export function ServerDetailLoading() {
onClick={() => { onClick={() => {
router.push("/") router.push("/")
}} }}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl" className="flex flex-none cursor-pointer items-center gap-0.5 break-all font-semibold text-xl leading-none tracking-tight"
> >
<BackIcon /> <BackIcon />
<Skeleton className="h-[20px] w-24 rounded-[5px] bg-muted-foreground/10 animate-none" /> <Skeleton className="h-[20px] w-24 animate-none rounded-[5px] bg-muted-foreground/10" />
</div> </div>
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none" /> <Skeleton className="mt-3 flex h-[81px] w-1/2 animate-none flex-wrap gap-2 rounded-[5px] bg-muted-foreground/10" />
</> </>
) )
} }

View File

@ -21,7 +21,7 @@ export default function AnimatedCircularProgressBar({
return ( return (
<div <div
className={cn("relative size-40 text-2xl font-semibold", className)} className={cn("relative size-40 font-semibold text-2xl", className)}
style={ style={
{ {
"--circle-size": "100px", "--circle-size": "100px",
@ -48,7 +48,7 @@ export default function AnimatedCircularProgressBar({
strokeDashoffset="0" strokeDashoffset="0"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
className="opacity-100 stroke-muted" className="stroke-muted opacity-100"
style={ style={
{ {
"--stroke-percent": 90 - currentPercent, "--stroke-percent": 90 - currentPercent,
@ -71,7 +71,7 @@ export default function AnimatedCircularProgressBar({
strokeDashoffset="0" strokeDashoffset="0"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
className={cn("opacity-100 stroke-current", { className={cn("stroke-current opacity-100", {
"stroke-[var(--stroke-primary-color)]": primaryColor, "stroke-[var(--stroke-primary-color)]": primaryColor,
})} })}
style={ style={
@ -92,7 +92,7 @@ export default function AnimatedCircularProgressBar({
</svg> </svg>
<span <span
data-current-value={currentPercent} data-current-value={currentPercent}
className="duration-[var(--transition-length)] delay-[var(--delay)] absolute inset-0 m-auto size-fit ease-linear animate-in fade-in" className="fade-in absolute inset-0 m-auto size-fit animate-in delay-[var(--delay)] duration-[var(--transition-length)] ease-linear"
> >
{currentPercent} {currentPercent}
</span> </span>

View File

@ -14,7 +14,7 @@ export const AnimatedTooltip = ({
return ( return (
<> <>
{items.map((item) => ( {items.map((item) => (
<div className="group relative -mr-4" key={item.name}> <div className="group -mr-4 relative" key={item.name}>
<Link href="https://buycoffee.top" target="_blank"> <Link href="https://buycoffee.top" target="_blank">
<Image <Image
width={40} width={40}

View File

@ -26,7 +26,7 @@ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HT
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<h3 <h3
ref={ref} ref={ref}
className={cn("text-2xl font-semibold leading-none tracking-tight", className)} className={cn("font-semibold text-2xl leading-none tracking-tight", className)}
{...props} {...props}
/> />
), ),
@ -37,7 +37,7 @@ const CardDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} /> <p ref={ref} className={cn("text-muted-foreground text-sm", className)} {...props} />
)) ))
CardDescription.displayName = "CardDescription" CardDescription.displayName = "CardDescription"

View File

@ -164,16 +164,16 @@ const ChartTooltipContent = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"grid min-w-[8rem] items-start overflow-hidden gap-1.5 rounded-sm border border-border/50 bg-stone-100 dark:bg-stone-900 text-xs", "grid min-w-[8rem] items-start gap-1.5 overflow-hidden rounded-sm border border-border/50 bg-stone-100 text-xs dark:bg-stone-900",
className, className,
)} )}
> >
{!nestLabel && ( {!nestLabel && (
<div className="px-2.5 pt-1 mx-auto -mb-1">{!nestLabel ? tooltipLabel : null}</div> <div className="-mb-1 mx-auto px-2.5 pt-1">{!nestLabel ? tooltipLabel : null}</div>
)} )}
<div <div
className={cn("grid gap-1.5 bg-white dark:bg-black px-2.5 py-1.5", { className={cn("grid gap-1.5 bg-white px-2.5 py-1.5 dark:bg-black", {
"border-t-[1px]": !nestLabel, "border-t-[1px]": !nestLabel,
})} })}
> >
@ -233,7 +233,7 @@ const ChartTooltipContent = React.forwardRef<
{item.value && ( {item.value && (
<span <span
className={cn( className={cn(
"ml-2 font-medium tabular-nums text-foreground", "ml-2 font-medium text-foreground tabular-nums",
payload.length === 1 && "-ml-9", payload.length === 1 && "-ml-9",
)} )}
> >

View File

@ -40,7 +40,7 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>, React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className="flex items-center bg-stone-100 dark:bg-stone-900 px-3" cmdk-input-wrapper=""> <div className="flex items-center bg-stone-100 px-3 dark:bg-stone-900" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input <CommandPrimitive.Input
ref={ref} ref={ref}
@ -61,7 +61,7 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.List <CommandPrimitive.List
ref={ref} ref={ref}
className={cn("max-h-[300px] mb-1 overflow-y-auto overflow-x-hidden", className)} className={cn("mb-1 max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props} {...props}
/> />
)) ))
@ -84,7 +84,7 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group <CommandPrimitive.Group
ref={ref} ref={ref}
className={cn( className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:text-xs",
className, className,
)} )}
{...props} {...props}
@ -112,7 +112,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-[8px] px-2 py-1.5 text-xs outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-stone-100 dark:data-[selected='true']:bg-stone-900 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "relative flex cursor-default select-none items-center gap-2 rounded-[8px] px-2 py-1.5 text-xs outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-stone-100 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 dark:data-[selected='true']:bg-stone-900 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className, className,
)} )}
{...props} {...props}
@ -124,7 +124,7 @@ CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return ( return (
<span <span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} className={cn("ml-auto text-muted-foreground text-xs tracking-widest", className)}
{...props} {...props}
/> />
) )

View File

@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
className, className,
)} )}
{...props} {...props}
@ -38,13 +38,13 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:rounded-lg",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> <DialogPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
@ -72,7 +72,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref} ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)} className={cn("font-semibold text-lg leading-none tracking-tight", className)}
{...props} {...props}
/> />
)) ))
@ -84,7 +84,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Description <DialogPrimitive.Description
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
)) ))

View File

@ -45,7 +45,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in",
className, className,
)} )}
{...props} {...props}
@ -62,7 +62,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl dark:shadow-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl data-[state=closed]:animate-out data-[state=open]:animate-in dark:shadow-none",
className, className,
)} )}
{...props} {...props}
@ -80,7 +80,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-[10px] px-2 py-1.5 text-xs font-normal outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-default select-none items-center rounded-[10px] px-2 py-1.5 font-normal text-xs outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
inset && "pl-8", inset && "pl-8",
className, className,
)} )}
@ -96,7 +96,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className, className,
)} )}
checked={checked} checked={checked}
@ -119,7 +119,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className, className,
)} )}
{...props} {...props}
@ -142,7 +142,7 @@ const DropdownMenuLabel = React.forwardRef<
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
ref={ref} ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)} className={cn("px-2 py-1.5 font-semibold text-sm", inset && "pl-8", className)}
{...props} {...props}
/> />
)) ))

View File

@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<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 placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 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:font-medium file:text-sm placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className, className,
)} )}
ref={ref} ref={ref}

View File

@ -62,7 +62,7 @@ const NavigationMenuContent = React.forwardRef<
<NavigationMenuPrimitive.Content <NavigationMenuPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto", "data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out md:absolute md:w-auto",
className, className,
)} )}
{...props} {...props}
@ -76,10 +76,10 @@ const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>, React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}> <div className={cn("absolute top-full left-0 flex justify-center")}>
<NavigationMenuPrimitive.Viewport <NavigationMenuPrimitive.Viewport
className={cn( className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]", "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full origin-top-center overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in md:w-[var(--radix-navigation-menu-viewport-width)]",
className, className,
)} )}
ref={ref} ref={ref}
@ -96,7 +96,7 @@ const NavigationMenuIndicator = React.forwardRef<
<NavigationMenuPrimitive.Indicator <NavigationMenuPrimitive.Indicator
ref={ref} ref={ref}
className={cn( className={cn(
"top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in", "data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=hidden]:animate-out data-[state=visible]:animate-in",
className, className,
)} )}
{...props} {...props}

View File

@ -18,7 +18,7 @@ const PopoverContent = React.forwardRef<
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in",
className, className,
)} )}
{...props} {...props}

View File

@ -0,0 +1,50 @@
"use client"
import { cn } from "@/lib/utils"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import type * as React from "react"
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
)
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"aspect-square size-4 shrink-0 rounded-full border border-input shadow-xs outline-none transition-shadow focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:aria-invalid:ring-destructive/40",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center text-current">
<svg
role="img"
aria-label="Radio indicator"
width="6"
height="6"
viewBox="0 0 6 6"
fill="currentcolor"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="3" cy="3" r="3" />
</svg>
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem }

View File

@ -20,7 +20,7 @@ const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay <SheetPrimitive.Overlay
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
className, className,
)} )}
{...props} {...props}
@ -60,7 +60,7 @@ const SheetContent = React.forwardRef<
<SheetOverlay /> <SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}> <SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children} {children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> <SheetPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</SheetPrimitive.Close> </SheetPrimitive.Close>
@ -88,7 +88,7 @@ const SheetTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Title <SheetPrimitive.Title
ref={ref} ref={ref}
className={cn("text-lg font-semibold text-foreground", className)} className={cn("font-semibold text-foreground text-lg", className)}
{...props} {...props}
/> />
)) ))
@ -100,7 +100,7 @@ const SheetDescription = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Description <SheetPrimitive.Description
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
)) ))

View File

@ -18,7 +18,7 @@ const TooltipContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 overflow-hidden rounded-[10px] border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 animate-in overflow-hidden rounded-[10px] border bg-popover px-3 py-1.5 text-popover-foreground text-sm shadow-md data-[state=closed]:animate-out",
className, className,
)} )}
{...props} {...props}

View File

@ -4,7 +4,7 @@ import getEnv from "./lib/env-entry"
export const localeItems = [ export const localeItems = [
{ code: "en", name: "English" }, { code: "en", name: "English" },
{ code: "ja", name: "日本語" }, { code: "ja", name: "日本語" },
{ code: "zh-t", name: "中文繁體" }, { code: "zh-TW", name: "中文繁體" },
{ code: "zh", name: "中文简体" }, { code: "zh", name: "中文简体" },
] ]

View File

@ -16,6 +16,10 @@ export function formatNezhaInfo(serverInfo: NezhaAPISafe) {
last_active_time_string: serverInfo.last_active last_active_time_string: serverInfo.last_active
? new Date(serverInfo.last_active * 1000).toLocaleString() ? new Date(serverInfo.last_active * 1000).toLocaleString()
: "", : "",
boot_time: serverInfo.host.BootTime,
boot_time_string: serverInfo.host.BootTime
? new Date(serverInfo.host.BootTime * 1000).toLocaleString()
: "",
online: serverInfo.online_status, online: serverInfo.online_status,
uptime: serverInfo.status.Uptime || 0, uptime: serverInfo.status.Uptime || 0,
version: serverInfo.host.Version || null, version: serverInfo.host.Version || null,

View File

@ -86,7 +86,8 @@
"Upload": "Upload", "Upload": "Upload",
"Download": "Download", "Download": "Download",
"Load": "Load", "Load": "Load",
"LastActive": "Last Active" "LastActive": "Last Active",
"BootTime": "Boot Time"
}, },
"ServerDetailChartClient": { "ServerDetailChartClient": {
"chart_fetch_error_message": "Please check your environment variables and review the server console", "chart_fetch_error_message": "Please check your environment variables and review the server console",

View File

@ -86,7 +86,8 @@
"Load": "負荷", "Load": "負荷",
"Upload": "Upload", "Upload": "Upload",
"Download": "Download", "Download": "Download",
"LastActive": "Last Active" "LastActive": "Last Active",
"BootTime": "Boot Time"
}, },
"ServerDetailChartClient": { "ServerDetailChartClient": {
"chart_fetch_error_message": "環境変数を確認し、サーバーコンソールを確認してください", "chart_fetch_error_message": "環境変数を確認し、サーバーコンソールを確認してください",

View File

@ -86,7 +86,8 @@
"Upload": "上傳", "Upload": "上傳",
"Download": "下載", "Download": "下載",
"Load": "負載", "Load": "負載",
"LastActive": "最後上報時間" "LastActive": "最後上報時間",
"BootTime": "啟動時間"
}, },
"ServerDetailChartClient": { "ServerDetailChartClient": {
"chart_fetch_error_message": "獲取伺服器詳情失敗,請檢查您的環境變數並檢查伺服器控制台", "chart_fetch_error_message": "獲取伺服器詳情失敗,請檢查您的環境變數並檢查伺服器控制台",

View File

@ -86,7 +86,8 @@
"Upload": "上传", "Upload": "上传",
"Download": "下载", "Download": "下载",
"Load": "负载", "Load": "负载",
"LastActive": "最后上报时间" "LastActive": "最后上报时间",
"BootTime": "启动时间"
}, },
"ServerDetailChartClient": { "ServerDetailChartClient": {
"chart_fetch_error_message": "获取服务器详情失败,请检查您的环境变量并检查服务器控制台", "chart_fetch_error_message": "获取服务器详情失败,请检查您的环境变量并检查服务器控制台",

View File

@ -1,6 +1,6 @@
{ {
"name": "nezha-dash", "name": "nezha-dash",
"version": "2.7.2", "version": "2.9.3",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3040", "dev": "next dev -p 3040",
@ -15,62 +15,62 @@
"dependencies": { "dependencies": {
"@ducanh2912/next-pwa": "^10.2.9", "@ducanh2912/next-pwa": "^10.2.9",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@number-flow/react": "^0.5.5", "@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-label": "^2.1.2", "@radix-ui/react-navigation-menu": "^1.2.10",
"@radix-ui/react-navigation-menu": "^1.2.5", "@radix-ui/react-popover": "^1.1.11",
"@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-progress": "^1.1.4",
"@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-radio-group": "^1.3.4",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.4",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-switch": "^1.2.2",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.2.4",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/d3-geo": "^3.1.0", "@types/d3-geo": "^3.1.0",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.6.2",
"babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112", "babel-plugin-react-compiler": "^19.0.0-beta-ebf51a3-20250411",
"caniuse-lite": "^1.0.30001699", "caniuse-lite": "^1.0.30001715",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.4", "cmdk": "^1.1.1",
"country-flag-icons": "^1.5.16", "country-flag-icons": "^1.5.18",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"d3-geo": "^3.1.1", "d3-geo": "^3.1.1",
"d3-selection": "^3.0.0", "d3-selection": "^3.0.0",
"flag-icons": "^7.3.2", "flag-icons": "^7.3.2",
"i18n-iso-countries": "^7.13.0", "i18n-iso-countries": "^7.14.0",
"lucide-react": "^0.474.0", "lucide-react": "^0.474.0",
"luxon": "^3.5.0", "luxon": "^3.6.1",
"maxmind": "^4.3.24", "maxmind": "^4.3.24",
"next": "^15.1.7", "next": "^15.3.1",
"next-auth": "^5.0.0-beta.25", "next-auth": "^5.0.0-beta.26",
"next-intl": "^3.26.3", "next-intl": "^4.0.3",
"next-runtime-env": "^3.2.2", "next-runtime-env": "^3.3.0",
"next-themes": "^0.4.4", "next-themes": "^0.4.6",
"react": "^19.0.0", "react": "^19.1.0",
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
"react-dom": "^19.0.0", "react-dom": "^19.1.0",
"react-intersection-observer": "^9.15.1", "react-intersection-observer": "^9.16.0",
"react-wrap-balancer": "^1.1.1", "react-wrap-balancer": "^1.1.1",
"recharts": "^2.15.1", "recharts": "^2.15.3",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"swr": "^2.3.2", "swr": "^2.3.3",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^3.2.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@next/bundle-analyzer": "^15.1.7", "@next/bundle-analyzer": "^15.3.1",
"@tailwindcss/postcss": "^4.0.6", "@tailwindcss/postcss": "^4.1.4",
"@types/node": "^22.13.4", "@types/node": "^22.14.1",
"@types/react": "^19.0.8", "@types/react": "^19.1.2",
"@types/react-dom": "^19.0.3", "@types/react-dom": "^19.1.2",
"postcss": "^8.5.2", "postcss": "^8.5.3",
"tailwindcss": "^4.0.6", "tailwindcss": "^4.1.4",
"typescript": "^5.7.3", "typescript": "^5.8.3",
"vercel": "^39.4.2" "vercel": "^41.6.2"
}, },
"overrides": { "overrides": {
"react-is": "^19.0.0-rc-69d4b800-20241021" "react-is": "^19.0.0-rc-69d4b800-20241021"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
public/ui-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

BIN
public/ui-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

BIN
public/ui-system.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

View File

@ -146,6 +146,7 @@
--chart-8: 252 50% 50%; --chart-8: 252 50% 50%;
--chart-9: 288 50% 50%; --chart-9: 288 50% 50%;
--chart-10: 324 50% 50%; --chart-10: 324 50% 50%;
--timing: cubic-bezier(0.4, 0, 0.2, 1);
} }
.dark { .dark {
@ -334,3 +335,53 @@
filter: blur(0px); filter: blur(0px);
} }
} }
/* Thanks to next.js. */
[data-issues-count-animation] {
display: flex;
justify-content: center;
align-items: center;
}
[data-issues-count-animation] > div {
text-align: center;
}
[data-issues-count-exit].animate {
animation: fadeOut 300ms var(--timing) forwards;
}
[data-issues-count-enter].animate {
animation: fadeIn 300ms var(--timing) forwards;
}
[data-issues-count-plural] {
display: inline-block;
animation: fadeIn 300ms var(--timing) forwards;
}
@keyframes fadeIn {
0% {
opacity: 0;
filter: blur(2px);
transform: translateY(8px);
}
100% {
opacity: 1;
filter: blur(0px);
transform: translateY(0);
}
}
@keyframes fadeOut {
0% {
opacity: 1;
filter: blur(0px);
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-12px);
filter: blur(2px);
}
}