diff --git a/bun.lockb b/bun.lockb index fc35a79..13d1157 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index a802d6c..bb905cc 100644 --- a/package.json +++ b/package.json @@ -16,55 +16,56 @@ "@heroicons/react": "^2.2.0", "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-checkbox": "^1.1.3", - "@radix-ui/react-dialog": "^1.1.4", - "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-dialog": "^1.1.5", + "@radix-ui/react-dropdown-menu": "^2.1.5", "@radix-ui/react-label": "^2.1.1", - "@radix-ui/react-popover": "^1.1.4", + "@radix-ui/react-popover": "^1.1.5", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.2", - "@tanstack/react-query": "^5.63.0", - "@tanstack/react-query-devtools": "^5.63.0", + "@tanstack/react-query": "^5.64.2", + "@tanstack/react-query-devtools": "^5.64.2", "@tanstack/react-table": "^8.20.6", "@trivago/prettier-plugin-sort-imports": "^5.2.1", "@types/d3-geo": "^3.1.0", "@types/luxon": "^3.4.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "country-flag-icons": "^1.5.13", + "cmdk": "1.0.0", + "country-flag-icons": "^1.5.14", "d3-geo": "^3.1.1", "dayjs": "^1.11.13", - "framer-motion": "^12.0.0-alpha.2", + "framer-motion": "^12.0.3", "i18n-iso-countries": "^7.13.0", "i18next": "^24.2.1", "lucide-react": "^0.460.0", "luxon": "^3.5.0", - "prettier-plugin-tailwindcss": "^0.6.9", + "prettier-plugin-tailwindcss": "^0.6.10", "react": "^19.0.0", "react-dom": "^19.0.0", "react-i18next": "^15.4.0", - "react-router-dom": "^7.1.1", + "react-router-dom": "^7.1.3", "recharts": "^2.15.0", - "sonner": "^1.7.1", + "sonner": "^1.7.2", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "@eslint/js": "^9.17.0", - "@types/node": "^22.10.5", - "@types/react": "^19.0.4", - "@types/react-dom": "^19.0.2", + "@eslint/js": "^9.18.0", + "@types/node": "^22.10.9", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", "@vitejs/plugin-react-swc": "^3.7.2", "autoprefixer": "^10.4.20", - "eslint": "^9.17.0", + "eslint": "^9.18.0", "eslint-plugin-react-hooks": "^5.1.0", - "eslint-plugin-react-refresh": "^0.4.16", + "eslint-plugin-react-refresh": "^0.4.18", "globals": "^15.14.0", - "postcss": "^8.4.49", + "postcss": "^8.5.1", "tailwindcss": "^3.4.17", "typescript": "~5.6.3", - "typescript-eslint": "^8.19.1", - "vite": "^6.0.7" + "typescript-eslint": "^8.21.0", + "vite": "^6.0.11" } } diff --git a/src/App.tsx b/src/App.tsx index 4866b31..1b01be3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import ErrorPage from "./pages/ErrorPage" import NotFound from "./pages/NotFound" import Server from "./pages/Server" import ServerDetail from "./pages/ServerDetail" +import { DashCommand } from "./components/DashCommand" const App: React.FC = () => { const { data: settingData, error } = useQuery({ @@ -62,9 +63,7 @@ const App: React.FC = () => { i18n.changeLanguage(settingData?.data?.config?.language) } - const customMobileBackgroundImage = - // @ts-expect-error CustomMobileBackgroundImage is a global variable - (window.CustomMobileBackgroundImage as string) !== "" ? window.CustomMobileBackgroundImage : undefined + const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined return ( @@ -92,6 +91,7 @@ const App: React.FC = () => {
+ } /> } /> diff --git a/src/components/DashCommand.tsx b/src/components/DashCommand.tsx new file mode 100644 index 0000000..d847eb2 --- /dev/null +++ b/src/components/DashCommand.tsx @@ -0,0 +1,114 @@ +"use client" + +import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command" +import { useTheme } from "@/hooks/use-theme" +import { useWebSocketContext } from "@/hooks/use-websocket-context" +import { formatNezhaInfo } from "@/lib/utils" +import { NezhaWebsocketResponse } from "@/types/nezha-api" +import { Home, Moon, Sun, SunMoon } from "lucide-react" +import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" + +export function DashCommand() { + const [open, setOpen] = useState(false) + const [search, setSearch] = useState("") + const navigate = useNavigate() + const { t } = useTranslation() + const { setTheme } = useTheme() + + const { lastMessage, connected } = useWebSocketContext() + + const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null + + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault() + setOpen((open) => !open) + } + } + + document.addEventListener("keydown", down) + return () => document.removeEventListener("keydown", down) + }, []) + + if (!connected || !nezhaWsData) return null + + const shortcuts = [ + { + keywords: ["home", "homepage"], + icon: , + label: t("Home"), + action: () => navigate("/"), + }, + { + keywords: ["light", "theme", "lightmode"], + icon: , + label: t("ToggleLightMode"), + action: () => setTheme("light"), + }, + { + keywords: ["dark", "theme", "darkmode"], + icon: , + label: t("ToggleDarkMode"), + action: () => setTheme("dark"), + }, + { + keywords: ["system", "theme", "systemmode"], + icon: , + label: t("ToggleSystemMode"), + action: () => setTheme("system"), + }, + ].map((item) => ({ + ...item, + value: `${item.keywords.join(" ")} ${item.label}`, + })) + + return ( + <> + + + + {t("NoResults")} + + {nezhaWsData.servers.map((server) => ( + { + navigate(`/server/${server.id}`) + setOpen(false) + }} + > + {formatNezhaInfo(nezhaWsData.now, server).online ? ( + + ) : ( + + )} + {server.name} + + ))} + + + + + {shortcuts.map((item) => ( + { + item.action() + setOpen(false) + }} + > + {item.icon} + {item.label} + + ))} + + + + + ) +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 11d64d9..5718b71 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -41,6 +41,8 @@ function Header() { // @ts-expect-error CustomDesc is a global variable const customDesc = window.CustomDesc || t("nezha") + const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined + useEffect(() => { const link = document.querySelector("link[rel*='icon']") || document.createElement("link") // @ts-expect-error set link.type @@ -109,6 +111,7 @@ function Header() { onClick={handleBackgroundToggle} className={cn("rounded-full px-[9px] bg-white dark:bg-black", { "bg-white/70 dark:bg-black/70": customBackgroundImage, + "hidden sm:block": customMobileBackgroundImage, })} > diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..2d76f1e --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,107 @@ +"use client" + +import { Dialog, DialogContent } from "@/components/ui/dialog" +import { cn } from "@/lib/utils" +import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" +import * as React from "react" + +const Command = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, ...props }, ref) => ( + + ), +) +Command.displayName = CommandPrimitive.displayName + +const CommandDialog = ({ children, ...props }: DialogProps) => { + return ( + + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, ...props }, ref) => ( +
+ + +
+ ), +) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, ...props }, ref) => ( + + ), +) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef, React.ComponentPropsWithoutRef>( + (props, ref) => , +) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, ...props }, ref) => ( + + ), +) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, ...props }, ref) => ( + + ), +) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return +} +CommandShortcut.displayName = "CommandShortcut" + +export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator } diff --git a/src/hooks/use-background.ts b/src/hooks/use-background.ts index 1f70651..addcac0 100644 --- a/src/hooks/use-background.ts +++ b/src/hooks/use-background.ts @@ -3,6 +3,7 @@ import { useEffect, useState } from "react" declare global { interface Window { CustomBackgroundImage: string + CustomMobileBackgroundImage: string } } diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 9ea7cd0..98811dd 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -116,5 +116,13 @@ "price": "Price", "free": "Free", "usage-baseed": "Usage-based" - } + }, + "TypeCommand": "Type a command or search...", + "NoResults": "No results found.", + "Servers": "Servers", + "Shortcuts": "Shortcuts", + "ToggleLightMode": "Toggle Light Mode", + "ToggleDarkMode": "Toggle Dark Mode", + "ToggleSystemMode": "Toggle System Mode", + "Home": "Home" } diff --git a/src/locales/ta/translation.json b/src/locales/ta/translation.json new file mode 100644 index 0000000..e99e0f9 --- /dev/null +++ b/src/locales/ta/translation.json @@ -0,0 +1,120 @@ +{ + "nezha": "கண்காணிப்பு", + "overview": "கண்ணோட்டம்", + "dashboard": "முகப்புப்பெட்டி", + "login": "புகுபதிவு", + "serverCard": { + "mem": "மெம்", + "stg": "Stg", + "days": "நாட்கள்", + "hours": "மணி", + "upload": "பதிவேற்றும்", + "download": "பதிவிறக்கம்", + "system": "மண்டலம்", + "uptime": "நேரம்", + "totalUpload": "பதிவேற்றும்", + "totalDownload": "பதிவிறக்கம்" + }, + "online": "ஆன்லைனில்", + "offline": "இணையமில்லாமல்", + "whereTheTimeIs": "நேரம் இருக்கும் இடம்", + "refreshing": "புத்துணர்ச்சி", + "info": { + "websocketConnecting": "வெப்சாக்கெட் இணைத்தல்", + "websocketConnected": "வெப்சாக்கெட் இணைக்கப்பட்டுள்ளது", + "websocketDisconnected": "வெப்சாக்கெட் துண்டிக்கப்பட்டது", + "processing": "செயலாக்கம் ..." + }, + "serverOverview": { + "totalServers": "மொத்த சேவையகங்கள்", + "onlineServers": "நிகழ்நிலை சேவையகங்கள்", + "offlineServers": "இணைப்பில்லாத சேவையகங்கள்", + "totalBandwidth": "மொத்த அலைவரிசை", + "speed": "வேகம்", + "network": "பிணையம்" + }, + "map": { + "Distributions": "சேவையகங்கள் விநியோகிக்கப்படுகின்றன", + "Regions": "பகுதிகள்", + "Servers": "சேவையகங்கள்" + }, + "cycleTransfer": { + "used": "பயன்படுத்தப்பட்டது", + "total": "மொத்தம்", + "nextUpdate": "அடுத்த புதுப்பிப்பு" + }, + "serverDetail": { + "offline": "இணையமில்லாமல்", + "unknown": "தெரியவில்லை", + "uptime": "நேரம்", + "version": "பதிப்பு", + "arch": "மான்", + "mem": "மெம்", + "disk": "வட்டு", + "region": "பகுதி", + "system": "மண்டலம்", + "status": "நிலை", + "online": "ஆன்லைனில்", + "days": "நாட்கள்", + "upload": "பதிவேற்றும்", + "download": "பதிவிறக்கம்", + "lastActive": "கடைசி செயலில் நேரம்", + "temperature": "வெப்பநிலை" + }, + "serverDetailChart": { + "swap": "இடமாற்றம்", + "upload": "பதிவேற்றும்", + "download": "பதிவிறக்கம்", + "process": "செயல்முறை", + "disk": "வட்டு", + "mem": "மெம்" + }, + "footer": { + "themeBy": "மூலம் கருப்பொருள் " + }, + "language": { + "zh-CN": "எளிமைப்படுத்தப்பட்ட சீன", + "zh-TW": "பாரம்பரிய சீன", + "en-US": "ஆங்கிலம்" + }, + "theme": { + "light": "ஒளி", + "dark": "இருண்ட", + "system": "மண்டலம்" + }, + "error": { + "pageNotFound": "பக்கம் கிடைக்கவில்லை", + "backToHome": "வீட்டிற்கு திரும்பவும்" + }, + "tabSwitch": { + "Detail": "விவரம்", + "Network": "பிணையம்" + }, + "monitor": { + "noData": "சேவையக மானிட்டர் தரவு இல்லை, முதலில் ஒரு பணி மானிட்டரைச் சேர்க்கவும்", + "avgDelay": "சுணக்கம்", + "monitorCount": "சேவைகள்" + }, + "pwa": { + "offlineReady": "ஆஃப்லைனில் வேலை செய்ய பயன்பாடு தயாராக உள்ளது", + "newContent": "புதிய உள்ளடக்கம் கிடைக்கிறது", + "reload": "புதுப்பிப்பு" + }, + "billingInfo": { + "remaining": "மீதமுள்ள", + "error": "பிழை", + "indefinite": "காலவரையற்றது", + "expired": "காலாவதியான", + "days": "நாட்கள்", + "price": "விலை", + "free": "இலவசம்", + "usage-baseed": "பயன்பாடு அடிப்படையிலானது" + }, + "serviceTracker": { + "noService": "பணி தரவு இல்லை", + "uptime": "நேரம்", + "daysAgo": "சில நாட்களுக்கு முன்பு", + "today": "இன்று", + "loading": "ஏற்றுகிறது ..." + } +} diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index 467bef8..0fdf110 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -116,5 +116,13 @@ "price": "价格", "free": "免费", "usage-baseed": "按量计费" - } + }, + "TypeCommand": "输入命令或搜索", + "NoResults": "结果为空", + "Servers": "服务器", + "Shortcuts": "快捷键", + "ToggleLightMode": "切换亮色模式", + "ToggleDarkMode": "切换暗色模式", + "ToggleSystemMode": "切换系统模式", + "Home": "首页" } diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index c472168..969acb7 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -112,5 +112,13 @@ "price": "價格", "free": "免費", "usage-baseed": "按量計費" - } + }, + "TypeCommand": "輸入命令或搜尋", + "NoResults": "沒有結果", + "Servers": "伺服器", + "Shortcuts": "快捷鍵", + "ToggleLightMode": "切換亮色模式", + "ToggleDarkMode": "切換暗色模式", + "ToggleSystemMode": "切換系統模式", + "Home": "首頁" } diff --git a/src/pages/Server.tsx b/src/pages/Server.tsx index 7a95628..db37c74 100644 --- a/src/pages/Server.tsx +++ b/src/pages/Server.tsx @@ -72,7 +72,16 @@ export default function Servers() { restoreScrollPosition() }, []) - const groupTabs = ["All", ...(groupData?.data?.map((item: ServerGroup) => item.group.name) || [])] + const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null + + const groupTabs = [ + "All", + ...(groupData?.data + ?.filter((item: ServerGroup) => { + return Array.isArray(item.servers) && item.servers.some((serverId) => nezhaWsData?.servers?.some((server) => server.id === serverId)) + }) + ?.map((item: ServerGroup) => item.group.name) || []), + ] if (!connected && !lastMessage) { return ( @@ -85,8 +94,6 @@ export default function Servers() { ) } - const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null - if (!nezhaWsData) { return (