Compare commits

..

10 Commits

11 changed files with 236 additions and 60 deletions

View File

@ -3,12 +3,13 @@
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"
import { useEffect, useState } from "react" import React from "react"
function Header() { function Header() {
const t = useTranslations("Header") const t = useTranslations("Header")
@ -99,33 +100,51 @@ function Links() {
function Overview() { function Overview() {
const t = useTranslations("Overview") const t = useTranslations("Overview")
const [mouted, setMounted] = useState(false) const [time, setTime] = React.useState({
useEffect(() => { hh: DateTime.now().setLocale("en-US").hour,
setMounted(true) mm: DateTime.now().setLocale("en-US").minute,
}, []) ss: DateTime.now().setLocale("en-US").second,
const timeOption = DateTime.TIME_WITH_SECONDS })
timeOption.hour12 = true
const [timeString, setTimeString] = useState( React.useEffect(() => {
DateTime.now().setLocale("en-US").toLocaleString(timeOption), const timer = setInterval(() => {
) setTime({
useEffect(() => { hh: DateTime.now().setLocale("en-US").hour,
const updateTime = () => { mm: DateTime.now().setLocale("en-US").minute,
const now = DateTime.now().setLocale("en-US").toLocaleString(timeOption) ss: DateTime.now().setLocale("en-US").second,
setTimeString(now) })
requestAnimationFrame(updateTime) }, 1000)
}
requestAnimationFrame(updateTime) return () => clearInterval(timer)
}, []) }, [])
return ( return (
<section className={"mt-10 flex flex-col md:mt-16"}> <section className={"mt-10 flex flex-col md:mt-16"}>
<p className="text-base font-semibold">{t("p_2277-2331_Overview")}</p> <p className="text-base font-semibold">{t("p_2277-2331_Overview")}</p>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<p className="text-sm font-medium opacity-50">{t("p_2390-2457_wherethetimeis")}</p> <p className="text-sm font-medium opacity-50">{t("p_2390-2457_wherethetimeis")}</p>
{mouted ? ( <NumberFlowGroup>
<p className="text-sm font-medium">{timeString}</p> <div
) : ( style={{ fontVariantNumeric: "tabular-nums" }}
<Skeleton className="h-[20px] w-[50px] rounded-[5px] bg-muted-foreground/10 animate-none" /> className="flex text-sm font-medium mt-0.5"
)} >
<NumberFlow trend={1} value={time.hh} format={{ minimumIntegerDigits: 2 }} />
<NumberFlow
prefix=":"
trend={1}
value={time.mm}
digits={{ 1: { max: 5 } }}
format={{ minimumIntegerDigits: 2 }}
/>
<NumberFlow
prefix=":"
trend={1}
value={time.ss}
digits={{ 1: { max: 5 } }}
format={{ minimumIntegerDigits: 2 }}
/>
</div>
</NumberFlowGroup>
</div> </div>
</section> </section>
) )

BIN
bun.lockb

Binary file not shown.

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:bg-accent/50 transition-colors", "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-col": fixedTopServerName, "flex-col": fixedTopServerName,
"lg:flex-row": !fixedTopServerName, "lg:flex-row": !fixedTopServerName,
@ -137,7 +137,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:bg-accent/50 transition-colors", "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",
showNetTransfer ? "lg:min-h-[91px] min-h-[123px]" : "lg:min-h-[61px] min-h-[93px]", showNetTransfer ? "lg:min-h-[91px] min-h-[123px]" : "lg:min-h-[61px] min-h-[93px]",
{ {
"flex-col": fixedTopServerName, "flex-col": fixedTopServerName,

View File

@ -29,7 +29,7 @@ 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:bg-accent/50 transition-colors min-w-[900px] w-full", "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",
)} )}
> >
<section <section
@ -124,27 +124,34 @@ export default function ServerCardInline({
</Card> </Card>
</Link> </Link>
) : ( ) : (
<Card <Link onClick={saveSession} href={`/server/${id}`} prefetch={true}>
className={cn( <Card
"flex items-center justify-start gap-3 p-3 md:px-5 min-h-[61px] min-w-[900px] flex-row", 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",
> )}
<section
className={cn("grid items-center gap-2 lg:w-40")}
style={{ gridTemplateColumns: "auto auto 1fr" }}
> >
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" /> <section
<div className={cn("grid items-center gap-2 lg:w-40")}
className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")} style={{ gridTemplateColumns: "auto auto 1fr" }}
> >
{showFlag ? <ServerFlag country_code={country_code} /> : null} <span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" />
</div> <div
<div className="relative w-28"> className={cn(
<p className={cn("break-all font-bold tracking-tight", showFlag ? "text-xs" : "text-sm")}> "flex items-center justify-center",
{name} showFlag ? "min-w-[17px]" : "min-w-0",
</p> )}
</div> >
</section> {showFlag ? <ServerFlag country_code={country_code} /> : null}
</Card> </div>
<div className="relative w-28">
<p
className={cn("break-all font-bold tracking-tight", showFlag ? "text-xs" : "text-sm")}
>
{name}
</p>
</div>
</section>
</Card>
</Link>
) )
} }

View File

@ -1,8 +1,10 @@
import { env } from "next-runtime-env" import { getClientEnv, getServerEnv } from "./env"
import type { EnvKey } from "./env"
export default function getEnv(key: string) { export default function getEnv(key: EnvKey): string | undefined {
if (key.startsWith("NEXT_PUBLIC_")) { if (key.startsWith("NEXT_PUBLIC_")) {
return env(key) const clientKey = key.replace("NEXT_PUBLIC_", "") as any
return getClientEnv(clientKey)
} }
return process.env[key] return getServerEnv(key as any)
} }

147
lib/env.ts Normal file
View File

@ -0,0 +1,147 @@
import { env } from "next-runtime-env"
/**
* Server-side environment variables
*/
export interface ServerEnvConfig {
/** Nezha API base URL */
NezhaBaseUrl: string
/** Nezha API authentication token */
NezhaAuth: string
/** Default locale for the application */
DefaultLocale: string
/** Force show all servers */
ForceShowAllServers: boolean
/** Site password */
SitePassword: string
}
/**
* Client-side environment variables (NEXT_PUBLIC_*)
*/
export interface ClientEnvConfig {
/** Nezha data fetch interval in milliseconds */
NezhaFetchInterval: number
/** Show country flags */
ShowFlag: boolean
/** Disable cartoon effects */
DisableCartoon: boolean
/** Show server tags */
ShowTag: boolean
/** Show network transfer information */
ShowNetTransfer: boolean
/** Force use SVG flags */
ForceUseSvgFlag: boolean
/** Fix server names at the top */
FixedTopServerName: boolean
/** Custom logo URL */
CustomLogo: string
/** Custom site title */
CustomTitle: string
/** Custom site description */
CustomDescription: string
/** Custom navigation links */
Links: string
/** Disable search engine indexing */
DisableIndex: boolean
/** Show tag count */
ShowTagCount: boolean
/** Show IP information */
ShowIpInfo: boolean
}
/**
*
*/
export type EnvKey = ServerEnvKey | ClientEnvKey
/**
*
*/
export type ServerEnvKey = keyof ServerEnvConfig
/**
*
*/
export type ClientEnvKey = `NEXT_PUBLIC_${keyof ClientEnvConfig}`
/**
* Get a server-side environment variable
* @param key - Environment variable key
* @returns Environment variable value
*/
export function getServerEnv<K extends keyof ServerEnvConfig>(key: K): string | undefined {
const value = process.env[key]
if (!value) {
console.warn(`Environment variable ${key} is not set`)
return undefined
}
return value
}
/**
* Get a client-side environment variable
* @param key - Environment variable key
* @returns Environment variable value
*/
export function getClientEnv<K extends keyof ClientEnvConfig>(key: K): string | undefined {
const envKey = `NEXT_PUBLIC_${key}`
const value = env(envKey)
if (!value) {
console.warn(`Environment variable ${envKey} is not set`)
return undefined
}
return value
}
/**
* Parse boolean environment variable
* @param value - Environment variable value
* @returns Parsed boolean value
*/
export function parseBoolean(value: string | undefined): boolean {
return value?.toLowerCase() === "true"
}
/**
* Parse number environment variable
* @param value - Environment variable value
* @param defaultValue - Default value if parsing fails
* @returns Parsed number value
*/
export function parseNumber(value: string | undefined, defaultValue: number): number {
if (!value) return defaultValue
const parsed = Number.parseInt(value, 10)
return Number.isNaN(parsed) ? defaultValue : parsed
}
/**
* Get all environment variables with their current values
*/
export function getAllEnvConfig(): { server: ServerEnvConfig; client: ClientEnvConfig } {
return {
server: {
NezhaBaseUrl: getServerEnv("NezhaBaseUrl") || "",
NezhaAuth: getServerEnv("NezhaAuth") || "",
DefaultLocale: getServerEnv("DefaultLocale") || "",
ForceShowAllServers: parseBoolean(getServerEnv("ForceShowAllServers")),
SitePassword: getServerEnv("SitePassword") || "",
},
client: {
NezhaFetchInterval: parseNumber(getClientEnv("NezhaFetchInterval"), 5000),
ShowFlag: parseBoolean(getClientEnv("ShowFlag")),
DisableCartoon: parseBoolean(getClientEnv("DisableCartoon")),
ShowTag: parseBoolean(getClientEnv("ShowTag")),
ShowNetTransfer: parseBoolean(getClientEnv("ShowNetTransfer")),
ForceUseSvgFlag: parseBoolean(getClientEnv("ForceUseSvgFlag")),
FixedTopServerName: parseBoolean(getClientEnv("FixedTopServerName")),
CustomLogo: getClientEnv("CustomLogo") || "",
CustomTitle: getClientEnv("CustomTitle") || "",
CustomDescription: getClientEnv("CustomDescription") || "",
Links: getClientEnv("Links") || "",
DisableIndex: parseBoolean(getClientEnv("DisableIndex")),
ShowTagCount: parseBoolean(getClientEnv("ShowTagCount")),
ShowIpInfo: parseBoolean(getClientEnv("ShowIpInfo")),
},
}
}

View File

@ -112,7 +112,7 @@
"p_1079-1199_Simpleandbeautifuldashbo": "Simple and beautiful dashboard" "p_1079-1199_Simpleandbeautifuldashbo": "Simple and beautiful dashboard"
}, },
"Overview": { "Overview": {
"p_2277-2331_Overview": "👋 Overview", "p_2277-2331_Overview": "Overview",
"p_2390-2457_wherethetimeis": "where the time is" "p_2390-2457_wherethetimeis": "where the time is"
}, },
"Global": { "Global": {

View File

@ -112,7 +112,7 @@
"p_1079-1199_Simpleandbeautifuldashbo": "シンプルで美しいダッシュボード" "p_1079-1199_Simpleandbeautifuldashbo": "シンプルで美しいダッシュボード"
}, },
"Overview": { "Overview": {
"p_2277-2331_Overview": "👋 概要", "p_2277-2331_Overview": "概要",
"p_2390-2457_wherethetimeis": "現在の時間" "p_2390-2457_wherethetimeis": "現在の時間"
}, },
"Global": { "Global": {

View File

@ -112,7 +112,7 @@
"p_1079-1199_Simpleandbeautifuldashbo": "簡單美觀的儀錶板" "p_1079-1199_Simpleandbeautifuldashbo": "簡單美觀的儀錶板"
}, },
"Overview": { "Overview": {
"p_2277-2331_Overview": "👋 概覽", "p_2277-2331_Overview": "概覽",
"p_2390-2457_wherethetimeis": "當前時間" "p_2390-2457_wherethetimeis": "當前時間"
}, },
"Global": { "Global": {

View File

@ -112,7 +112,7 @@
"p_1079-1199_Simpleandbeautifuldashbo": "简单美观的仪表板" "p_1079-1199_Simpleandbeautifuldashbo": "简单美观的仪表板"
}, },
"Overview": { "Overview": {
"p_2277-2331_Overview": "👋 概览", "p_2277-2331_Overview": "概览",
"p_2390-2457_wherethetimeis": "当前时间" "p_2390-2457_wherethetimeis": "当前时间"
}, },
"Global": { "Global": {

View File

@ -1,6 +1,6 @@
{ {
"name": "nezha-dash", "name": "nezha-dash",
"version": "2.5.1", "version": "2.6.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3040", "dev": "next dev -p 3040",
@ -15,6 +15,7 @@
"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.5", "@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-dropdown-menu": "^2.1.5", "@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-label": "^2.1.1", "@radix-ui/react-label": "^2.1.1",
@ -25,19 +26,19 @@
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.7", "@radix-ui/react-tooltip": "^1.1.7",
"@trivago/prettier-plugin-sort-imports": "^5.2.1", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/d3-geo": "^3.1.0", "@types/d3-geo": "^3.1.0",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"caniuse-lite": "^1.0.30001695", "caniuse-lite": "^1.0.30001696",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.0", "cmdk": "^1.0.4",
"country-flag-icons": "^1.5.14", "country-flag-icons": "^1.5.14",
"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.13.0",
"lucide-react": "^0.454.0", "lucide-react": "^0.474.0",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"maxmind": "^4.3.24", "maxmind": "^4.3.24",
"next": "^15.1.6", "next": "^15.1.6",
@ -66,7 +67,7 @@
"postcss": "^8.5.1", "postcss": "^8.5.1",
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"vercel": "^39.3.0" "vercel": "^39.4.2"
}, },
"overrides": { "overrides": {
"react-is": "^19.0.0-rc-69d4b800-20241021" "react-is": "^19.0.0-rc-69d4b800-20241021"