Files
jenna-mcp/src/pages/scr/dashboard/dashboard_layout.tsx
amaliadwiy 80df579499 qc: nomer 2 dan 4
Deskripsi:
- button back
- breadcrumb
- active menu

No Issues
2026-01-13 15:10:08 +08:00

344 lines
10 KiB
TypeScript

import {
default as clientRoute,
default as clientRoutes,
} from "@/clientRoutes";
import apiFetch from "@/lib/apiFetch";
import {
ActionIcon,
AppShell,
Avatar,
Badge,
Button,
Card,
Divider,
Flex,
Group,
NavLink,
Paper,
ScrollArea,
Stack,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { useLocalStorage } from "@mantine/hooks";
import {
IconChevronLeft,
IconChevronRight,
IconDashboard,
IconFileCertificate,
IconKey,
IconLock,
IconMessageReport,
IconSettings,
IconUser,
IconUsersGroup,
} from "@tabler/icons-react";
import type { User } from "generated/prisma";
import type { JsonValue } from "generated/prisma/runtime/library";
import { useEffect, useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
function Logout() {
return (
<Group justify="center" mt="sm">
<Button
variant="gradient"
gradient={{ from: "red", to: "orange", deg: 60 }}
size="xs"
radius="md"
onClick={async () => {
await apiFetch.auth.logout.delete();
localStorage.removeItem("token");
window.location.href = "/login";
}}
style={{
boxShadow: "0 0 10px rgba(255,100,100,0.25)",
transition: "transform 0.2s ease",
}}
>
Log Out
</Button>
</Group>
);
}
export default function DashboardLayout() {
const [opened, setOpened] = useLocalStorage({
key: "nav_open",
defaultValue: true,
});
return (
<AppShell
padding="lg"
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: !opened },
}}
style={{
background: "radial-gradient(circle at top, #0a0a0a, #101010 70%)",
color: "#f8f9fa",
}}
>
<AppShell.Navbar
style={{
background: "linear-gradient(180deg, #141414, #1e1e1e)",
borderRight: "1px solid rgba(255,255,255,0.05)",
boxShadow: "2px 0 10px rgba(0,0,0,0.6)",
}}
>
<AppShell.Section>
<Group justify="flex-end" p="xs">
<Tooltip
label={opened ? "Collapse navigation" : "Expand navigation"}
withArrow
>
<ActionIcon
variant="light"
color="gray"
onClick={() => setOpened((v) => !v)}
radius="xl"
size="lg"
style={{
backgroundColor: "rgba(255,255,255,0.05)",
boxShadow: "0 0 6px hsla(167, 100%, 50%, 0.20), 0.20)",
}}
>
{opened ? <IconChevronLeft /> : <IconChevronRight />}
</ActionIcon>
</Tooltip>
</Group>
</AppShell.Section>
<AppShell.Section grow component={ScrollArea} flex={1}>
<NavigationDashboard />
</AppShell.Section>
<AppShell.Section>
<HostView />
</AppShell.Section>
</AppShell.Navbar>
<AppShell.Main>
<Stack gap="md">
<Paper
withBorder
radius="lg"
p="md"
style={{
background: "linear-gradient(145deg, #181818, #202020)",
border: "1px solid rgba(255,255,255,0.05)",
boxShadow: "0 0 15px rgba(0,255,200,0.08)",
}}
>
<Flex align="center" gap="md">
{!opened && (
<Tooltip label="Open navigation menu" withArrow>
<ActionIcon
variant="light"
color="gray"
onClick={() => setOpened(true)}
radius="xl"
>
<IconChevronRight />
</ActionIcon>
</Tooltip>
)}
<Title order={3} fw={600} c="gray.0">
Dashboard Control Panel
</Title>
</Flex>
</Paper>
<Outlet />
</Stack>
</AppShell.Main>
</AppShell>
);
}
function HostView() {
const [host, setHost] = useState<User | null>(null);
useEffect(() => {
async function fetchHost() {
const { data } = await apiFetch.api.user.find.get();
setHost(data?.user ?? null);
}
fetchHost();
}, []);
return (
<Card
radius="lg"
withBorder
shadow="sm"
p="md"
style={{
background: "linear-gradient(145deg, #181818, #212121)",
borderColor: "rgba(255,255,255,0.05)",
}}
>
{host ? (
<Stack gap="sm">
<Flex gap="md" align="center">
<Avatar size="md" radius="xl" color="teal" variant="filled">
{host.name?.[0]?.toUpperCase()}
</Avatar>
<Stack gap={2}>
<Text fw={600} c="gray.0">
{host.name}
</Text>
<Text size="sm" c="dimmed">
{host.roleId}
</Text>
</Stack>
</Flex>
<Divider my="xs" color="rgba(255,255,255,0.1)" />
<Logout />
</Stack>
) : (
<Flex align="center" justify="center" direction="column" p="md">
<IconUser size={28} color="gray" />
<Text size="sm" c="dimmed" mt={4}>
No user information available
</Text>
</Flex>
)}
</Card>
);
}
function NavigationDashboard() {
const navigate = useNavigate();
const location = useLocation();
const [permissions, setPermissions] = useState<JsonValue[]>([]);
useEffect(() => {
async function fetchPermissions() {
const { data } = await apiFetch.api.user.find.get();
if (Array.isArray(data?.permissions)) {
setPermissions(data.permissions);
} else {
setPermissions([]);
}
}
fetchPermissions();
}, []);
const isActive = (path: keyof typeof clientRoute) =>
location.pathname.startsWith(clientRoute[path]);
const navItems = [
{
key: "dashboard",
path: "/scr/dashboard/dashboard-home",
icon: <IconDashboard size={20} />,
label: "Dashboard Overview",
description: "Quick summary and insights",
},
{
key: "pengaduan",
path: "/scr/dashboard/pengaduan/list",
icon: <IconMessageReport size={20} />,
label: "Pengaduan Warga",
description: "Manage pengaduan warga",
},
{
key: "pelayanan",
path: "/scr/dashboard/pelayanan-surat/list-pelayanan",
icon: <IconFileCertificate size={20} />,
label: "Pelayanan Surat",
description: "Manage pelayanan surat",
},
{
key: "warga",
path: "/scr/dashboard/warga/list-warga",
icon: <IconUsersGroup size={20} />,
label: "Warga",
description: "Manage warga",
},
{
key: "setting",
path: "/scr/dashboard/setting/detail-setting",
icon: <IconSettings size={20} />,
label: "Setting",
description:
"Manage setting (category pengaduan dan pelayanan surat, desa, etc)",
},
{
key: "api_key",
path: "/scr/dashboard/apikey/apikey",
icon: <IconKey size={20} />,
label: "API Key Manager",
description: "Create and manage API keys",
},
{
key: "credential",
path: "/scr/dashboard/credential/credential",
icon: <IconLock size={20} />,
label: "Credentials",
description: "Manage service credentials",
},
];
return (
<Stack gap="xs" p="sm">
{navItems
.filter((item) => permissions.includes(item.key))
.map((item) => (
<NavLink
key={item.path}
active={isActive(item.path as keyof typeof clientRoute) ||
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")}
leftSection={item.icon}
label={
<Flex align="center" gap={6}>
<Text fw={500}>{item.label}</Text>
{(
isActive(item.path as keyof typeof clientRoute) ||
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")
)
&& (
<Badge
variant="light"
color="teal"
radius="sm"
size="xs"
style={{ textTransform: "none" }}
>
Active
</Badge>
)}
</Flex>
}
description={item.description}
onClick={() =>
navigate(clientRoutes[item.path as keyof typeof clientRoute])
}
style={{
backgroundColor: isActive(item.path as keyof typeof clientRoute) ||
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")
? "rgba(0,255,200,0.1)"
: "transparent",
borderRadius: "8px",
transition: "all 0.2s ease",
}}
styles={{
label: { color: "white" },
description: { color: "#aaa" },
section: { color: "teal" },
}}
/>
))}
</Stack>
);
}