diff --git a/.github/workflows/dashboard-ci.yml b/.github/workflows/dashboard-ci.yml index b8c1fb0..f530073 100644 --- a/.github/workflows/dashboard-ci.yml +++ b/.github/workflows/dashboard-ci.yml @@ -3,12 +3,8 @@ name: Dashboard CI on: push: - paths: - - './dashboard/**' branches: [ "master" ] pull_request: - paths: - - './dashboard/**' jobs: build: diff --git a/dashboard/.eslintrc.cjs b/dashboard/.eslintrc.cjs index b56ba58..466a078 100644 --- a/dashboard/.eslintrc.cjs +++ b/dashboard/.eslintrc.cjs @@ -10,6 +10,8 @@ module.exports = { parser: '@typescript-eslint/parser', plugins: ['react-refresh'], rules: { + '@typescript-eslint/ban-ts-comment': ['off'], + '@typescript-eslint/no-explicit-any': ['off'], 'react-refresh/only-export-components': ['off'], }, -} +}; diff --git a/dashboard/package.json b/dashboard/package.json index 5a7353b..e464d42 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -13,7 +13,9 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { + "@heroicons/react": "^2.1.1", "@hookform/resolvers": "^3.3.4", + "@kodingdotninja/use-tailwind-breakpoint": "^0.0.5", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-aspect-ratio": "^1.0.3", "@radix-ui/react-checkbox": "^1.0.4", @@ -22,11 +24,13 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", + "@react-hook/media-query": "^1.1.1", "@remix-run/router": "^1.15.3", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.7", diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml index 9d1cb92..2647525 100644 --- a/dashboard/pnpm-lock.yaml +++ b/dashboard/pnpm-lock.yaml @@ -5,9 +5,15 @@ settings: excludeLinksFromLockfile: false dependencies: + '@heroicons/react': + specifier: ^2.1.1 + version: 2.1.1(react@18.2.0) '@hookform/resolvers': specifier: ^3.3.4 version: 3.3.4(react-hook-form@7.51.1) + '@kodingdotninja/use-tailwind-breakpoint': + specifier: ^0.0.5 + version: 0.0.5(react@18.2.0) '@radix-ui/react-alert-dialog': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) @@ -32,6 +38,9 @@ dependencies: '@radix-ui/react-popover': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-radio-group': + specifier: ^1.1.3 + version: 1.1.3(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-separator': specifier: ^1.0.3 version: 1.0.3(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) @@ -47,6 +56,9 @@ dependencies: '@radix-ui/react-tooltip': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) + '@react-hook/media-query': + specifier: ^1.1.1 + version: 1.1.1(react@18.2.0) '@remix-run/router': specifier: ^1.15.3 version: 1.15.3 @@ -474,6 +486,14 @@ packages: resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} dev: false + /@heroicons/react@2.1.1(react@18.2.0): + resolution: {integrity: sha512-JyyN9Lo66kirbCMuMMRPtJxtKJoIsXKS569ebHGGRKbl8s4CtUfLnyKJxteA+vIKySocO4s1SkTkGS4xtG/yEA==} + peerDependencies: + react: '>= 16' + dependencies: + react: 18.2.0 + dev: false + /@hookform/resolvers@3.3.4(react-hook-form@7.51.1): resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==} peerDependencies: @@ -545,6 +565,17 @@ packages: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + /@kodingdotninja/use-tailwind-breakpoint@0.0.5(react@18.2.0): + resolution: {integrity: sha512-7h52zL+l+14u9diyjVcGoS5NRIwrc6euIGlHDwtJXlrR3h31RNyTrOfimy7PJTvS4fFdNgbzyrNcGfhj3sDRfQ==} + peerDependencies: + react: '>=16.8' + peerDependenciesMeta: + react: + optional: true + dependencies: + react: 18.2.0 + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1095,6 +1126,36 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-radio-group@1.1.3(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.64)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.64)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.64)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.64)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.64)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.64)(react@18.2.0) + '@types/react': 18.2.64 + '@types/react-dom': 18.2.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} peerDependencies: @@ -1371,6 +1432,14 @@ packages: '@babel/runtime': 7.24.0 dev: false + /@react-hook/media-query@1.1.1(react@18.2.0): + resolution: {integrity: sha512-VM14wDOX5CW5Dn6b2lTiMd79BFMTut9AZj2+vIRT3LCKgMCYmdqruTtzDPSnIVDQdtxdPgtOzvU9oK20LopuOw==} + peerDependencies: + react: '>=16.8' + dependencies: + react: 18.2.0 + dev: false + /@remix-run/router@1.15.3: resolution: {integrity: sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==} engines: {node: '>=14.0.0'} @@ -3707,7 +3776,6 @@ packages: /ufo@1.5.2: resolution: {integrity: sha512-eiutMaL0J2MKdhcOM1tUy13pIrYnyR87fEd8STJQFrrAwImwvlXkxlZEjaKah8r2viPohld08lt73QfLG1NxMg==} - dev: true /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} diff --git a/dashboard/src/components/header/index.tsx b/dashboard/src/components/header/index.tsx new file mode 100644 index 0000000..cf8f24c --- /dev/null +++ b/dashboard/src/components/header/index.tsx @@ -0,0 +1,21 @@ +import { ThemeToggle } from "@marzneshin/features/theme-switch" +import { Link } from "@tanstack/react-router" +import { FC, PropsWithChildren } from "react" + +export const Header: FC = ({ children }) => { + return ( +
+
+ +
+ M +
+ + {children} +
+
+ +
+
+ ) +} diff --git a/dashboard/src/components/index.ts b/dashboard/src/components/index.ts index ed58495..8763cdd 100644 --- a/dashboard/src/components/index.ts +++ b/dashboard/src/components/index.ts @@ -1 +1,3 @@ export * from './ui' +export * from './header' +export * from './sidebar' diff --git a/dashboard/src/components/nav-dir/index.tsx b/dashboard/src/components/nav-dir/index.tsx new file mode 100644 index 0000000..42445b0 --- /dev/null +++ b/dashboard/src/components/nav-dir/index.tsx @@ -0,0 +1,144 @@ + +import { Link, useRouterState } from "@tanstack/react-router"; +import React, { FC } from 'react' +import { + BreadcrumbPage, + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerTrigger, + Breadcrumb, + BreadcrumbEllipsis, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbSeparator, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Button, + SidebarObject, +} from "@marzneshin/components" +import { useBreakpoint } from "@marzneshin/hooks/use-screen-breakpoint"; +import { AppRouterPaths } from "@marzneshin/types"; +import i18n from "@marzneshin/features/i18n"; + +interface Dir { + href: AppRouterPaths; + label: string; +} + +interface NavigationDirectoryProps { + sidebar: SidebarObject; +} + + +function getPathDirs(currentPath: string, sidebar: SidebarObject): Dir[] { + const dirs: Dir[] = []; + const pathArray = currentPath.split('/').filter(Boolean); // Split path and remove empty strings + let fullPath = '/'; + for (let i = 0; i < pathArray.length; i++) { + const dir = pathArray[i]; + fullPath += `/${dir}`; + if (sidebar[dir]) { + dirs.push({ href: sidebar[dir][0].to, label: sidebar[dir][0].title }); + } else { + dirs.push({ href: fullPath as AppRouterPaths, label: i18n.t(dir) }); + } + } + return dirs; +} + +const ITEMS_TO_DISPLAY = 3 + +export const NavigationDirectory: FC = ({ sidebar }) => { + const [open, setOpen] = React.useState(false) + const currentPath = useRouterState().location.pathname; + const items = getPathDirs(currentPath, sidebar); + const isDesktop = !useBreakpoint("sm"); + return ( + + + {items.length > ITEMS_TO_DISPLAY ? ( + <> + + {isDesktop ? ( + + + + + + {items.slice(1, -2).map((item, index) => ( + + + {item.label} + + + ))} + + + ) : ( + + + + + + + Navigate to + + Select a page to navigate to. + + +
+ {items.slice(1, -2).map((item, index) => ( + + {item.label} + + ))} +
+ + + + + +
+
+ )} +
+ + ) : null} + {items.slice(-ITEMS_TO_DISPLAY + 1).map((item, index) => ( + + {item.href ? ( + <> + + {item.label} + + + + ) : ( + + {item.label} + + )} + + ))} +
+
+ ) +} diff --git a/dashboard/src/components/sidebar/index.ts b/dashboard/src/components/sidebar/index.ts new file mode 100644 index 0000000..b0ee519 --- /dev/null +++ b/dashboard/src/components/sidebar/index.ts @@ -0,0 +1,2 @@ +export * from './types' +export * from './sidebar' diff --git a/dashboard/src/components/sidebar/sidebar-body.tsx b/dashboard/src/components/sidebar/sidebar-body.tsx new file mode 100644 index 0000000..3f8642d --- /dev/null +++ b/dashboard/src/components/sidebar/sidebar-body.tsx @@ -0,0 +1,14 @@ +import { FC, PropsWithChildren } from "react" + +export interface SidebarBodyProps + extends PropsWithChildren { + +} + +export const SidebarBody: FC = ({ children }) => { + return ( +
    + {children} +
+ ) +} diff --git a/dashboard/src/components/sidebar/sidebar-footer.tsx b/dashboard/src/components/sidebar/sidebar-footer.tsx new file mode 100644 index 0000000..a1f7d14 --- /dev/null +++ b/dashboard/src/components/sidebar/sidebar-footer.tsx @@ -0,0 +1,12 @@ +import { PropsWithChildren } from "react" + +export interface SidebarFooterProps + extends PropsWithChildren { +} + + +export const SidebarFooter = () => { + return ( +
SidebarHeader =
+ ) +} diff --git a/dashboard/src/components/sidebar/sidebar-group.tsx b/dashboard/src/components/sidebar/sidebar-group.tsx new file mode 100644 index 0000000..1567b20 --- /dev/null +++ b/dashboard/src/components/sidebar/sidebar-group.tsx @@ -0,0 +1,20 @@ + +import { FC, PropsWithChildren } from "react" +import { Label } from "@marzneshin/components" +import { useSidebarContext } from "./sidebar-provider"; + +export interface SidebarGroupProps + extends PropsWithChildren { + +} + +export const SidebarGroup: FC = ({ children }) => { + const { collapsed } = useSidebarContext(); + + if (!collapsed) + return ( + + ) +} diff --git a/dashboard/src/components/sidebar/sidebar-header.tsx b/dashboard/src/components/sidebar/sidebar-header.tsx new file mode 100644 index 0000000..3d71bbd --- /dev/null +++ b/dashboard/src/components/sidebar/sidebar-header.tsx @@ -0,0 +1,12 @@ +import { cn } from "@marzneshin/utils" +import { FC, PropsWithChildren } from "react" + +export interface SidebarHeaderProps extends + PropsWithChildren, + React.HTMLAttributes { } + +export const SidebarHeader: FC = ({ children, className }) => { + return ( +
{children}
+ ) +} diff --git a/dashboard/src/components/sidebar/sidebar-item.tsx b/dashboard/src/components/sidebar/sidebar-item.tsx new file mode 100644 index 0000000..4a132ae --- /dev/null +++ b/dashboard/src/components/sidebar/sidebar-item.tsx @@ -0,0 +1,38 @@ + +import { Link } from '@tanstack/react-router' +import { FC } from 'react' +import { SidebarItem as SidebarItemType } from './types' +import { cn } from '@marzneshin/utils'; +import { VariantProps, cva } from 'class-variance-authority'; +import { useSidebarContext } from './sidebar-provider'; + +const sidebarItemVariants = cva("w-full rounded-lg border p-2", { + variants: { + variant: { + default: "bg-primary-foreground text-primary hover:bg-accent", + active: "bg-primary text-primary-foreground" + }, + size: { + default: "", + collapsed: "" + } + } +}); + + +export interface SidebarItemProps extends + React.HTMLAttributes, + VariantProps { + item: SidebarItemType; +} + +export const SidebarItem: FC = ({ item, className, variant }) => { + const { collapsed } = useSidebarContext(); + return ( +
  • + + {item.icon}{!collapsed && item.title} + +
  • + ) +} diff --git a/dashboard/src/components/sidebar/sidebar-provider.tsx b/dashboard/src/components/sidebar/sidebar-provider.tsx new file mode 100644 index 0000000..67ef4d6 --- /dev/null +++ b/dashboard/src/components/sidebar/sidebar-provider.tsx @@ -0,0 +1,17 @@ +import { createContext, useContext } from "react"; +import { SidebarItemGroup } from "./types"; + +interface SidebarContextProps { + sidebar: SidebarItemGroup; + collapsed: boolean; + setCollapsed: (state: boolean) => void; +} + +export const SidebarContext = createContext(null); + +export const useSidebarContext = () => { + const ctx = useContext(SidebarContext); + if (!ctx) + throw new Error('Sidebar.* component must be rendered as child of Sidebar '); + return ctx; +} diff --git a/dashboard/src/components/sidebar/sidebar-title.tsx b/dashboard/src/components/sidebar/sidebar-title.tsx new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/src/components/sidebar/sidebar.tsx b/dashboard/src/components/sidebar/sidebar.tsx new file mode 100644 index 0000000..d8a35ef --- /dev/null +++ b/dashboard/src/components/sidebar/sidebar.tsx @@ -0,0 +1,54 @@ +import { FC, PropsWithChildren } from "react"; +import { SidebarContext } from "./sidebar-provider" +import { SidebarItemGroup } from "./types" +import { VariantProps, cva } from "class-variance-authority" +import { cn } from "@marzneshin/utils"; +import { SidebarHeader } from "./sidebar-header"; +import { SidebarFooter } from "./sidebar-footer"; +import { SidebarBody } from "./sidebar-body"; +import { SidebarItem } from "./sidebar-item"; +import { SidebarGroup } from "./sidebar-group"; + +const sidebarVariants = cva("flex flex-col justify-between h-full w-full", { + variants: { + variant: { + default: "" + } + } +}); + +interface ContentComposition { + Header: typeof SidebarHeader; + Footer: typeof SidebarFooter; + Body: typeof SidebarBody; + Group: typeof SidebarGroup; + Item: typeof SidebarItem; +} + +export interface SidebarProps + extends React.HTMLAttributes, + VariantProps { + sidebar: SidebarItemGroup; + collapsed: boolean; + setCollapsed: (state: boolean) => void; +} + +const Sidebar: FC & ContentComposition = + ({ collapsed, setCollapsed, sidebar, variant, className, children, ...props }) => { + return ( + +
    + {children} +
    +
    + ) + } + +Sidebar.Header = SidebarHeader; +Sidebar.Footer = SidebarFooter; +Sidebar.Body = SidebarBody; +Sidebar.Item = SidebarItem; +Sidebar.Group = SidebarGroup; +Sidebar.displayName = "Sidebar" + +export { Sidebar, sidebarVariants } diff --git a/dashboard/src/components/sidebar/types.ts b/dashboard/src/components/sidebar/types.ts new file mode 100644 index 0000000..4b5b325 --- /dev/null +++ b/dashboard/src/components/sidebar/types.ts @@ -0,0 +1,17 @@ +import { AppRouterPaths } from "@marzneshin/types"; +import { ReactNode } from "react"; + + +export interface SidebarItem { + title: string; + to: AppRouterPaths; + icon: ReactNode; + isParent: boolean; + subItem?: SidebarItem[]; +} + +export interface SidebarObject { + [key: string]: SidebarItem[]; +} + +export type SidebarItemGroup = Record; diff --git a/dashboard/src/components/ui/radio-group.tsx b/dashboard/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..5b63af3 --- /dev/null +++ b/dashboard/src/components/ui/radio-group.tsx @@ -0,0 +1,42 @@ +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { Circle } from "lucide-react" + +import { cn } from "@marzneshin/utils" + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/dashboard/src/features/sidebar/index.ts b/dashboard/src/features/sidebar/index.ts new file mode 100644 index 0000000..f59e305 --- /dev/null +++ b/dashboard/src/features/sidebar/index.ts @@ -0,0 +1,4 @@ +export * from './sidebar'; +export * from './items'; +export * from './use-panel-toggle'; +export * from './toggle'; diff --git a/dashboard/src/features/sidebar/items.tsx b/dashboard/src/features/sidebar/items.tsx new file mode 100644 index 0000000..e0c106f --- /dev/null +++ b/dashboard/src/features/sidebar/items.tsx @@ -0,0 +1,39 @@ +import { SidebarObject } from '@marzneshin/components'; +import { Box, Home, Server, ServerCog, UsersIcon } from 'lucide-react'; + +export const sidebarItems: SidebarObject = { + Dashboard: [ + { + title: 'Home', + to: '/', + icon: , + isParent: false, + }, + ], + Management: [ + { + title: 'Users', + to: '/users', + icon: , + isParent: false, + }, + { + title: 'Services', + to: '/services', + icon: , + isParent: false, + }, + { + title: 'Nodes', + to: '/nodes', + icon: , + isParent: false, + }, + { + title: 'Hosts', + to: '/hosts', + icon: , + isParent: false, + }, + ] +}; diff --git a/dashboard/src/features/sidebar/sidebar.tsx b/dashboard/src/features/sidebar/sidebar.tsx new file mode 100644 index 0000000..c1e9036 --- /dev/null +++ b/dashboard/src/features/sidebar/sidebar.tsx @@ -0,0 +1,51 @@ +import { Sidebar, SidebarItem } from "@marzneshin/components"; +import { useRouterState } from "@tanstack/react-router"; +import { FC, useEffect } from "react"; +import { sidebarItems as sidebar } from "."; + + +interface DashboardSidebarProps { + collapsed: boolean; + setCollapsed: (state: boolean) => void; +} + +export const DashboardSidebar: FC = ({ collapsed, setCollapsed }) => { + const router = useRouterState(); + const currentActivePath = router.location.pathname; + + useEffect(() => { }, [collapsed]); + + return ( + + ) +} diff --git a/dashboard/src/features/sidebar/toggle.tsx b/dashboard/src/features/sidebar/toggle.tsx new file mode 100644 index 0000000..ced204f --- /dev/null +++ b/dashboard/src/features/sidebar/toggle.tsx @@ -0,0 +1,23 @@ + +import { Button } from '@marzneshin/components'; +import { PanelBottomClose, PanelBottomOpen, PanelRightClose, PanelRightOpen } from 'lucide-react'; +import { FC } from 'react'; + +interface ToggleButtonProps { + isDesktop: boolean; + collapsed: boolean; + open: boolean; + onToggle: () => void; +} + +export const ToggleButton: FC = ({ isDesktop, collapsed, open, onToggle }) => { + const Icon = isDesktop ? + (collapsed ? PanelRightClose : PanelRightOpen) : + (open ? PanelBottomClose : PanelBottomOpen); + + return ( + + ); +}; diff --git a/dashboard/src/features/sidebar/use-panel-toggle.ts b/dashboard/src/features/sidebar/use-panel-toggle.ts new file mode 100644 index 0000000..6494847 --- /dev/null +++ b/dashboard/src/features/sidebar/use-panel-toggle.ts @@ -0,0 +1,26 @@ +import { useRef, useState } from "react"; +import { ImperativePanelHandle } from "react-resizable-panels"; + +export const usePanelToggle = (isDesktop: boolean) => { + const [collapsed, setCollapsed] = useState(false); + const panelRef = useRef(null); + const [open, setOpen] = useState(false); + + const toggleCollapse = () => { + const panel = panelRef.current; + if (isDesktop && panel) { + collapsed ? panel.expand() : panel.collapse(); + setCollapsed(!collapsed); + setOpen(false); + } + }; + + const toggleOpen = () => { + if (!isDesktop) { + setCollapsed(false); + setOpen(!open); + } + }; + + return { collapsed, open, panelRef, setCollapsed, setOpen, toggleCollapse, toggleOpen }; +}; diff --git a/dashboard/src/features/theme-switch/theme-toggle/index.tsx b/dashboard/src/features/theme-switch/theme-toggle/index.tsx index da48279..04140c0 100644 --- a/dashboard/src/features/theme-switch/theme-toggle/index.tsx +++ b/dashboard/src/features/theme-switch/theme-toggle/index.tsx @@ -15,9 +15,9 @@ export function ThemeToggle() { return ( - diff --git a/dashboard/src/hooks/index.ts b/dashboard/src/hooks/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/src/hooks/use-screen-breakpoint.ts b/dashboard/src/hooks/use-screen-breakpoint.ts new file mode 100644 index 0000000..c8c4d5a --- /dev/null +++ b/dashboard/src/hooks/use-screen-breakpoint.ts @@ -0,0 +1,14 @@ +//@ts-expect-error +import create from "@kodingdotninja/use-tailwind-breakpoint"; +import resolveConfig from "tailwindcss/resolveConfig"; + +//@ts-expect-error +import tailwindConfig from "@marzneshin/../tailwind.config.js"; + +const config = resolveConfig(tailwindConfig); + +export const { + useBreakpoint, + useBreakpointEffect, + useBreakpointValue +} = create(config.theme.screens); diff --git a/dashboard/src/routeTree.gen.ts b/dashboard/src/routeTree.gen.ts index eb5d344..5a08674 100644 --- a/dashboard/src/routeTree.gen.ts +++ b/dashboard/src/routeTree.gen.ts @@ -1,6 +1,6 @@ /* prettier-ignore-start */ -/* eslint-disable */ + // @ts-nocheck @@ -14,6 +14,10 @@ import { Route as rootRoute } from './routes/__root' import { Route as DashboardImport } from './routes/_dashboard' import { Route as AuthImport } from './routes/_auth' import { Route as DashboardIndexImport } from './routes/_dashboard/index' +import { Route as DashboardUsersImport } from './routes/_dashboard/users' +import { Route as DashboardServicesImport } from './routes/_dashboard/services' +import { Route as DashboardNodesImport } from './routes/_dashboard/nodes' +import { Route as DashboardHostsImport } from './routes/_dashboard/hosts' import { Route as AuthLoginImport } from './routes/_auth/login' // Create/Update Routes @@ -33,6 +37,26 @@ const DashboardIndexRoute = DashboardIndexImport.update({ getParentRoute: () => DashboardRoute, } as any) +const DashboardUsersRoute = DashboardUsersImport.update({ + path: '/users', + getParentRoute: () => DashboardRoute, +} as any) + +const DashboardServicesRoute = DashboardServicesImport.update({ + path: '/services', + getParentRoute: () => DashboardRoute, +} as any) + +const DashboardNodesRoute = DashboardNodesImport.update({ + path: '/nodes', + getParentRoute: () => DashboardRoute, +} as any) + +const DashboardHostsRoute = DashboardHostsImport.update({ + path: '/hosts', + getParentRoute: () => DashboardRoute, +} as any) + const AuthLoginRoute = AuthLoginImport.update({ path: '/login', getParentRoute: () => AuthRoute, @@ -54,6 +78,22 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthLoginImport parentRoute: typeof AuthImport } + '/_dashboard/hosts': { + preLoaderRoute: typeof DashboardHostsImport + parentRoute: typeof DashboardImport + } + '/_dashboard/nodes': { + preLoaderRoute: typeof DashboardNodesImport + parentRoute: typeof DashboardImport + } + '/_dashboard/services': { + preLoaderRoute: typeof DashboardServicesImport + parentRoute: typeof DashboardImport + } + '/_dashboard/users': { + preLoaderRoute: typeof DashboardUsersImport + parentRoute: typeof DashboardImport + } '/_dashboard/': { preLoaderRoute: typeof DashboardIndexImport parentRoute: typeof DashboardImport @@ -65,7 +105,13 @@ declare module '@tanstack/react-router' { export const routeTree = rootRoute.addChildren([ AuthRoute.addChildren([AuthLoginRoute]), - DashboardRoute.addChildren([DashboardIndexRoute]), + DashboardRoute.addChildren([ + DashboardHostsRoute, + DashboardNodesRoute, + DashboardServicesRoute, + DashboardUsersRoute, + DashboardIndexRoute, + ]), ]) /* prettier-ignore-end */ diff --git a/dashboard/src/routes/_auth.tsx b/dashboard/src/routes/_auth.tsx index b9aa0eb..9a7aaf4 100644 --- a/dashboard/src/routes/_auth.tsx +++ b/dashboard/src/routes/_auth.tsx @@ -6,7 +6,7 @@ const AuthLayout = () => {
    -
    +
    ) } diff --git a/dashboard/src/routes/_dashboard.tsx b/dashboard/src/routes/_dashboard.tsx index 857bbc9..e8decf1 100644 --- a/dashboard/src/routes/_dashboard.tsx +++ b/dashboard/src/routes/_dashboard.tsx @@ -1,8 +1,78 @@ +import { + Drawer, + DrawerContent, + Header, + ResizableHandle, + ResizablePanel, + ResizablePanelGroup +} from '@marzneshin/components' +import { NavigationDirectory } from '@marzneshin/components/nav-dir'; import { useAuth } from '@marzneshin/features/auth' -import { createFileRoute, redirect } from '@tanstack/react-router' +import { DashboardSidebar, ToggleButton, sidebarItems } from '@marzneshin/features/sidebar'; +import { usePanelToggle } from '@marzneshin/features/sidebar/use-panel-toggle'; +import { useBreakpoint } from '@marzneshin/hooks/use-screen-breakpoint'; +import { cn } from '@marzneshin/utils'; +import { Outlet, createFileRoute, redirect } from '@tanstack/react-router' + +export const DashboardLayout = () => { + const isDesktop = useBreakpoint("sm"); + 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-[100px]")} + defaultSize={20} + ref={panelRef} + maxSize={30}> + + + + +
    + +
    +
    +
    + ) : ( +
    + +
    + +
    +
    + )} +
    + ) +} export const Route = createFileRoute('/_dashboard')({ - component: () =>
    Hello /_dashboard!
    , + component: () => , beforeLoad: async () => { if (!useAuth.getState().isLoggedIn) { diff --git a/dashboard/src/routes/_dashboard/hosts.tsx b/dashboard/src/routes/_dashboard/hosts.tsx new file mode 100644 index 0000000..a73508f --- /dev/null +++ b/dashboard/src/routes/_dashboard/hosts.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_dashboard/hosts')({ + component: () =>
    Hello /_dashboard/hosts!
    +}) \ No newline at end of file diff --git a/dashboard/src/routes/_dashboard/nodes.tsx b/dashboard/src/routes/_dashboard/nodes.tsx new file mode 100644 index 0000000..1e06b51 --- /dev/null +++ b/dashboard/src/routes/_dashboard/nodes.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_dashboard/nodes')({ + component: () =>
    Hello /_dashboard/nodes!
    +}) \ No newline at end of file diff --git a/dashboard/src/routes/_dashboard/services.tsx b/dashboard/src/routes/_dashboard/services.tsx new file mode 100644 index 0000000..1206c52 --- /dev/null +++ b/dashboard/src/routes/_dashboard/services.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_dashboard/services')({ + component: () =>
    Hello /_dashboard/services!
    +}) diff --git a/dashboard/src/routes/_dashboard/users.tsx b/dashboard/src/routes/_dashboard/users.tsx new file mode 100644 index 0000000..383e1be --- /dev/null +++ b/dashboard/src/routes/_dashboard/users.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_dashboard/users')({ + component: () =>
    Hello /_dashboard/users!
    +}) \ No newline at end of file diff --git a/dashboard/src/types/index.ts b/dashboard/src/types/index.ts new file mode 100644 index 0000000..be5b633 --- /dev/null +++ b/dashboard/src/types/index.ts @@ -0,0 +1,4 @@ +import { routeTree } from "@marzneshin/routeTree.gen"; +import { RoutePaths } from "@tanstack/react-router"; + +export type AppRouterPaths = RoutePaths;