diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index 00fb43d..268b082 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -2,6 +2,7 @@ import Footer from "@/app/(main)/footer" import Header from "@/app/(main)/header" import { ServerDataProvider } from "@/app/context/server-data-context" import { auth } from "@/auth" +import { DashCommand } from "@/components/DashCommand" import { SignIn } from "@/components/SignIn" import getEnv from "@/lib/env-entry" import type React from "react" @@ -15,7 +16,10 @@ export default function MainLayout({ children }: DashboardProps) {
- {children} + + + {children} +
diff --git a/app/layout.tsx b/app/layout.tsx index b595453..82d703d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,5 @@ import { FilterProvider } from "@/app/context/network-filter-context" import { StatusProvider } from "@/app/context/status-context" -// @auto-i18n-check. Please do not delete the line. import { ThemeColorManager } from "@/components/ThemeColorManager" import getEnv from "@/lib/env-entry" import { cn } from "@/lib/utils" diff --git a/bun.lockb b/bun.lockb index 2f0b772..88388b7 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/DashCommand.tsx b/components/DashCommand.tsx new file mode 100644 index 0000000..7595f80 --- /dev/null +++ b/components/DashCommand.tsx @@ -0,0 +1,137 @@ +"use client" + +import { Home, Languages, Moon, Sun, SunMoon } from "lucide-react" + +import { useServerData } from "@/app/context/server-data-context" +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command" +import { localeItems } from "@/i18n-metadata" +import { setUserLocale } from "@/i18n/locale" +import { useTranslations } from "next-intl" +import { useTheme } from "next-themes" +import { useRouter } from "next/navigation" +import { useEffect, useState } from "react" + +export function DashCommand() { + const [open, setOpen] = useState(false) + const [search, setSearch] = useState("") + const { data } = useServerData() + const router = useRouter() + const { setTheme } = useTheme() + const t = useTranslations("DashCommand") + + 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 (!data?.result) return null + + const sortedServers = data.result.sort((a, b) => { + const displayIndexDiff = (b.display_index || 0) - (a.display_index || 0) + if (displayIndexDiff !== 0) return displayIndexDiff + return a.id - b.id + }) + + // 添加语言切换快捷方式 + const languageShortcuts = localeItems.map((item) => ({ + keywords: ["language", "locale", item.code.toLowerCase()], + icon: , + label: item.name, + action: () => setUserLocale(item.code), + value: `language ${item.name.toLowerCase()} ${item.code}`, + })) + + // 将语言快捷方式添加到现有的shortcuts数组中 + const shortcuts = [ + { + keywords: ["home", "homepage"], + icon: , + label: t("Home"), + action: () => router.push("/"), + }, + { + 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"), + }, + ...languageShortcuts, + ].map((item) => ({ + ...item, + value: `${item.keywords.join(" ")} ${item.label}`, + })) + + return ( + <> + + + + {t("NoResults")} + + {sortedServers.map((server) => ( + { + router.push(`/server/${server.id}`) + setOpen(false) + }} + > + {server.online_status ? ( + + ) : ( + + )} + {server.name} + + ))} + + + + + {shortcuts.map((item) => ( + { + item.action() + setOpen(false) + }} + > + {item.icon} + {item.label} + + ))} + + + + + ) +} diff --git a/components/ui/command.tsx b/components/ui/command.tsx new file mode 100644 index 0000000..d29ada4 --- /dev/null +++ b/components/ui/command.tsx @@ -0,0 +1,147 @@ +"use client" + +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" + +import { Dialog, DialogContent } from "@/components/ui/dialog" +import { cn } from "@/lib/utils" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +const CommandDialog = ({ children, ...props }: DialogProps) => { + return ( + + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + 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.ElementRef, + 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/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..fbd7e86 --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,104 @@ +"use client" + +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/messages/en.json b/messages/en.json index f1d1edf..03872bf 100644 --- a/messages/en.json +++ b/messages/en.json @@ -125,5 +125,15 @@ "h1_490-590_404NotFound": "Server Not Found", "h1_490-590_404NotFoundBack": " Press here to go back", "h1_490-590_Error": "Something went wrong" + }, + "DashCommand": { + "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/messages/ja.json b/messages/ja.json index 76fb128..c4580b7 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -125,5 +125,15 @@ "h1_490-590_404NotFound": "サーバーは見つかりません", "h1_490-590_404NotFoundBack": "戻る", "h1_490-590_Error": "何らかの問題が発生しました" + }, + "DashCommand": { + "TypeCommand": "コマンドを入力してください", + "NoResults": "結果は見つかりませんでした", + "Servers": "サーバー", + "Shortcuts": "ショートカット", + "ToggleLightMode": "ライトモードに切り替え", + "ToggleDarkMode": "ダークモードに切り替え", + "ToggleSystemMode": "システムモードに切り替え", + "Home": "ホーム" } } diff --git a/messages/zh-t.json b/messages/zh-t.json index 477ae3f..8fb39ab 100644 --- a/messages/zh-t.json +++ b/messages/zh-t.json @@ -125,5 +125,15 @@ "h1_490-590_404NotFound": "伺服器未找到", "h1_490-590_404NotFoundBack": "返回", "h1_490-590_Error": "發生錯誤" + }, + "DashCommand": { + "TypeCommand": "輸入命令或搜尋", + "NoResults": "沒有結果", + "Servers": "伺服器", + "Shortcuts": "快捷鍵", + "ToggleLightMode": "切換亮色模式", + "ToggleDarkMode": "切換暗色模式", + "ToggleSystemMode": "切換系統模式", + "Home": "首頁" } } diff --git a/messages/zh.json b/messages/zh.json index 6346934..0ed66f0 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -125,5 +125,15 @@ "h1_490-590_404NotFound": "服务器不存在", "h1_490-590_404NotFoundBack": "返回", "h1_490-590_Error": "发生错误" + }, + "DashCommand": { + "TypeCommand": "输入命令或搜索", + "NoResults": "结果为空", + "Servers": "服务器", + "Shortcuts": "快捷键", + "ToggleLightMode": "切换亮色模式", + "ToggleDarkMode": "切换暗色模式", + "ToggleSystemMode": "切换系统模式", + "Home": "首页" } } diff --git a/package.json b/package.json index ec83ceb..1242742 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "caniuse-lite": "^1.0.30001695", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "1.0.0", "country-flag-icons": "^1.5.14", "d3-geo": "^3.1.1", "d3-selection": "^3.0.0", @@ -65,11 +66,11 @@ "@biomejs/biome": "1.9.4", "@next/bundle-analyzer": "^15.1.6", "@tailwindcss/postcss": "^4.0.0", - "@types/node": "^22.10.7", - "@types/react": "^19.0.7", + "@types/node": "^22.10.9", + "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "eslint-config-next": "^15.1.6", - "eslint-plugin-turbo": "^2.3.3", + "eslint-plugin-turbo": "^2.3.4", "eslint-plugin-unused-imports": "^4.1.4", "postcss": "^8.5.1", "tailwindcss": "^4.0.0", @@ -78,6 +79,5 @@ }, "overrides": { "react-is": "^19.0.0-rc-69d4b800-20241021" - }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + } }