Files
whatshooked/web/src/components/DashboardLayout.tsx
Hein aaf6ad473a
Some checks failed
CI / Build (push) Failing after -30m38s
CI / Lint (push) Failing after -30m36s
CI / Test (1.22) (push) Failing after -30m39s
CI / Test (1.23) (push) Failing after -30m39s
feat(ui): add theme toggle to dashboard layout
- Implement theme switching between light and dark modes
- Use Mantine's color scheme for automatic detection
- Add tooltip for theme toggle button
- Update App component to use 'auto' color scheme
2026-03-05 01:25:03 +02:00

319 lines
9.6 KiB
TypeScript

import { Outlet, useNavigate, useLocation } from "react-router-dom";
import {
AppShell,
Burger,
Group,
Text,
NavLink,
Button,
Avatar,
Stack,
Image,
ActionIcon,
Tooltip,
useMantineColorScheme,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import {
IconDashboard,
IconUsers,
IconWebhook,
IconBrandWhatsapp,
IconSend,
IconBuildingStore,
IconTemplate,
IconCategory,
IconArrowsShuffle,
IconFileText,
IconDatabase,
IconLogout,
IconSun,
IconMoon,
} from "@tabler/icons-react";
import { useAuthStore } from "../stores/authStore";
export default function DashboardLayout() {
const { user, logout } = useAuthStore();
const { colorScheme, setColorScheme } = useMantineColorScheme();
const navigate = useNavigate();
const location = useLocation();
const [opened, { toggle }] = useDisclosure();
const handleLogout = () => {
logout();
navigate("/login");
};
const isActive = (path: string) => location.pathname === path;
const isAnyActive = (paths: string[]) =>
paths.some((path) => location.pathname === path);
const displayName =
user?.username?.trim() ||
user?.full_name?.trim() ||
user?.email?.trim() ||
"User";
const displayInitial = displayName[0]?.toUpperCase() || "U";
const logoSrc = `${import.meta.env.BASE_URL}logo.png`;
const swaggerIconSrc = `${import.meta.env.BASE_URL}swagger-icon.svg`;
const isDark = colorScheme === "dark";
const toggleTheme = () => {
if (colorScheme === "auto") {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
setColorScheme(prefersDark ? "light" : "dark");
return;
}
setColorScheme(isDark ? "light" : "dark");
};
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 280,
breakpoint: "sm",
collapsed: { mobile: !opened },
}}
padding="md"
>
<AppShell.Header>
<Group h="100%" px="md" justify="space-between">
<Group>
<Burger
opened={opened}
onClick={toggle}
hiddenFrom="sm"
size="sm"
/>
<Image src={logoSrc} alt="WhatsHooked logo" w={24} h={24} fit="contain" />
<Text size="xl" fw={700}>
WhatsHooked
</Text>
</Group>
<Group>
<Text size="sm" c="dimmed">
{displayName}
</Text>
<Avatar color="blue" radius="xl" size="sm">
{displayInitial}
</Avatar>
</Group>
</Group>
</AppShell.Header>
<AppShell.Navbar p="md">
<AppShell.Section grow>
<Stack gap="xs">
<NavLink
href="/dashboard"
label="Dashboard"
leftSection={<IconDashboard size={20} stroke={1.5} />}
active={isActive("/dashboard")}
onClick={(e) => {
e.preventDefault();
navigate("/dashboard");
if (opened) toggle();
}}
/>
<NavLink
href="/users"
label="Users"
leftSection={<IconUsers size={20} stroke={1.5} />}
active={isActive("/users")}
onClick={(e) => {
e.preventDefault();
navigate("/users");
if (opened) toggle();
}}
/>
<NavLink
href="/hooks"
label="Hooks"
leftSection={<IconWebhook size={20} stroke={1.5} />}
active={isActive("/hooks")}
onClick={(e) => {
e.preventDefault();
navigate("/hooks");
if (opened) toggle();
}}
/>
<NavLink
label="WhatsApp Accounts"
leftSection={<IconBrandWhatsapp size={20} stroke={1.5} />}
defaultOpened
active={isAnyActive([
"/accounts",
"/whatsapp-business",
"/business-templates",
"/catalogs",
"/flows",
])}
>
<NavLink
href="/accounts"
label="Account List"
active={isActive("/accounts")}
leftSection={
<IconBrandWhatsapp size={20} stroke={1.5} color="green" />
}
onClick={(e) => {
e.preventDefault();
navigate("/accounts");
if (opened) toggle();
}}
/>
<NavLink
label="Business Management"
leftSection={
<IconBrandWhatsapp size={20} stroke={1.5} color="orange" />
}
defaultOpened
active={isAnyActive([
"/whatsapp-business",
"/business-templates",
"/catalogs",
"/flows",
])}
>
<NavLink
href="/whatsapp-business"
label="Business Management Tools"
active={isActive("/whatsapp-business")}
leftSection={<IconBuildingStore size={16} stroke={1.5} />}
onClick={(e) => {
e.preventDefault();
navigate("/whatsapp-business");
if (opened) toggle();
}}
/>
<NavLink
href="/business-templates"
label="Templates"
leftSection={<IconTemplate size={16} stroke={1.5} />}
active={isActive("/business-templates")}
onClick={(e) => {
e.preventDefault();
navigate("/business-templates");
if (opened) toggle();
}}
/>
<NavLink
href="/catalogs"
label="Catalogs"
leftSection={<IconCategory size={16} stroke={1.5} />}
active={isActive("/catalogs")}
onClick={(e) => {
e.preventDefault();
navigate("/catalogs");
if (opened) toggle();
}}
/>
<NavLink
href="/flows"
label="Flows"
leftSection={<IconArrowsShuffle size={16} stroke={1.5} />}
active={isActive("/flows")}
onClick={(e) => {
e.preventDefault();
navigate("/flows");
if (opened) toggle();
}}
/>
</NavLink>
</NavLink>
<NavLink
href="/send-message"
label="Send Message"
leftSection={<IconSend size={20} stroke={1.5} color="green" />}
active={isActive("/send-message")}
onClick={(e) => {
e.preventDefault();
navigate("/send-message");
if (opened) toggle();
}}
/>
<NavLink
href="/event-logs"
label="Event Logs"
leftSection={
<IconFileText size={20} stroke={1.5} color="maroon" />
}
active={isActive("/event-logs")}
onClick={(e) => {
e.preventDefault();
navigate("/event-logs");
if (opened) toggle();
}}
/>
<NavLink
href="/message-cache"
label="Message Cache"
leftSection={
<IconDatabase size={20} stroke={1.5} color="indigo" />
}
active={isActive("/message-cache")}
onClick={(e) => {
e.preventDefault();
navigate("/message-cache");
if (opened) toggle();
}}
/>
<NavLink
href="/sw"
label="Swagger"
leftSection={
<Image src={swaggerIconSrc} alt="Swagger" w={18} h={18} fit="contain" />
}
active={isActive("/sw")}
onClick={(e) => {
e.preventDefault();
navigate("/sw");
if (opened) toggle();
}}
/>
</Stack>
</AppShell.Section>
<AppShell.Section>
<Stack gap="xs">
<Group justify="space-between" px="sm">
<Tooltip label={isDark ? "Switch to light theme" : "Switch to dark theme"}>
<ActionIcon
variant="light"
size="lg"
onClick={toggleTheme}
aria-label={isDark ? "Switch to light theme" : "Switch to dark theme"}
>
{isDark ? <IconSun size={18} /> : <IconMoon size={18} />}
</ActionIcon>
</Tooltip>
<div>
<Text size="sm" fw={500}>
{displayName}
</Text>
<Text size="xs" c="dimmed">
{user?.role || "user"}
</Text>
</div>
</Group>
<Button
leftSection={<IconLogout size={16} />}
variant="light"
color="red"
fullWidth
onClick={handleLogout}
>
Logout
</Button>
</Stack>
</AppShell.Section>
</AppShell.Navbar>
<AppShell.Main>
<Outlet />
</AppShell.Main>
</AppShell>
);
}