diff --git a/apps/ui/src/App.tsx b/apps/ui/src/App.tsx index 3f88216..3843c31 100644 --- a/apps/ui/src/App.tsx +++ b/apps/ui/src/App.tsx @@ -33,7 +33,7 @@ const queryClient = new QueryClient({ export default function App() { return ( - + diff --git a/apps/ui/src/configs/configureSidebar.tsx b/apps/ui/src/configs/configureSidebar.tsx new file mode 100644 index 0000000..ff01f23 --- /dev/null +++ b/apps/ui/src/configs/configureSidebar.tsx @@ -0,0 +1,26 @@ +import type { AppNavbarLink } from "@/types/sidebar"; +import { Settings, SquareGanttChart, Users } from "lucide-react"; + +export const sidebarItems: AppNavbarLink[] = [ + { + id: "overview", + url: "/dashboard", + label: "Overview", + icon: SquareGanttChart, + }, + { + id: "system", + label: "System", + icon: Settings, + keywords: ["system", "setting"], + children: [ + { + id: "users", + url: "/dashboard/users", + label: "User management", + icon: Users, + keywords: ["users"], + }, + ], + }, +]; diff --git a/apps/ui/src/pages/DasboardLayout/components/AppNavbar.tsx b/apps/ui/src/pages/DasboardLayout/components/AppNavbar.tsx index 50fa14a..803eeaf 100644 --- a/apps/ui/src/pages/DasboardLayout/components/AppNavbar.tsx +++ b/apps/ui/src/pages/DasboardLayout/components/AppNavbar.tsx @@ -1,101 +1,41 @@ -import { - Box, - Group, - Input, - NavLink, - Stack, - Text, - type NavLinkProps, -} from "@mantine/core"; -import { useLocation } from "@tanstack/react-router"; -import { Search, Settings, SquareGanttChart, Users } from "lucide-react"; -import { useMemo, useState } from "react"; +import { AppShell, Input, ScrollArea } from "@mantine/core"; +import { Search } from "lucide-react"; +import { useState } from "react"; +import { sidebarItems } from "@/configs/configureSidebar"; +import AppNavbarFooter from "./AppNavbarFooter"; +import AppNavbarLinkGroup from "./AppNavbarLinkGroup"; -export type AppNavbarLink = { - id: string; - url?: string; - navLinkProps?: NavLinkProps; - children?: AppNavbarLink[]; - keywords?: string[]; -}; +interface AppNavbarProps { + compact: boolean; + onToggleCompact: () => void; +} -export default function AppNavbar() { +export default function AppNavbar({ compact, onToggleCompact }: AppNavbarProps) { const [searchValue, setSearchValue] = useState(""); - const location = useLocation(); - - const navlinks: AppNavbarLink[] = [ - { - id: "overview", - url: "/dashboard", - navLinkProps: { - label: ( - - - Overview - - ), - }, - }, - { - id: "system", - navLinkProps: { - label: ( - - - System - - ), - }, - children: [ - { - id: "users", - url: "/dashboard/users", - navLinkProps: { - label: ( - - - User management - - ), - }, - keywords: ["users"], - }, - ], - keywords: ["system", "setting"], - }, - ]; - - const renderNavigationLinks = useMemo( - () => (links: AppNavbarLink[]) => { - return links.map((link) => ( - - {link.children && - link.children.length > 0 && - renderNavigationLinks(link.children)} - - )); - }, - [searchValue, navlinks, location] - ); return ( - - {/* Search input */} - } - size={"xs"} - value={searchValue} - onChange={(e) => setSearchValue(e.currentTarget.value)} - placeholder="Search..." - /> + <> + {!compact && ( + + } + size={"xs"} + value={searchValue} + onChange={(e) => setSearchValue(e.currentTarget.value)} + placeholder="Search..." + /> + + )} + + + + - {/* Navigation links */} - {renderNavigationLinks(navlinks)} - + + ); } diff --git a/apps/ui/src/pages/DasboardLayout/components/AppNavbarFooter.tsx b/apps/ui/src/pages/DasboardLayout/components/AppNavbarFooter.tsx new file mode 100644 index 0000000..ca0a9b3 --- /dev/null +++ b/apps/ui/src/pages/DasboardLayout/components/AppNavbarFooter.tsx @@ -0,0 +1,80 @@ +import { ActionIcon, AppShell, Divider, Menu, useMantineColorScheme } from "@mantine/core"; +import { useAuthStore } from "@/store/useAuthStore"; +import { useQueryClient } from "@tanstack/react-query"; +import { useRouter } from "@tanstack/react-router"; +import { LogOut, Moon, Settings, Sun, Cog, PanelLeftClose, PanelLeftOpen } from "lucide-react"; + +interface AppNavbarFooterProps { + compact: boolean; + onToggleCompact: () => void; +} + +export default function AppNavbarFooter({ compact, onToggleCompact }: AppNavbarFooterProps) { + const { colorScheme, toggleColorScheme } = useMantineColorScheme(); + const setAuthenticated = useAuthStore((s) => s.setAuthenticated); + const queryClient = useQueryClient(); + const router = useRouter(); + + const handleLogout = () => { + localStorage.removeItem("token"); + setAuthenticated(false); + queryClient.clear(); + router.navigate({ to: "/" }); + }; + + const ThemeIcon = colorScheme === "dark" ? Sun : Moon; + + return ( + + + + + + + + + + + }> + Settings + + + : } + onClick={onToggleCompact} + > + {compact ? "Expand sidebar" : "Compact mode"} + + + } + onClick={() => toggleColorScheme()} + > + {colorScheme === "dark" ? "Light mode" : "Dark mode"} + + + + + } + onClick={handleLogout} + > + Logout + + + + + ); +} diff --git a/apps/ui/src/pages/DasboardLayout/components/AppNavbarLinkGroup.tsx b/apps/ui/src/pages/DasboardLayout/components/AppNavbarLinkGroup.tsx new file mode 100644 index 0000000..5bf3333 --- /dev/null +++ b/apps/ui/src/pages/DasboardLayout/components/AppNavbarLinkGroup.tsx @@ -0,0 +1,74 @@ +import { NavLink, Tooltip } from "@mantine/core"; +import type { AppNavbarLink } from "@/types/sidebar"; +import { useLocation } from "@tanstack/react-router"; +import { useMemo } from "react"; + +interface AppNavbarLinkGroupProps { + links: AppNavbarLink[]; + searchValue: string; + compact: boolean; +} + +function matchLink(link: AppNavbarLink, searchValue: string): boolean { + const q = searchValue.toLowerCase(); + const labelMatch = link.label.toLowerCase().includes(q); + const keywordMatch = + link.keywords?.some((k) => k.toLowerCase().includes(q)) ?? false; + const childMatch = + link.children?.some((c) => matchLink(c, searchValue)) ?? false; + return labelMatch || keywordMatch || childMatch; +} + +export default function AppNavbarLinkGroup({ links, searchValue, compact }: AppNavbarLinkGroupProps) { + const location = useLocation(); + + const items = useMemo(() => { + if (!searchValue.trim()) return links; + return links.filter((link) => matchLink(link, searchValue)); + }, [links, searchValue]); + + return items.map((link) => ); +} + +interface AppNavbarLinkItemProps { + link: AppNavbarLink; + location: ReturnType; + searchValue: string; + compact: boolean; +} + +function AppNavbarLinkItem({ link, location, searchValue, compact }: AppNavbarLinkItemProps) { + const Icon = link.icon; + const isActive = link.url ? location.pathname === link.url : false; + const hasChildren = link.children && link.children.length > 0; + + const navLink = ( + : undefined} + active={isActive} + childrenOffset={compact ? 0 : undefined} + {...link.navLinkProps} + style={compact ? { display: "flex", justifyContent: "center", ...(link.navLinkProps?.style || {}) } : link.navLinkProps?.style} + > + {hasChildren && !compact && ( + + )} + + ); + + if (compact) { + return ( + + {navLink} + + ); + } + + return navLink; +} diff --git a/apps/ui/src/pages/DasboardLayout/index.tsx b/apps/ui/src/pages/DasboardLayout/index.tsx index 7cf1a04..72444eb 100644 --- a/apps/ui/src/pages/DasboardLayout/index.tsx +++ b/apps/ui/src/pages/DasboardLayout/index.tsx @@ -1,16 +1,19 @@ import { AppShell } from "@mantine/core"; import { Outlet } from "@tanstack/react-router"; +import { useState } from "react"; import AppHeader from "./components/AppHeader"; import AppNavbar from "./components/AppNavbar"; export default function DashboardLayout() { + const [compact, setCompact] = useState(false); + return ( @@ -18,10 +21,13 @@ export default function DashboardLayout() { - + setCompact((c) => !c)} + /> - + diff --git a/apps/ui/src/types/sidebar.ts b/apps/ui/src/types/sidebar.ts new file mode 100644 index 0000000..a685ad0 --- /dev/null +++ b/apps/ui/src/types/sidebar.ts @@ -0,0 +1,12 @@ +import type { NavLinkProps } from "@mantine/core"; +import type { LucideIcon } from "lucide-react"; + +export type AppNavbarLink = { + id: string; + url?: string; + label: string; + icon: LucideIcon; + navLinkProps?: Omit; + children?: AppNavbarLink[]; + keywords?: string[]; +};