Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const queryClient = new QueryClient({

export default function App() {
return (
<MantineProvider theme={theme}>
<MantineProvider theme={theme} defaultColorScheme="dark">
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<Toaster position="bottom-right" />
Expand Down
26 changes: 26 additions & 0 deletions apps/ui/src/configs/configureSidebar.tsx
Original file line number Diff line number Diff line change
@@ -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"],
},
],
},
];
126 changes: 33 additions & 93 deletions apps/ui/src/pages/DasboardLayout/components/AppNavbar.tsx
Original file line number Diff line number Diff line change
@@ -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<string>("");
const location = useLocation();

const navlinks: AppNavbarLink[] = [
{
id: "overview",
url: "/dashboard",
navLinkProps: {
label: (
<Group gap={"xs"}>
<SquareGanttChart size={14} />
<Text size="xs">Overview</Text>
</Group>
),
},
},
{
id: "system",
navLinkProps: {
label: (
<Group gap={"xs"}>
<Settings size={14} />
<Text size="xs">System</Text>
</Group>
),
},
children: [
{
id: "users",
url: "/dashboard/users",
navLinkProps: {
label: (
<Group gap={"xs"}>
<Users size={12} />
<Text size="xs">User management</Text>
</Group>
),
},
keywords: ["users"],
},
],
keywords: ["system", "setting"],
},
];

const renderNavigationLinks = useMemo(
() => (links: AppNavbarLink[]) => {
return links.map((link) => (
<NavLink
key={link.id}
href={link.url}
{...link.navLinkProps}
active={location.pathname === link.url}
>
{link.children &&
link.children.length > 0 &&
renderNavigationLinks(link.children)}
</NavLink>
));
},
[searchValue, navlinks, location]
);

return (
<Stack p={"sm"}>
{/* Search input */}
<Input
leftSection={<Search size={14} />}
size={"xs"}
value={searchValue}
onChange={(e) => setSearchValue(e.currentTarget.value)}
placeholder="Search..."
/>
<>
{!compact && (
<AppShell.Section p={"sm"}>
<Input
leftSection={<Search size={14} />}
size={"xs"}
value={searchValue}
onChange={(e) => setSearchValue(e.currentTarget.value)}
placeholder="Search..."
/>
</AppShell.Section>
)}

<AppShell.Section grow component={ScrollArea}>
<AppNavbarLinkGroup
links={sidebarItems}
searchValue={searchValue}
compact={compact}
/>
</AppShell.Section>

{/* Navigation links */}
<Box c={"gray.6"}>{renderNavigationLinks(navlinks)}</Box>
</Stack>
<AppNavbarFooter compact={compact} onToggleCompact={onToggleCompact} />
</>
);
}
80 changes: 80 additions & 0 deletions apps/ui/src/pages/DasboardLayout/components/AppNavbarFooter.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<AppShell.Section>
<Divider />
<Menu position="right-start" withinPortal>
<Menu.Target>
<ActionIcon
variant="subtle"
color="gray"
size={compact ? "md" : "lg"}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
paddingBlock: compact ? 4 : 8,
}}
>
<Cog size={compact ? 18 : 20} />
</ActionIcon>
</Menu.Target>

<Menu.Dropdown>
<Menu.Item leftSection={<Settings size={14} />}>
Settings
</Menu.Item>

<Menu.Item
leftSection={compact ? <PanelLeftOpen size={14} /> : <PanelLeftClose size={14} />}
onClick={onToggleCompact}
>
{compact ? "Expand sidebar" : "Compact mode"}
</Menu.Item>

<Menu.Item
leftSection={<ThemeIcon size={14} />}
onClick={() => toggleColorScheme()}
>
{colorScheme === "dark" ? "Light mode" : "Dark mode"}
</Menu.Item>

<Menu.Divider />

<Menu.Item
color="red"
leftSection={<LogOut size={14} />}
onClick={handleLogout}
>
Logout
</Menu.Item>
</Menu.Dropdown>
</Menu>
</AppShell.Section>
);
}
74 changes: 74 additions & 0 deletions apps/ui/src/pages/DasboardLayout/components/AppNavbarLinkGroup.tsx
Original file line number Diff line number Diff line change
@@ -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) => <AppNavbarLinkItem key={link.id} link={link} location={location} searchValue={searchValue} compact={compact} />);
}

interface AppNavbarLinkItemProps {
link: AppNavbarLink;
location: ReturnType<typeof useLocation>;
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 = (
<NavLink
href={link.url}
label={compact ? undefined : link.label}
leftSection={Icon ? <Icon size={compact ? 18 : 14} /> : undefined}
active={isActive}
childrenOffset={compact ? 0 : undefined}
{...link.navLinkProps}
style={compact ? { display: "flex", justifyContent: "center", ...(link.navLinkProps?.style || {}) } : link.navLinkProps?.style}
>
{hasChildren && !compact && (
<AppNavbarLinkGroup
links={link.children!}
searchValue={searchValue}
compact={compact}
/>
)}
</NavLink>
);

if (compact) {
return (
<Tooltip label={link.label} position="right" withArrow>
{navLink}
</Tooltip>
);
}

return navLink;
}
14 changes: 10 additions & 4 deletions apps/ui/src/pages/DasboardLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
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 (
<AppShell
header={{ height: 40 }}
navbar={{
width: 200,
width: compact ? 60 : 200,
breakpoint: "sm",
// collapsed: { mobile: !opened },
collapsed: { mobile: true },
}}
>
<AppShell.Header>
<AppHeader />
</AppShell.Header>

<AppShell.Navbar>
<AppNavbar />
<AppNavbar
compact={compact}
onToggleCompact={() => setCompact((c) => !c)}
/>
</AppShell.Navbar>

<AppShell.Main bg={"dark.1"} p={0} mih={"100vh"}>
<AppShell.Main p={0} mih={"100vh"}>
<Outlet />
</AppShell.Main>
</AppShell>
Expand Down
12 changes: 12 additions & 0 deletions apps/ui/src/types/sidebar.ts
Original file line number Diff line number Diff line change
@@ -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<NavLinkProps, "label" | "children">;
children?: AppNavbarLink[];
keywords?: string[];
};
Loading