diff --git a/dashboard/package.json b/dashboard/package.json index 8578441..1c7f1cd 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -64,6 +64,7 @@ "@vitest/coverage-istanbul": "^1.6.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "date-fns": "^3.6.0", "dayjs": "^1.11.11", "framer-motion": "^11.2.4", diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml index 6722a89..96a6787 100644 --- a/dashboard/pnpm-lock.yaml +++ b/dashboard/pnpm-lock.yaml @@ -152,6 +152,9 @@ dependencies: clsx: specifier: ^2.1.1 version: 2.1.1 + cmdk: + specifier: ^1.0.0 + version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) date-fns: specifier: ^3.6.0 version: 3.6.0 @@ -7271,6 +7274,21 @@ packages: engines: {node: '>=6'} dev: false + /cmdk@1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + dev: false + /codemirror@5.65.16: resolution: {integrity: sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg==} dev: false diff --git a/dashboard/src/components/header/index.tsx b/dashboard/src/components/header/index.tsx index 9e75aa0..5bf28ec 100644 --- a/dashboard/src/components/header/index.tsx +++ b/dashboard/src/components/header/index.tsx @@ -2,24 +2,26 @@ import { LanguageSwitch } from "@marzneshin/features/language-switch"; import { ThemeToggle } from "@marzneshin/features/theme-switch"; import { Logout } from "@marzneshin/features/auth"; import { Link } from "@tanstack/react-router"; -import { FC, PropsWithChildren } from "react"; +import { CommandBox } from "@marzneshin/features/search-command"; +import type { FC, PropsWithChildren } from "react"; export const Header: FC = ({ children }) => { - return ( -
-
- -
- M -
- - {children} -
-
- - - -
-
- ); + return ( +
+
+ +
+ M +
+ + {children} +
+ +
+ + + +
+
+ ); }; diff --git a/dashboard/src/components/ui/command.tsx b/dashboard/src/components/ui/command.tsx new file mode 100644 index 0000000..3152023 --- /dev/null +++ b/dashboard/src/components/ui/command.tsx @@ -0,0 +1,153 @@ +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@marzneshin/utils" +import { Dialog, DialogContent } from "@marzneshin/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + 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/dashboard/src/components/ui/index.ts b/dashboard/src/components/ui/index.ts index e633c4e..4a47b94 100644 --- a/dashboard/src/components/ui/index.ts +++ b/dashboard/src/components/ui/index.ts @@ -32,3 +32,4 @@ export * from "./toggle-group"; export * from "./textarea"; export * from "./radio-group"; export * from "./accordion"; +export * from "./command"; diff --git a/dashboard/src/features/search-command/command-box.tsx b/dashboard/src/features/search-command/command-box.tsx new file mode 100644 index 0000000..b6fc29d --- /dev/null +++ b/dashboard/src/features/search-command/command-box.tsx @@ -0,0 +1,102 @@ +import { + UsersIcon, + ServerIcon, + BoxIcon, + ServerCogIcon, + SettingsIcon, +} from "lucide-react"; +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@marzneshin/components"; +import { useNavigate } from "@tanstack/react-router"; +import { SearchBox } from "./search-box"; + +export function CommandBox() { + const [open, setOpen] = useState(false); + const { t } = useTranslation(); + const navigate = useNavigate(); + + 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); + }, []); + + return ( + <> + setOpen(true)} /> + + + + No results found. + + { + navigate({ to: "/users" }); + setOpen(false); + }} + > + + {t("users")} + + { + navigate({ to: "/services" }); + setOpen(false); + }} + > + + {t("services")} + + { + navigate({ to: "/nodes" }); + setOpen(false); + }} + > + + {t("nodes")} + + { + navigate({ to: "/hosts" }); + setOpen(false); + }} + > + + {t("hosts")} + + + + + { + navigate({ to: "/settings" }); + setOpen(false); + }} + > + + {t("settings")} + + + + + + ); +} diff --git a/dashboard/src/features/search-command/index.ts b/dashboard/src/features/search-command/index.ts new file mode 100644 index 0000000..40b9c43 --- /dev/null +++ b/dashboard/src/features/search-command/index.ts @@ -0,0 +1,2 @@ +export * from "./command-box"; +export * from "./search-box"; diff --git a/dashboard/src/features/search-command/search-box.tsx b/dashboard/src/features/search-command/search-box.tsx new file mode 100644 index 0000000..6c948b3 --- /dev/null +++ b/dashboard/src/features/search-command/search-box.tsx @@ -0,0 +1,19 @@ +import type { FC, HTMLAttributes } from "react"; +import { SearchIcon } from "lucide-react"; + +export const SearchBox: FC> = ({ ...props }) => { + return ( +
+
+ +

Search

+
+ + CTRLK + +
+ ); +}; diff --git a/dashboard/src/routes/_dashboard.tsx b/dashboard/src/routes/_dashboard.tsx index e5c206f..23e4169 100644 --- a/dashboard/src/routes/_dashboard.tsx +++ b/dashboard/src/routes/_dashboard.tsx @@ -1,88 +1,103 @@ import { - Drawer, - DrawerContent, - Header, - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, - Toaster, -} from '@marzneshin/components' -import { NavigationDirectory } from '@marzneshin/components/nav-dir'; -import { useAuth } from '@marzneshin/features/auth' -import { DashboardSidebar, ToggleButton, sidebarItems } from '@marzneshin/features/sidebar'; -import { usePanelToggle } from '@marzneshin/features/sidebar/use-panel-toggle'; -import { useScreenBreakpoint } from '@marzneshin/hooks/use-screen-breakpoint'; -import { cn } from '@marzneshin/utils'; -import { Outlet, createFileRoute, redirect } from '@tanstack/react-router' + Drawer, + DrawerContent, + Header, + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, + Toaster, +} from "@marzneshin/components"; +import { useAuth } from "@marzneshin/features/auth"; +import { + DashboardSidebar, + ToggleButton, +} from "@marzneshin/features/sidebar"; +import { usePanelToggle } from "@marzneshin/features/sidebar/use-panel-toggle"; +import { useScreenBreakpoint } from "@marzneshin/hooks/use-screen-breakpoint"; +import { cn } from "@marzneshin/utils"; +import { Outlet, createFileRoute, redirect } from "@tanstack/react-router"; export const DashboardLayout = () => { - const isDesktop = useScreenBreakpoint("md"); - const { collapsed, panelRef, open, setCollapsed, setOpen, toggleCollapse, toggleOpen } = usePanelToggle(isDesktop); + const isDesktop = useScreenBreakpoint("md"); + const { + collapsed, + panelRef, + open, + setCollapsed, + setOpen, + toggleCollapse, + toggleOpen, + } = usePanelToggle(isDesktop); - return ( -
-
-
- - -
-
- {isDesktop ? - ( - - setCollapsed(true)} - onExpand={() => setCollapsed(false)} - minSize={15} - className={cn("w-[120px] min-w-[70px]")} - defaultSize={20} - ref={panelRef} - maxSize={30}> - - - - -
- - -
-
-
- ) : ( -
- -
- - -
-
- )} -
- ) -} + return ( +
+
+
+ +
+
+ {isDesktop ? ( + + setCollapsed(true)} + onExpand={() => setCollapsed(false)} + minSize={15} + className={cn("w-[120px] min-w-[70px]")} + defaultSize={20} + ref={panelRef} + maxSize={30} + > + + + + +
+ + +
+
+
+ ) : ( +
+ +
+ + +
+
+ )} +
+ ); +}; -export const Route = createFileRoute('/_dashboard')({ - component: () => , +export const Route = createFileRoute("/_dashboard")({ + component: () => , - beforeLoad: async () => { - const loggedIn = await useAuth.getState().isLoggedIn() - if (!loggedIn) { - throw redirect({ - to: '/login', - }) - } - } -}) + beforeLoad: async () => { + const loggedIn = await useAuth.getState().isLoggedIn(); + if (!loggedIn) { + throw redirect({ + to: "/login", + }); + } + }, +});