mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
update usermanagement
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user