mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
Merge branch 'feature/pencil-design' of github.com:Tria-plc/emaui into feature/pencil-design
This commit is contained in:
@@ -1,43 +1,437 @@
|
||||
import type { ElementType } from 'react';
|
||||
import { Grid, Paper, Text, Title, Stack, Group } from '@mantine/core';
|
||||
import { IconBox, IconUsers, IconActivity } from '@tabler/icons-react';
|
||||
import { useState, type ElementType, type ReactNode } from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Text,
|
||||
Title,
|
||||
Stack,
|
||||
Group,
|
||||
SimpleGrid,
|
||||
Grid,
|
||||
ThemeIcon,
|
||||
Badge,
|
||||
Avatar,
|
||||
SegmentedControl,
|
||||
Button,
|
||||
Box,
|
||||
UnstyledButton,
|
||||
Anchor,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconUsers,
|
||||
IconUserCheck,
|
||||
IconUserPlus,
|
||||
IconClockHour4,
|
||||
IconShieldLock,
|
||||
IconMail,
|
||||
IconUpload,
|
||||
IconDownload,
|
||||
IconClipboardList,
|
||||
IconArrowUpRight,
|
||||
IconArrowDownRight,
|
||||
IconArrowRight,
|
||||
} from '@tabler/icons-react';
|
||||
import {
|
||||
ResponsiveContainer,
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Cell,
|
||||
PieChart,
|
||||
Pie,
|
||||
} from 'recharts';
|
||||
|
||||
interface StatCardProps {
|
||||
label: string;
|
||||
value: string;
|
||||
icon: ElementType;
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* sample data — swap for live API results */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const REGISTRATIONS = [
|
||||
{ month: 'Nov', value: 320 },
|
||||
{ month: 'Dec', value: 410 },
|
||||
{ month: 'Jan', value: 380 },
|
||||
{ month: 'Feb', value: 520 },
|
||||
{ month: 'Mar', value: 470 },
|
||||
{ month: 'Apr', value: 610 },
|
||||
{ month: 'May', value: 560 },
|
||||
{ month: 'Jun', value: 720 },
|
||||
];
|
||||
|
||||
const ROLES = [
|
||||
{ name: 'Admin', value: 18, color: '#1d4ed8' },
|
||||
{ name: 'Staff', value: 42, color: '#3b82f6' },
|
||||
{ name: 'Viewer', value: 30, color: '#60a5fa' },
|
||||
{ name: 'Guest', value: 10, color: '#93c5fd' },
|
||||
];
|
||||
|
||||
const KPIS = [
|
||||
{ label: 'Total Users', value: '9,431', icon: IconUsers, color: 'blue', trend: '+12.5%', up: true },
|
||||
{ label: 'Active Users', value: '7,218', icon: IconUserCheck, color: 'indigo', trend: '+8.2%', up: true },
|
||||
{ label: 'New This Month', value: '642', icon: IconUserPlus, color: 'cyan', trend: '+23.1%', up: true },
|
||||
{ label: 'Pending Approvals', value: '37', icon: IconClockHour4, color: 'orange', trend: '-4.0%', up: false },
|
||||
];
|
||||
|
||||
const QUICK_LINKS = [
|
||||
{ label: 'Add User', desc: 'Create a new account', icon: IconUserPlus, color: 'blue' },
|
||||
{ label: 'Roles & Permissions', desc: 'Manage access levels', icon: IconShieldLock, color: 'indigo' },
|
||||
{ label: 'Invite Members', desc: 'Send email invites', icon: IconMail, color: 'cyan' },
|
||||
{ label: 'Import Users', desc: 'Bulk CSV import', icon: IconUpload, color: 'violet' },
|
||||
{ label: 'Export Data', desc: 'Download reports', icon: IconDownload, color: 'blue' },
|
||||
{ label: 'Audit Log', desc: 'Review activity', icon: IconClipboardList, color: 'grape' },
|
||||
];
|
||||
|
||||
type UserStatus = 'Active' | 'Pending' | 'Invited';
|
||||
const STATUS_COLOR: Record<UserStatus, string> = {
|
||||
Active: 'teal',
|
||||
Pending: 'orange',
|
||||
Invited: 'blue',
|
||||
};
|
||||
|
||||
const RECENT_USERS: {
|
||||
name: string;
|
||||
email: string;
|
||||
initials: string;
|
||||
color: string;
|
||||
}
|
||||
status: UserStatus;
|
||||
}[] = [
|
||||
{ name: 'Sara Tesfaye', email: 'sara.t@ema.gov.et', initials: 'ST', color: 'blue', status: 'Active' },
|
||||
{ name: 'Daniel Bekele', email: 'daniel.b@ema.gov.et', initials: 'DB', color: 'indigo', status: 'Active' },
|
||||
{ name: 'Hanna Girma', email: 'hanna.g@ema.gov.et', initials: 'HG', color: 'violet', status: 'Pending' },
|
||||
{ name: 'Yonas Alemu', email: 'yonas.a@ema.gov.et', initials: 'YA', color: 'cyan', status: 'Active' },
|
||||
{ name: 'Meron Tadesse', email: 'meron.t@ema.gov.et', initials: 'MT', color: 'grape', status: 'Invited' },
|
||||
];
|
||||
|
||||
function StatCard({ label, value, icon: Icon, color }: StatCardProps) {
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* small building blocks */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function SectionCard({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<Paper p="md" shadow="sm" radius="md" withBorder>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={4}>
|
||||
<Text size="sm" c="dimmed">
|
||||
{label}
|
||||
</Text>
|
||||
<Title order={3}>{value}</Title>
|
||||
</Stack>
|
||||
<Icon size={32} color={color} />
|
||||
</Group>
|
||||
<Paper p="lg" radius="lg" withBorder h="100%">
|
||||
{children}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardPage() {
|
||||
function CardHeading({ title, subtitle }: { title: string; subtitle?: string }) {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Title order={2}>Dashboard</Title>
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, sm: 4 }}>
|
||||
<StatCard label="Total Items" value="—" icon={IconBox} color="#2563eb" />
|
||||
<Stack gap={2}>
|
||||
<Text fw={700} fz="lg">
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle && (
|
||||
<Text size="sm" c="dimmed">
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
label,
|
||||
value,
|
||||
icon: Icon,
|
||||
color,
|
||||
trend,
|
||||
up,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon: ElementType;
|
||||
color: string;
|
||||
trend: string;
|
||||
up: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Paper p="lg" radius="lg" withBorder>
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<ThemeIcon size={46} radius="md" variant="light" color={color}>
|
||||
<Icon size={22} />
|
||||
</ThemeIcon>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={up ? 'teal' : 'red'}
|
||||
radius="sm"
|
||||
leftSection={up ? <IconArrowUpRight size={12} /> : <IconArrowDownRight size={12} />}
|
||||
>
|
||||
{trend}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text fz={30} fw={800} mt="md" lh={1.1}>
|
||||
{value}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" mt={4}>
|
||||
{label}
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickLinkTile({
|
||||
label,
|
||||
desc,
|
||||
icon: Icon,
|
||||
color,
|
||||
}: {
|
||||
label: string;
|
||||
desc: string;
|
||||
icon: ElementType;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<UnstyledButton
|
||||
style={{
|
||||
border: '1px solid var(--mantine-color-gray-2)',
|
||||
borderRadius: 'var(--mantine-radius-md)',
|
||||
padding: 'var(--mantine-spacing-md)',
|
||||
transition: 'border-color 120ms ease, box-shadow 120ms ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--mantine-color-blue-3)';
|
||||
e.currentTarget.style.boxShadow = 'var(--mantine-shadow-sm)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--mantine-color-gray-2)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
>
|
||||
<ThemeIcon size={42} radius="md" variant="light" color={color}>
|
||||
<Icon size={20} />
|
||||
</ThemeIcon>
|
||||
<Text fw={600} size="sm" mt="sm">
|
||||
{label}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={2}>
|
||||
{desc}
|
||||
</Text>
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* page */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export function DashboardPage() {
|
||||
const [range, setRange] = useState('week');
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
{/* header */}
|
||||
<Group justify="space-between" align="flex-end" wrap="wrap">
|
||||
<Stack gap={4}>
|
||||
<Title order={2}>Overview</Title>
|
||||
<Text c="dimmed" size="sm">
|
||||
Welcome back — here's what's happening across your platform.
|
||||
</Text>
|
||||
</Stack>
|
||||
<Group gap="sm">
|
||||
<SegmentedControl
|
||||
value={range}
|
||||
onChange={setRange}
|
||||
radius="md"
|
||||
data={[
|
||||
{ label: 'Today', value: 'today' },
|
||||
{ label: 'This Week', value: 'week' },
|
||||
{ label: 'This Month', value: 'month' },
|
||||
]}
|
||||
/>
|
||||
<Button leftSection={<IconDownload size={16} />} radius="md">
|
||||
Export
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* KPIs */}
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="lg">
|
||||
{KPIS.map((k) => (
|
||||
<StatCard key={k.label} {...k} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
{/* charts */}
|
||||
<Grid gutter="lg" align="stretch">
|
||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||
<SectionCard>
|
||||
<Group justify="space-between" align="flex-start" mb="lg">
|
||||
<CardHeading
|
||||
title="User Registrations"
|
||||
subtitle="New sign-ups over the last 8 months"
|
||||
/>
|
||||
<Stack gap={0} align="flex-end">
|
||||
<Text fw={700} c="teal">
|
||||
+18.2%
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
vs previous period
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Box h={260}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={REGISTRATIONS} margin={{ top: 8, right: 4, left: -22, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="barBlue" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#60a5fa" />
|
||||
<stop offset="100%" stopColor="#2563eb" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" stroke="#eef2f7" />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 12, fill: '#8d9bb3' }}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ fill: '#f1f5fb' }}
|
||||
contentStyle={{
|
||||
borderRadius: 12,
|
||||
border: '1px solid #e4e9f2',
|
||||
fontSize: 12,
|
||||
boxShadow: '0 4px 20px rgba(15,23,42,0.08)',
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="value" radius={[6, 6, 0, 0]} maxBarSize={34}>
|
||||
{REGISTRATIONS.map((entry, i) => (
|
||||
<Cell
|
||||
key={entry.month}
|
||||
fill={i === REGISTRATIONS.length - 1 ? '#1d4ed8' : 'url(#barBlue)'}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
</SectionCard>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 4 }}>
|
||||
<StatCard label="Total Users" value="—" icon={IconUsers} color="#16a34a" />
|
||||
|
||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||
<SectionCard>
|
||||
<CardHeading title="Users by Role" subtitle="Distribution across access levels" />
|
||||
<Group mt="lg" justify="center" wrap="nowrap" gap="lg">
|
||||
<Box pos="relative" w={168} h={168} style={{ flexShrink: 0 }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={ROLES}
|
||||
dataKey="value"
|
||||
innerRadius={56}
|
||||
outerRadius={82}
|
||||
paddingAngle={2}
|
||||
stroke="none"
|
||||
startAngle={90}
|
||||
endAngle={-270}
|
||||
>
|
||||
{ROLES.map((r) => (
|
||||
<Cell key={r.name} fill={r.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
borderRadius: 12,
|
||||
border: '1px solid #e4e9f2',
|
||||
fontSize: 12,
|
||||
}}
|
||||
formatter={(value, name) => [`${value}%`, name]}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<Stack
|
||||
gap={0}
|
||||
align="center"
|
||||
justify="center"
|
||||
style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
|
||||
>
|
||||
<Text fw={800} fz={22}>
|
||||
9,431
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Total users
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Stack gap="md" style={{ flex: 1 }}>
|
||||
{ROLES.map((r) => (
|
||||
<Group key={r.name} justify="space-between" wrap="nowrap">
|
||||
<Group gap={8} wrap="nowrap">
|
||||
<Box w={10} h={10} style={{ borderRadius: 999, background: r.color }} />
|
||||
<Text size="sm" c="dimmed">
|
||||
{r.name}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" fw={700}>
|
||||
{r.value}%
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Group>
|
||||
</SectionCard>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 4 }}>
|
||||
<StatCard label="Activity" value="—" icon={IconActivity} color="#d97706" />
|
||||
</Grid>
|
||||
|
||||
{/* quick links + recent users */}
|
||||
<Grid gutter="lg" align="stretch">
|
||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||
<SectionCard>
|
||||
<CardHeading title="Quick Links" subtitle="Common administrative shortcuts" />
|
||||
<SimpleGrid cols={{ base: 2, sm: 3 }} spacing="md" mt="lg">
|
||||
{QUICK_LINKS.map((q) => (
|
||||
<QuickLinkTile key={q.label} {...q} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</SectionCard>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||
<SectionCard>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Text fw={700} fz="lg">
|
||||
Recent Users
|
||||
</Text>
|
||||
<Anchor size="sm" fw={600}>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
View all
|
||||
<IconArrowRight size={14} />
|
||||
</Group>
|
||||
</Anchor>
|
||||
</Group>
|
||||
<Stack gap={0}>
|
||||
{RECENT_USERS.map((u, i) => (
|
||||
<Group
|
||||
key={u.email}
|
||||
justify="space-between"
|
||||
wrap="nowrap"
|
||||
py="sm"
|
||||
style={{
|
||||
borderBottom:
|
||||
i < RECENT_USERS.length - 1
|
||||
? '1px solid var(--mantine-color-gray-2)'
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Avatar color={u.color} radius="xl" size={38}>
|
||||
{u.initials}
|
||||
</Avatar>
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" fw={600}>
|
||||
{u.name}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{u.email}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Badge variant="light" color={STATUS_COLOR[u.status]} radius="sm">
|
||||
{u.status}
|
||||
</Badge>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</SectionCard>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
|
||||
@@ -9,9 +9,10 @@ export function BackofficeLayout() {
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{ width: 240, breakpoint: 'sm', collapsed: { mobile: !opened } }}
|
||||
padding="md"
|
||||
header={{ height: 64 }}
|
||||
navbar={{ width: 264, breakpoint: 'sm', collapsed: { mobile: !opened } }}
|
||||
padding="lg"
|
||||
styles={{ main: { backgroundColor: '#f5f7fb' } }}
|
||||
>
|
||||
<AppShell.Header>
|
||||
<AppHeader onToggle={toggle} />
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import { Group, Text, ActionIcon, Burger } from '@mantine/core';
|
||||
import { IconLogout } from '@tabler/icons-react';
|
||||
import {
|
||||
Group,
|
||||
Text,
|
||||
ActionIcon,
|
||||
Burger,
|
||||
TextInput,
|
||||
Avatar,
|
||||
Menu,
|
||||
Indicator,
|
||||
UnstyledButton,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconLogout,
|
||||
IconBell,
|
||||
IconSearch,
|
||||
IconChevronDown,
|
||||
IconUserCircle,
|
||||
IconSettings,
|
||||
} from '@tabler/icons-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { logout } from '@ema-platform/auth';
|
||||
@@ -18,16 +35,54 @@ export function AppHeader({ onToggle }: AppHeaderProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Group h="100%" px="md" justify="space-between">
|
||||
<Group>
|
||||
<Group h="100%" px="lg" justify="space-between">
|
||||
<Group gap="sm">
|
||||
<Burger onClick={onToggle} size="sm" hiddenFrom="sm" />
|
||||
<Text fw={700} size="lg">
|
||||
EMA Backoffice
|
||||
</Text>
|
||||
<TextInput
|
||||
visibleFrom="sm"
|
||||
w={320}
|
||||
radius="md"
|
||||
placeholder="Search users, roles..."
|
||||
leftSection={<IconSearch size={16} />}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group gap="sm">
|
||||
<Indicator color="red" size={8} offset={6} withBorder>
|
||||
<ActionIcon variant="subtle" color="gray" radius="xl" size="lg" aria-label="Notifications">
|
||||
<IconBell size={19} />
|
||||
</ActionIcon>
|
||||
</Indicator>
|
||||
|
||||
<Menu position="bottom-end" width={200} shadow="md">
|
||||
<Menu.Target>
|
||||
<UnstyledButton>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Avatar color="blue" radius="xl" size={36}>
|
||||
AD
|
||||
</Avatar>
|
||||
<div style={{ lineHeight: 1.1 }}>
|
||||
<Text size="sm" fw={600} visibleFrom="sm">
|
||||
Amanuel D.
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" visibleFrom="sm">
|
||||
Administrator
|
||||
</Text>
|
||||
</div>
|
||||
<IconChevronDown size={14} />
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item leftSection={<IconUserCircle size={16} />}>Profile</Menu.Item>
|
||||
<Menu.Item leftSection={<IconSettings size={16} />}>Settings</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item color="red" leftSection={<IconLogout size={16} />} onClick={handleLogout}>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
<ActionIcon variant="subtle" onClick={handleLogout} title="Logout">
|
||||
<IconLogout size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { NavLink, Stack } from '@mantine/core';
|
||||
import { Stack, Group, Text, NavLink, ThemeIcon, Button, Box } from '@mantine/core';
|
||||
import {
|
||||
IconDashboard,
|
||||
IconBox,
|
||||
IconLayoutDashboard,
|
||||
IconUsers,
|
||||
IconClipboardList,
|
||||
IconShieldCheck,
|
||||
IconLifebuoy,
|
||||
} from '@tabler/icons-react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ label: 'Dashboard', icon: IconDashboard, path: '/dashboard' },
|
||||
{ label: 'Items', icon: IconBox, path: '/items' },
|
||||
{ label: 'Dashboard', icon: IconLayoutDashboard, path: '/dashboard' },
|
||||
{ label: 'User Management', icon: IconUsers, path: '/um' },
|
||||
{ label: 'Audit Log', icon: IconClipboardList, path: '/audit-log' },
|
||||
];
|
||||
|
||||
export function AppSidebar() {
|
||||
@@ -19,16 +17,77 @@ export function AppSidebar() {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<Stack p="xs" gap={4}>
|
||||
{NAV_ITEMS.map(({ label, icon: Icon, path }) => (
|
||||
<NavLink
|
||||
key={path}
|
||||
label={label}
|
||||
leftSection={<Icon size={18} />}
|
||||
active={pathname.startsWith(path)}
|
||||
onClick={() => navigate(path)}
|
||||
/>
|
||||
))}
|
||||
<Stack h="100%" justify="space-between" p="md" gap="lg">
|
||||
<Stack gap="lg">
|
||||
{/* brand */}
|
||||
<Group gap="sm" px={4} wrap="nowrap">
|
||||
<ThemeIcon size={42} radius="md" variant="gradient" gradient={{ from: 'blue', to: 'indigo', deg: 135 }}>
|
||||
<IconShieldCheck size={22} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={700} lh={1.1}>
|
||||
EMA Admin
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Control Center
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
{/* menu */}
|
||||
<div>
|
||||
<Text size="xs" fw={700} c="dimmed" px="xs" mb={8} 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>
|
||||
|
||||
{/* help card */}
|
||||
<Box
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
padding: 18,
|
||||
color: '#fff',
|
||||
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
w={36}
|
||||
h={36}
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
background: 'rgba(255,255,255,0.18)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
|
||||
import {
|
||||
createBrowserRouter,
|
||||
RouterProvider,
|
||||
Navigate,
|
||||
Outlet,
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
LoginPage,
|
||||
ForgotPasswordPage,
|
||||
@@ -10,6 +15,15 @@ import { ProtectedRoute } from './ProtectedRoute';
|
||||
import { DashboardPage } from '../features/dashboard/pages/DashboardPage';
|
||||
import UserManagementHostPage from '../features/user-management-host/UserManagementHostPage';
|
||||
|
||||
/** Wraps the authenticated area in the IAM provider stack. */
|
||||
function IamShell() {
|
||||
return (
|
||||
<IamProviders>
|
||||
<Outlet />
|
||||
</IamProviders>
|
||||
);
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
element: <AuthLayout />,
|
||||
@@ -23,14 +37,21 @@ const router = createBrowserRouter([
|
||||
element: <ProtectedRoute />,
|
||||
children: [
|
||||
{
|
||||
element: <BackofficeLayout />,
|
||||
element: <IamShell />,
|
||||
children: [
|
||||
{ path: '/dashboard', element: <DashboardPage /> },
|
||||
{
|
||||
element: <BackofficeLayout />,
|
||||
children: [
|
||||
{ index: true, element: <Navigate to="/dashboard" replace /> },
|
||||
{ path: 'dashboard', element: <DashboardPage /> },
|
||||
],
|
||||
},
|
||||
// User-management module runs full-screen (its own chrome), outside the shell.
|
||||
{ path: 'um/*', element: <UserManagementHostPage /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: '/um/*', element: <UserManagementHostPage /> },
|
||||
{ path: '/404', element: <div>Page not found</div> },
|
||||
{ path: '*', element: <Navigate to="/404" replace /> },
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user