inspiren-sem-tool/resources/js/components/sidebar.tsx
brian-inspiren 221d3f8173
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
feat: sem codebase
2026-05-21 11:28:03 +08:00

179 lines
5.4 KiB
TypeScript

import React, { useState } from 'react';
import {
Navbar,
Stack,
UnstyledButton,
Text,
ThemeIcon,
createStyles,
Collapse,
rem,
Divider,
useMantineTheme,
} from '@mantine/core';
import {
IconHome,
IconUsers,
IconChevronDown,
IconFolder,
IconSettings,
IconUserCircle,
} from '@tabler/icons-react';
import { Inertia } from '@inertiajs/inertia';
const useStyles = createStyles((theme) => ({
link: {
display: 'flex',
alignItems: 'center',
width: '100%',
padding: `${theme.spacing.sm}px ${theme.spacing.md}px`,
borderRadius: theme.radius.sm,
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
fontWeight: 500,
transition: 'all 0.2s ease',
'&:hover': {
backgroundColor:
theme.colorScheme === 'dark'
? theme.colors.dark[6]
: theme.colors.gray[0],
cursor: 'pointer',
},
},
active: {
backgroundColor:
theme.colorScheme === 'dark'
? theme.colors.dark[5]
: theme.colors.gray[2],
fontWeight: 700,
},
nested: {
paddingLeft: rem(32),
},
toggleIcon: {
marginLeft: 'auto',
transition: 'transform 0.2s ease',
},
sectionTitle: {
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[7],
fontSize: theme.fontSizes.xs,
fontWeight: 700,
textTransform: 'uppercase',
padding: `${theme.spacing.xs}px ${theme.spacing.md}px`,
},
}));
type SidebarItem = {
label: string;
href?: string;
icon?: React.ReactNode;
children?: SidebarItem[];
section?: string;
};
const items: SidebarItem[] = [
{ label: 'Dashboard', href: route("dashboard"), icon: <IconHome size={16} />, section: 'Main' },
{
label: 'Clients',
icon: <IconFolder size={16} />,
section: 'Main',
href: route("google-ads.accounts.index"),
},
{
label: 'Users',
icon: <IconUsers size={16} />,
section: 'Management',
href: route("management.users.index"),
},
{
label: 'Roles',
icon: <IconUserCircle size={16} />,
section: 'Management',
href: route("management.roles.index"),
},
// { label: 'Settings', href: '/settings', icon: <IconSettings size={16} /> , section: 'Management' },
];
export default function Sidebar() {
const theme = useMantineTheme();
const { classes, cx } = useStyles();
const location = window.location.pathname;
const [opened, setOpened] = useState<Record<string, boolean>>({});
const toggle = (label: string) => {
setOpened((prev) => ({ ...prev, [label]: !prev[label] }));
};
const renderItem = (item: SidebarItem, depth = 0) => {
const hasChildren = !!item.children?.length;
const isActive =
item.href === location ||
(hasChildren && item.children?.some((c) => c.href === location));
return (
<div key={item.label}>
<UnstyledButton
className={cx(classes.link, { [classes.active]: isActive })}
style={{ paddingLeft: rem(16 + depth * 16) }}
onClick={(e) => {
e.preventDefault();
if (hasChildren) toggle(item.label);
else if (item.href) Inertia.visit(item.href);
}}
>
{item.icon && <ThemeIcon size="sm" variant="light" mr="sm">{item.icon}</ThemeIcon>}
<Text>{item.label}</Text>
{hasChildren && (
<IconChevronDown
size={16}
className={classes.toggleIcon}
style={{
transform: opened[item.label] ? 'rotate(180deg)' : 'rotate(0deg)',
}}
/>
)}
</UnstyledButton>
{hasChildren && (
<Collapse in={opened[item.label]}>
{item.children!.map((child) => renderItem(child, depth + 1))}
</Collapse>
)}
</div>
);
};
const sections = Array.from(new Set(items.map((i) => i.section).filter(Boolean)));
return (
<Navbar
width={{ base: 280 }}
p="xs"
style={{
backgroundColor:
theme.colorScheme === 'dark'
? theme.colors.dark[8]
: theme.colors.gray[0],
borderRight: `1px solid ${
theme.colorScheme === 'dark'
? theme.colors.dark[6]
: theme.colors.gray[2]
}`,
minHeight: '100%',
}}
>
<Stack spacing="xs">
{sections.map((section) => (
<div key={section}>
<Text className={classes.sectionTitle}>{section}</Text>
{items
.filter((item) => item.section === section)
.map((item) => renderItem(item))}
<Divider my="xs" />
</div>
))}
</Stack>
</Navbar>
);
}