fix: remove unused routes and errors

This commit is contained in:
mengstabketemaw
2026-06-16 15:56:32 +03:00
parent d749c67959
commit e674565845
7 changed files with 218 additions and 222 deletions

View File

@@ -0,0 +1,13 @@
import { Navigate, useLocation } from 'react-router-dom';
import { useAppSelector } from '../store/hooks';
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAppSelector((s) => s.auth.isAuthenticated);
const location = useLocation();
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}

View File

@@ -1,90 +0,0 @@
import { Box, Button, Group, Stack, Text, Title } from '@mantine/core';
import { IconFilePlus, IconCategory, IconAnchor } from '@tabler/icons-react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
export function DashboardHero({ name }: { name?: string }) {
const { t } = useTranslation();
const navigate = useNavigate();
return (
<Box
p={{ base: 'lg', sm: 'xl' }}
style={{
position: 'relative',
overflow: 'hidden',
borderRadius: 'var(--mantine-radius-lg)',
background: 'linear-gradient(135deg, #2453a2 0%, #3160b7 45%, #1fc29d 130%)',
color: 'white',
boxShadow: 'var(--mantine-shadow-md)',
}}
>
{/* decorative wash */}
<IconAnchor
size={220}
stroke={1}
aria-hidden
style={{
position: 'absolute',
right: -30,
bottom: -50,
opacity: 0.12,
}}
/>
<div
aria-hidden
style={{
position: 'absolute',
top: -60,
right: 120,
width: 200,
height: 200,
borderRadius: '50%',
background: 'rgba(255,255,255,0.08)',
}}
/>
<Stack gap="md" style={{ position: 'relative', maxWidth: 620 }}>
<Stack gap={4}>
<Text fz="sm" fw={600} style={{ opacity: 0.85 }}>
{t('dashboard.hero.greeting')}
{name ? ',' : ''}
</Text>
{name && (
<Title order={2} c="white">
{name}
</Title>
)}
<Text fz={{ base: 'sm', sm: 'md' }} style={{ opacity: 0.92 }} maw={520}>
{t('dashboard.hero.subtitle')}
</Text>
</Stack>
<Group gap="sm">
<Button
variant="white"
c="emaPrimary.7"
leftSection={<IconFilePlus size={18} />}
onClick={() => navigate('/apply')}
>
{t('dashboard.newApplication')}
</Button>
<Button
leftSection={<IconCategory size={18} />}
onClick={() => navigate('/services')}
styles={{
root: {
background: 'rgba(255,255,255,0.16)',
color: 'white',
border: '1px solid rgba(255,255,255,0.35)',
backdropFilter: 'blur(4px)',
},
}}
>
{t('dashboard.hero.browse')}
</Button>
</Group>
</Stack>
</Box>
);
}

View File

@@ -18,6 +18,8 @@ export const am: Translations = {
seafarerRegistry: 'የመርከበኞች ምዝገባ',
profile: 'መገለጫ',
support: 'እገዛና ድጋፍ',
collapseSidebar: 'ሰብስብ',
expandSidebar: 'ዘርጋ',
},
common: {

View File

@@ -16,6 +16,8 @@ export const en = {
seafarerRegistry: 'Seafarer Registry',
profile: 'Profile',
support: 'Help & Support',
collapseSidebar: 'Collapse',
expandSidebar: 'Expand sidebar',
},
common: {

View File

@@ -27,7 +27,10 @@ import {
} from '@tabler/icons-react';
import type { Icon } from '@tabler/icons-react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { notify } from '@ema-platform/ui';
import { logout } from '@ema-platform/auth';
import { LanguageSwitcher } from '../components/LanguageSwitcher';
import { ColorSchemeToggle } from '../components/ColorSchemeToggle';
import { BrandMark } from '@ema-platform/auth';
@@ -40,47 +43,44 @@ interface NavItem {
soon?: boolean;
}
const NAV_ITEMS: NavItem[] = [
{ to: '/dashboard', label: 'Dashboard', icon: IconLayoutDashboard },
{ to: '/seafarer-registry', label: 'Seafarer Registry', icon: IconList },
{ to: '/profile', label: 'Profile', icon: IconUser },
{ to: '/support', label: 'Support', icon: IconLifebuoy },
const NAV_ITEMS: (NavItem & { i18nKey: string })[] = [
{ to: '/dashboard', label: 'Dashboard', i18nKey: 'nav.dashboard', icon: IconLayoutDashboard },
{ to: '/seafarer-registry', label: 'Seafarer Registry', i18nKey: 'nav.seafarerRegistry', icon: IconList },
{ to: '/profile', label: 'Profile', i18nKey: 'nav.profile', icon: IconUser },
{ to: '/support', label: 'Support', i18nKey: 'nav.support', icon: IconLifebuoy },
];
const PAGE_META: Record<string, { title: string; subtitle: string }> = {
const PAGE_META: Record<string, { i18nKey: string; subtitleKey: string }> = {
'/dashboard': {
title: 'Dashboard',
subtitle: new Date().toLocaleDateString('en-GB', {
weekday: 'long',
day: '2-digit',
month: 'long',
year: 'numeric',
}),
i18nKey: 'nav.dashboard',
subtitleKey: 'dashboard.title',
},
'/seafarer-registry': {
title: 'Seafarer Registry',
subtitle: 'Manage all registered seafarers',
i18nKey: 'nav.seafarerRegistry',
subtitleKey: '',
},
'/profile': {
title: 'Profile',
subtitle: 'Manage your account and preferences',
i18nKey: 'nav.profile',
subtitleKey: '',
},
};
export function PortalLayout() {
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const dispatch = useDispatch();
const [navOpened, { toggle: toggleNav, close: closeNav }] = useDisclosure();
const [sidebarCollapsed, { toggle: toggleSidebar }] = useDisclosure(false);
// Breadcrumb trail: always rooted at "Home", then any matched page segments.
const segments = location.pathname.split('/').filter(Boolean);
const crumbs = [
{ label: 'Home', path: '/dashboard' },
{ label: t('nav.dashboard'), path: '/dashboard' },
...segments
.map((_, i) => '/' + segments.slice(0, i + 1).join('/'))
.filter((path) => PAGE_META[path] && path !== '/dashboard')
.map((path) => ({ label: PAGE_META[path].title, path })),
.map((path) => ({ label: t(PAGE_META[path].i18nKey), path })),
];
const go = (item: NavItem) => {
@@ -95,6 +95,7 @@ export function PortalLayout() {
};
const handleLogout = () => {
dispatch(logout());
navigate('/login');
};
@@ -381,7 +382,7 @@ export function PortalLayout() {
color: 'var(--mantine-color-primary-7)',
}}
>
EMA Portal
{t('app.name')}
</Text>
<Text
size="xs"
@@ -393,7 +394,7 @@ export function PortalLayout() {
lineHeight: 1.3,
}}
>
Ethiopian Maritime Authority
{t('app.authority')}
</Text>
</div>
)}
@@ -407,8 +408,9 @@ export function PortalLayout() {
const active = activeNavItem(item);
if (sidebarCollapsed) {
const label = t(item.i18nKey);
return (
<Tooltip key={item.label} label={item.label} position="right" withArrow>
<Tooltip key={item.label} label={label} position="right" withArrow>
<UnstyledButton
onClick={() => go(item)}
style={{
@@ -428,11 +430,12 @@ export function PortalLayout() {
);
}
const label = t(item.i18nKey);
return (
<NavLink
key={item.label}
active={active}
label={item.label}
label={label}
leftSection={<ItemIcon size={19} stroke={1.6} />}
onClick={() => go(item)}
variant="light"
@@ -452,7 +455,7 @@ export function PortalLayout() {
}}
>
<Tooltip
label={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
label={sidebarCollapsed ? t('nav.expandSidebar', 'Expand sidebar') : t('nav.collapseSidebar', 'Collapse sidebar')}
position="right"
withArrow
disabled={!sidebarCollapsed}
@@ -490,7 +493,7 @@ export function PortalLayout() {
) : (
<>
<IconChevronLeft size={18} stroke={1.6} />
<span>Collapse</span>
<span>{t('nav.collapseSidebar', 'Collapse')}</span>
</>
)}
</UnstyledButton>

View File

@@ -3,6 +3,7 @@ import type { ReactNode } from 'react';
import { I18nextProvider } from 'react-i18next';
import { i18n } from './i18n/config';
import { PortalLayout } from './layouts/PortalLayout';
import { ProtectedRoute } from './components/ProtectedRoute';
// Auth (standalone pages, no portal chrome)
import { LoginPage, SignupPage, OTPVerificationPage, ForgotPasswordPage } from '@ema-platform/auth';
@@ -28,19 +29,28 @@ function IsolatedIam({ children }: { children: ReactNode }) {
}
export const router = createBrowserRouter([
// Standalone auth pages
// Public auth pages
{ path: '/login', element: <LoginPage /> },
{ path: '/signup', element: <SignupPage /> },
{ path: '/otp-verify', element: <OTPVerificationPage /> },
{ path: '/forgot-password', element: <ForgotPasswordPage /> },
// Portal — wrapped in the portal's own i18n instance so it is isolated from
// the global i18next singleton that `@tria-plc/iamui-common` initializes.
// Protected auth pages
{
element: <ProtectedRoute><OTPVerificationPage /></ProtectedRoute>,
path: '/otp-verify',
},
{
element: <ProtectedRoute><ForgotPasswordPage /></ProtectedRoute>,
path: '/forgot-password',
},
// Portal — protected
{
element: (
<I18nextProvider i18n={i18n}>
<PortalLayout />
</I18nextProvider>
<ProtectedRoute>
<I18nextProvider i18n={i18n}>
<PortalLayout />
</I18nextProvider>
</ProtectedRoute>
),
children: [
{ path: '/', element: <Navigate to="/dashboard" replace /> },
@@ -53,12 +63,14 @@ export const router = createBrowserRouter([
],
},
// IAM admin user management (isolated providers)
// IAM admin user management (isolated providers) — protected
{
element: (
<IsolatedIam>
<UserManagementLayout />
</IsolatedIam>
<ProtectedRoute>
<IsolatedIam>
<UserManagementLayout />
</IsolatedIam>
</ProtectedRoute>
),
children: [{ path: '/users', element: <UserManagementPage /> }],
},

View File

@@ -7,11 +7,14 @@ import {
Stack,
Text,
Title,
UnstyledButton,
rem,
useComputedColorScheme,
useMantineColorScheme,
useMantineTheme,
type BoxProps,
} from '@mantine/core';
import { IconCheck } from '@tabler/icons-react';
import { IconCheck, IconMoon, IconSun } from '@tabler/icons-react';
import { useAuthConfig } from '../AuthConfig';
export function BrandMark({ size = 44, ...boxProps }: BoxProps & { size?: number }) {
@@ -29,6 +32,34 @@ export function BrandMark({ size = 44, ...boxProps }: BoxProps & { size?: number
);
}
function ThemeToggle() {
const { setColorScheme } = useMantineColorScheme();
const computed = useComputedColorScheme('light');
const isDark = computed === 'dark';
return (
<UnstyledButton
onClick={() => setColorScheme(isDark ? 'light' : 'dark')}
aria-label="Toggle theme"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: rem(36),
height: rem(36),
borderRadius: rem(10),
border: '1px solid var(--mantine-color-default-border)',
background: 'var(--mantine-color-body)',
color: 'var(--mantine-color-dimmed)',
cursor: 'pointer',
transition: 'all 150ms ease',
}}
>
{isDark ? <IconSun size={18} /> : <IconMoon size={18} />}
</UnstyledButton>
);
}
const FEATURES = [
'Submit applications online, 24/7',
'Real-time status tracking & alerts',
@@ -49,114 +80,137 @@ export function AuthShell({
const theme = useMantineTheme();
const { logoUrl } = useAuthConfig();
const heroGradient = theme.other.heroGradient as string;
const computed = useComputedColorScheme('light');
const isDark = computed === 'dark';
return (
<Flex
<Box
mih="100vh"
align="center"
justify="center"
style={{ background: '#f7f8fa' }}
style={{
background: 'var(--mantine-color-body)',
position: 'relative',
}}
p="md"
>
{/* Theme toggle — top-right corner */}
<Box
mx="auto"
maw={1280}
w="100%"
bg="white"
p={12}
style={{
borderRadius: rem(16),
boxShadow: '0 20px 60px rgba(0,0,0,0.15)',
overflow: 'hidden',
position: 'absolute',
top: rem(16),
right: rem(16),
zIndex: 10,
}}
>
<Flex direction={{ base: 'column', lg: 'row' }}>
<Box
w={{ base: '100%', lg: '50%' }}
px={48}
py={48}
style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
>
{children}
</Box>
<ThemeToggle />
</Box>
<Box
visibleFrom="lg"
w="50%"
p="lg"
pos="relative"
style={{ background: heroGradient, overflow: 'hidden' }}
>
<Flex
mih="100vh"
align="center"
justify="center"
>
<Box
mx="auto"
maw={1280}
w="100%"
p={12}
style={{
borderRadius: rem(16),
background: 'var(--mantine-color-body)',
boxShadow: isDark
? '0 20px 60px rgba(0,0,0,0.5)'
: '0 20px 60px rgba(0,0,0,0.15)',
overflow: 'hidden',
}}
>
<Flex direction={{ base: 'column', lg: 'row' }}>
<Box
pos="absolute"
top={-80}
right={-40}
w={240}
h={240}
style={{ borderRadius: '50%', background: 'rgba(255,255,255,0.10)' }}
/>
<Box
pos="absolute"
bottom={-80}
left={-60}
w={220}
h={220}
style={{ borderRadius: '50%', background: 'rgba(255,255,255,0.08)' }}
/>
<Stack
gap="lg"
pos="relative"
style={{ zIndex: 1 }}
h="100%"
justify="center"
w={{ base: '100%', lg: '50%' }}
px={48}
py={48}
style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
>
<Center>
<Box
component="img"
src={logoUrl}
alt="EMA Portal"
style={{ display: 'block', maxWidth: '60%', height: 'auto' }}
/>
</Center>
{children}
</Box>
<Stack gap={6}>
<Title order={2} c="white" fz={rem(28)} lh={1.2} fw={700}>
{brandTitle}
</Title>
<Text fz="sm" lh={1.6} style={{ color: 'rgba(255,255,255,0.85)' }}>
{brandSubtitle}
<Box
visibleFrom="lg"
w="50%"
p="lg"
pos="relative"
style={{ background: heroGradient, overflow: 'hidden' }}
>
<Box
pos="absolute"
top={-80}
right={-40}
w={240}
h={240}
style={{ borderRadius: '50%', background: 'rgba(255,255,255,0.10)' }}
/>
<Box
pos="absolute"
bottom={-80}
left={-60}
w={220}
h={220}
style={{ borderRadius: '50%', background: 'rgba(255,255,255,0.08)' }}
/>
<Stack
gap="lg"
pos="relative"
style={{ zIndex: 1 }}
h="100%"
justify="center"
>
<Center>
<Box
component="img"
src={logoUrl}
alt="EMA Portal"
style={{ display: 'block', maxWidth: '60%', height: 'auto' }}
/>
</Center>
<Stack gap={6}>
<Title order={2} c="white" fz={rem(28)} lh={1.2} fw={700}>
{brandTitle}
</Title>
<Text fz="sm" lh={1.6} style={{ color: 'rgba(255,255,255,0.85)' }}>
{brandSubtitle}
</Text>
</Stack>
<Stack gap="sm">
{FEATURES.map((feature) => (
<Group key={feature} gap="sm" wrap="nowrap">
<Center
w={22}
h={22}
style={{
borderRadius: '50%',
background: 'rgba(255,255,255,0.2)',
flexShrink: 0,
}}
>
<IconCheck size={12} color="white" stroke={2.4} />
</Center>
<Text c="white" fz="sm" fw={500}>
{feature}
</Text>
</Group>
))}
</Stack>
<Text fz="xs" style={{ color: 'rgba(255,255,255,0.7)' }}>
© 2026 Ethiopian Maritime Authority
</Text>
</Stack>
<Stack gap="sm">
{FEATURES.map((feature) => (
<Group key={feature} gap="sm" wrap="nowrap">
<Center
w={22}
h={22}
style={{
borderRadius: '50%',
background: 'rgba(255,255,255,0.2)',
flexShrink: 0,
}}
>
<IconCheck size={12} color="white" stroke={2.4} />
</Center>
<Text c="white" fz="sm" fw={500}>
{feature}
</Text>
</Group>
))}
</Stack>
<Text fz="xs" style={{ color: 'rgba(255,255,255,0.7)' }}>
© 2026 Ethiopian Maritime Authority
</Text>
</Stack>
</Box>
</Flex>
</Box>
</Flex>
</Box>
</Flex>
</Box>
</Flex>
</Box>
);
}