update usermanagement

This commit is contained in:
Mengisteab
2026-06-15 11:17:13 +00:00
parent a229259df6
commit 7e6e9eb1a7
7 changed files with 199 additions and 71 deletions

View File

@@ -17,7 +17,7 @@ export function BackofficeLayout() {
<AppShell.Header>
<AppHeader onToggle={toggle} />
</AppShell.Header>
<AppShell.Navbar>
<AppShell.Navbar p="md">
<AppSidebar />
</AppShell.Navbar>
<AppShell.Main>

View File

@@ -3,31 +3,61 @@ import {
Text,
ActionIcon,
Burger,
TextInput,
Avatar,
Menu,
Indicator,
UnstyledButton,
Breadcrumbs,
Anchor,
rem,
} from '@mantine/core';
import {
IconLogout,
IconBell,
IconSearch,
IconChevronDown,
IconUserCircle,
IconSettings,
IconHome,
} from '@tabler/icons-react';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { logout } from '@ema-platform/auth';
import { ColorSchemeToggle } from './ColorSchemeToggle';
import { LanguageSwitcher } from './LanguageSwitcher';
interface AppHeaderProps {
onToggle: () => void;
}
const ROUTE_LABELS: Record<string, string> = {
dashboard: 'Dashboard',
um: 'User Management',
};
function useBreadcrumbs() {
const location = useLocation();
const navigate = useNavigate();
const segments = location.pathname.split('/').filter(Boolean);
const crumbs = [
{ label: 'Home', path: '/dashboard', icon: <IconHome size={14} /> },
...segments
.filter((seg) => ROUTE_LABELS[seg])
.map((seg, i) => ({
label: ROUTE_LABELS[seg],
path: '/' + segments.slice(0, i + 1).join('/'),
icon: null,
})),
];
return { crumbs, navigate };
}
export function AppHeader({ onToggle }: AppHeaderProps) {
const dispatch = useDispatch();
const navigate = useNavigate();
const { crumbs } = useBreadcrumbs();
const handleLogout = () => {
dispatch(logout());
@@ -35,21 +65,48 @@ export function AppHeader({ onToggle }: AppHeaderProps) {
};
return (
<Group h="100%" px="lg" justify="space-between">
<Group gap="sm">
<Group h="100%" px="lg" justify="space-between" wrap="nowrap">
<Group gap="sm" wrap="nowrap">
<Burger onClick={onToggle} size="sm" hiddenFrom="sm" />
<TextInput
<Breadcrumbs
visibleFrom="sm"
w={320}
radius="md"
placeholder="Search users, roles..."
leftSection={<IconSearch size={16} />}
/>
styles={{
separator: { color: 'var(--mantine-color-dimmed)', fontSize: rem(12) },
}}
>
{crumbs.map((crumb, i) =>
i < crumbs.length - 1 ? (
<Anchor
key={crumb.path}
size="sm"
c="dimmed"
onClick={() => navigate(crumb.path)}
style={{ display: 'flex', alignItems: 'center', gap: 4 }}
>
{crumb.icon}
{crumb.label}
</Anchor>
) : (
<Text
key={crumb.path}
size="sm"
fw={600}
style={{ display: 'flex', alignItems: 'center', gap: 4 }}
>
{crumb.icon}
{crumb.label}
</Text>
),
)}
</Breadcrumbs>
</Group>
<Group gap="sm">
<Group gap="sm" wrap="nowrap">
<LanguageSwitcher />
<ColorSchemeToggle />
<Indicator color="red" size={8} offset={6} withBorder>
<ActionIcon variant="subtle" color="gray" radius="xl" size="lg" aria-label="Notifications">
<ActionIcon variant="default" size={38} radius="md" aria-label="Notifications">
<IconBell size={19} />
</ActionIcon>
</Indicator>

View File

@@ -1,11 +1,24 @@
import { Stack, Group, Text, NavLink, ThemeIcon, Button, Box } from '@mantine/core';
import {
AppShell,
Box,
Group,
NavLink,
ScrollArea,
Stack,
Text,
ThemeIcon,
ActionIcon,
rem,
} from '@mantine/core';
import {
IconLayoutDashboard,
IconUsers,
IconShieldCheck,
IconLifebuoy,
IconLogout,
} from '@tabler/icons-react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { logout } from '@ema-platform/auth';
const NAV_ITEMS = [
{ label: 'Dashboard', icon: IconLayoutDashboard, path: '/dashboard' },
@@ -14,12 +27,17 @@ const NAV_ITEMS = [
export function AppSidebar() {
const navigate = useNavigate();
const dispatch = useDispatch();
const { pathname } = useLocation();
const handleLogout = () => {
dispatch(logout());
navigate('/login');
};
return (
<Stack h="100%" justify="space-between" p="md" gap="lg">
<Stack gap="lg">
{/* brand */}
<>
<AppShell.Section pb="md">
<Group gap="sm" px={4} wrap="nowrap">
<ThemeIcon size={42} radius="md" variant="gradient" gradient={{ from: 'blue', to: 'indigo', deg: 135 }}>
<IconShieldCheck size={22} />
@@ -28,66 +46,64 @@ export function AppSidebar() {
<Text fw={700} lh={1.1}>
EMA Admin
</Text>
<Text size="xs" c="dimmed">
<Text size="xs" c="dimmed" lh={1.2}>
Control Center
</Text>
</div>
</Group>
</AppShell.Section>
{/* menu */}
<div>
<Text size="xs" fw={700} c="dimmed" px="xs" mb={8} style={{ letterSpacing: 1 }}>
<AppShell.Section grow component={ScrollArea}>
<Stack gap={4}>
<Text size="xs" fw={700} c="dimmed" px="xs" mb={4} style={{ letterSpacing: 1 }}>
MENU
</Text>
<Stack gap={4}>
{NAV_ITEMS.map(({ label, icon: Icon, path }) => (
<NavLink
key={path}
label={label}
leftSection={<Icon size={19} />}
active={pathname.startsWith(path)}
onClick={() => navigate(path)}
variant="light"
color="blue"
styles={{ root: { borderRadius: 10 }, label: { fontWeight: 500 } }}
/>
))}
</Stack>
</div>
</Stack>
{NAV_ITEMS.map(({ label, icon: Icon, path }) => (
<NavLink
key={path}
label={label}
leftSection={<Icon size={19} stroke={1.6} />}
active={pathname.startsWith(path)}
onClick={() => navigate(path)}
variant="light"
color="blue"
styles={{ root: { borderRadius: rem(10) }, label: { fontWeight: 500 } }}
/>
))}
</Stack>
</AppShell.Section>
{/* help card */}
<Box
style={{
borderRadius: 16,
padding: 18,
color: '#fff',
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
}}
>
<Box
w={36}
h={36}
<AppShell.Section>
<Group
gap="sm"
wrap="nowrap"
p="xs"
style={{
borderRadius: 10,
background: 'rgba(255,255,255,0.18)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: rem(12),
background: 'var(--mantine-color-default-hover)',
}}
>
<IconLifebuoy size={20} />
</Box>
<Text fw={700} mt="sm">
Need help?
</Text>
<Text size="xs" mt={4} style={{ opacity: 0.85 }}>
Browse guides or contact support.
</Text>
<Button fullWidth mt="md" size="xs" radius="md" variant="white" color="blue">
Open Help Center
</Button>
</Box>
</Stack>
<ThemeIcon size={38} radius="xl" variant="gradient" gradient={{ from: 'blue', to: 'indigo', deg: 135 }}>
<Text size="sm" fw={700} c="white">AD</Text>
</ThemeIcon>
<Box flex={1} style={{ minWidth: 0 }}>
<Text size="sm" fw={600} truncate>
Amanuel D.
</Text>
<Text size="xs" c="dimmed">
Administrator
</Text>
</Box>
<ActionIcon
variant="subtle"
color="gray"
aria-label="Log out"
onClick={handleLogout}
>
<IconLogout size={18} />
</ActionIcon>
</Group>
</AppShell.Section>
</>
);
}

View File

@@ -0,0 +1,20 @@
import { ActionIcon, useMantineColorScheme, useComputedColorScheme } from '@mantine/core';
import { IconSun, IconMoon } from '@tabler/icons-react';
export function ColorSchemeToggle() {
const { setColorScheme } = useMantineColorScheme();
const computed = useComputedColorScheme('light', { getInitialValueInEffect: true });
const isDark = computed === 'dark';
return (
<ActionIcon
variant="default"
size={38}
radius="md"
aria-label="Toggle color scheme"
onClick={() => setColorScheme(isDark ? 'light' : 'dark')}
>
{isDark ? <IconSun size={19} /> : <IconMoon size={19} />}
</ActionIcon>
);
}

View File

@@ -0,0 +1,34 @@
import { useState } from 'react';
import { Menu, ActionIcon } from '@mantine/core';
import { IconWorld, IconCheck } from '@tabler/icons-react';
const LANGUAGES = [
{ code: 'en', label: 'English' },
{ code: 'am', label: 'አማርኛ' },
];
export function LanguageSwitcher() {
const [current, setCurrent] = useState('en');
return (
<Menu shadow="md" width={160} position="bottom-end" withinPortal>
<Menu.Target>
<ActionIcon variant="default" size={38} radius="md" aria-label="Language">
<IconWorld size={19} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Language</Menu.Label>
{LANGUAGES.map((lng) => (
<Menu.Item
key={lng.code}
onClick={() => setCurrent(lng.code)}
rightSection={current === lng.code ? <IconCheck size={16} /> : undefined}
>
{lng.label}
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
);
}

View File

@@ -5,7 +5,7 @@ import type { ReactNode } from 'react';
export function MantineThemeProvider({ children }: { children: ReactNode }) {
return (
<MantineProvider theme={emaTheme}>
<MantineProvider theme={emaTheme} defaultColorScheme="light">
<Notifications position="top-right" />
{children}
</MantineProvider>