Files
monitoring-app/src/frontend/components/DashboardLayout.tsx
2026-04-02 14:33:16 +08:00

249 lines
7.6 KiB
TypeScript

import { APP_CONFIGS } from '@/frontend/config/appMenus'
import {
ActionIcon,
AppShell,
Avatar,
Box,
Burger,
Button,
Group,
Menu,
NavLink,
Select,
Stack,
Text,
ThemeIcon
} from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { useMantineColorScheme, useComputedColorScheme } from '@mantine/core'
import { Link, useLocation, useMatches, useNavigate, useParams } from '@tanstack/react-router'
import {
TbApps,
TbArrowLeft,
TbChevronRight,
TbDashboard,
TbDeviceMobile,
TbLogout,
TbSettings,
TbUserCircle,
TbSun,
TbMoon,
TbUser,
TbHistory
} from 'react-icons/tb'
interface DashboardLayoutProps {
children: React.ReactNode
}
export function DashboardLayout({ children }: DashboardLayoutProps) {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true)
const { toggleColorScheme } = useMantineColorScheme()
const computedColorScheme = useComputedColorScheme('light', { getInitialValueInEffect: true })
const location = useLocation()
const navigate = useNavigate()
const { appId } = useParams({ strict: false }) as { appId?: string }
const matches = useMatches()
const currentPath = matches[matches.length - 1]?.pathname
const globalNav = [
{ label: 'Dashboard', icon: TbDashboard, to: '/dashboard' },
{ label: 'Applications', icon: TbApps, to: '/apps' },
{ label: 'Log Activity', icon: TbHistory, to: '/logs' },
{ label: 'Users', icon: TbUser, to: '/users' },
]
const activeApp = appId ? APP_CONFIGS[appId] : null
const navLinks = activeApp ? activeApp.menus : globalNav
return (
<AppShell
header={{ height: 70 }}
navbar={{
width: 260,
breakpoint: 'sm',
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}}
padding="xl"
styles={(theme) => ({
main: {
backgroundColor: computedColorScheme === 'dark' ? theme.colors.dark[9] : theme.colors.gray[0],
transition: 'background-color 0.2s ease',
},
})}
>
<AppShell.Header px="xl">
<Group h="100%" justify="space-between">
<Group>
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
<Group gap="xs">
<ThemeIcon
size={34}
radius="md"
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
>
<TbDeviceMobile size={18} />
</ThemeIcon>
<Text
size="lg"
fw={700}
className="gradient-text"
style={{ letterSpacing: '-0.5px' }}
>
Monitoring System
</Text>
</Group>
</Group>
<Group gap="md">
<ActionIcon
onClick={() => toggleColorScheme()}
variant="default"
size="lg"
aria-label="Toggle color scheme"
>
{computedColorScheme === 'dark' ? <TbSun size={18} /> : <TbMoon size={18} />}
</ActionIcon>
<Menu shadow="md" width={200} position="bottom-end">
<Menu.Target>
<Avatar
src={undefined}
alt="User"
color="brand-blue"
radius="xl"
style={{ cursor: 'pointer' }}
/>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Application</Menu.Label>
<Menu.Item leftSection={<TbUserCircle size={16} />}>Profile</Menu.Item>
<Menu.Item leftSection={<TbSettings size={16} />}>Settings</Menu.Item>
<Menu.Divider />
<Menu.Label>Danger Zone</Menu.Label>
<Menu.Item color="red" leftSection={<TbLogout size={16} />}>
Logout
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
</Group>
</AppShell.Header>
<AppShell.Navbar p="md">
<Stack gap="xs" mt="md">
{activeApp && (
<NavLink
label="Back to Dashboard"
leftSection={<TbArrowLeft size={20} />}
component={Link}
to="/dashboard"
styles={(theme) => ({
root: {
borderRadius: theme.radius.md,
opacity: 0.7,
'&:hover': { opacity: 1 },
},
})}
/>
)}
{
activeApp &&
<Select
label="Selected Application"
value={appId}
data={[
{ value: 'desa-plus', label: 'Desa+' },
{ value: 'e-commerce', label: 'E-Commerce' },
{ value: 'fitness-app', label: 'Fitness App' },
]}
onChange={(val) => val && navigate({ to: '/apps/$appId', params: { appId: val } })}
radius="md"
size="sm"
w={220}
mb={"md"}
variant="filled"
styles={(theme) => ({
input: { border: computedColorScheme === 'dark' ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(0,0,0,0.1)' }
})}
/>
}
{/* {activeApp && (
<Text size="xs" fw={700} c="dimmed" px="md" mb="xs" style={{ textTransform: 'uppercase' }}>
{activeApp.name} Context
</Text>
)} */}
{navLinks.map((link: any) => {
const isActive = currentPath === link.to
return (
<NavLink
key={link.label}
component={Link}
to={link.to}
activeOptions={{ exact: true }}
label={link.label}
leftSection={<link.icon size={20} />}
rightSection={<TbChevronRight size={14} />}
active={isActive}
variant="filled"
color="brand-blue"
className="sidebar-nav-item"
styles={(theme) => ({
root: {
borderRadius: theme.radius.md,
transition: 'all 0.2s ease',
'&[data-active]': {
background: 'var(--gradient-blue-purple)',
fontWeight: 600,
},
},
})}
/>
)
})}
</Stack>
<Box style={{ marginTop: 'auto' }}>
<Stack gap="xs">
<Box
p="md"
className="glass"
style={{ borderRadius: '12px', border: computedColorScheme === 'dark' ? '1px solid rgba(255,255,255,0.05)' : '1px solid rgba(0,0,0,0.05)' }}
>
<Text size="xs" c="dimmed" fw={600} mb="xs">SYSTEM STATUS</Text>
<Group gap="xs">
<Box style={{ width: 8, height: 8, borderRadius: '50%', background: '#10b981' }} />
<Text size="sm" fw={500}>All Systems Operational</Text>
</Group>
</Box>
<Button
variant="light"
color="red"
fullWidth
leftSection={<TbLogout size={16} />}
mt="md"
>
Log out
</Button>
</Stack>
</Box>
</AppShell.Navbar>
<AppShell.Main>
{children}
</AppShell.Main>
</AppShell>
)
}