diff --git a/apps/portal/src/app/components/ProtectedRoute.tsx b/apps/portal/src/app/components/ProtectedRoute.tsx new file mode 100644 index 000000000..1b5a4fd32 --- /dev/null +++ b/apps/portal/src/app/components/ProtectedRoute.tsx @@ -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 ; + } + + return <>{children}; +} diff --git a/apps/portal/src/app/features/dashboard/components/DashboardHero.tsx b/apps/portal/src/app/features/dashboard/components/DashboardHero.tsx deleted file mode 100644 index 9c71d1b0b..000000000 --- a/apps/portal/src/app/features/dashboard/components/DashboardHero.tsx +++ /dev/null @@ -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 ( - - {/* decorative wash */} - -
- - - - - {t('dashboard.hero.greeting')} - {name ? ',' : ''} - - {name && ( - - {name} - - )} - - {t('dashboard.hero.subtitle')} - - - - - - - - - - ); -} diff --git a/apps/portal/src/app/i18n/locales/am.ts b/apps/portal/src/app/i18n/locales/am.ts index 38cf0c923..5a7c1c0e3 100644 --- a/apps/portal/src/app/i18n/locales/am.ts +++ b/apps/portal/src/app/i18n/locales/am.ts @@ -18,6 +18,8 @@ export const am: Translations = { seafarerRegistry: 'የመርከበኞች ምዝገባ', profile: 'መገለጫ', support: 'እገዛና ድጋፍ', + collapseSidebar: 'ሰብስብ', + expandSidebar: 'ዘርጋ', }, common: { diff --git a/apps/portal/src/app/i18n/locales/en.ts b/apps/portal/src/app/i18n/locales/en.ts index cdc1589f5..7495259c1 100644 --- a/apps/portal/src/app/i18n/locales/en.ts +++ b/apps/portal/src/app/i18n/locales/en.ts @@ -16,6 +16,8 @@ export const en = { seafarerRegistry: 'Seafarer Registry', profile: 'Profile', support: 'Help & Support', + collapseSidebar: 'Collapse', + expandSidebar: 'Expand sidebar', }, common: { diff --git a/apps/portal/src/app/layouts/PortalLayout.tsx b/apps/portal/src/app/layouts/PortalLayout.tsx index 14e792a4a..7592cf28e 100644 --- a/apps/portal/src/app/layouts/PortalLayout.tsx +++ b/apps/portal/src/app/layouts/PortalLayout.tsx @@ -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 = { +const PAGE_META: Record = { '/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')} - Ethiopian Maritime Authority + {t('app.authority')}
)} @@ -407,8 +408,9 @@ export function PortalLayout() { const active = activeNavItem(item); if (sidebarCollapsed) { + const label = t(item.i18nKey); return ( - + go(item)} style={{ @@ -428,11 +430,12 @@ export function PortalLayout() { ); } + const label = t(item.i18nKey); return ( } onClick={() => go(item)} variant="light" @@ -452,7 +455,7 @@ export function PortalLayout() { }} > - Collapse + {t('nav.collapseSidebar', 'Collapse')} )} diff --git a/apps/portal/src/app/router.tsx b/apps/portal/src/app/router.tsx index 8b7638d9e..ffe76ae84 100644 --- a/apps/portal/src/app/router.tsx +++ b/apps/portal/src/app/router.tsx @@ -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: }, { path: '/signup', element: }, - { path: '/otp-verify', element: }, - { path: '/forgot-password', element: }, - // 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: , + path: '/otp-verify', + }, + { + element: , + path: '/forgot-password', + }, + + // Portal — protected { element: ( - - - + + + + + ), children: [ { path: '/', element: }, @@ -53,12 +63,14 @@ export const router = createBrowserRouter([ ], }, - // IAM admin user management (isolated providers) + // IAM admin user management (isolated providers) — protected { element: ( - - - + + + + + ), children: [{ path: '/users', element: }], }, diff --git a/libs/auth/src/lib/components/AuthShell.tsx b/libs/auth/src/lib/components/AuthShell.tsx index 07b7c7204..f3e75ba54 100644 --- a/libs/auth/src/lib/components/AuthShell.tsx +++ b/libs/auth/src/lib/components/AuthShell.tsx @@ -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 ( + 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 ? : } + + ); +} + 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 ( - + {/* Theme toggle — top-right corner */} - - - {children} - + + - + + + - - - -
- -
+ {children} +
- - - {brandTitle} - - - {brandSubtitle} + + + + + +
+ +
+ + + + {brandTitle} + + + {brandSubtitle} + + + + + {FEATURES.map((feature) => ( + +
+ +
+ + {feature} + +
+ ))} +
+ + + © 2026 Ethiopian Maritime Authority
- - - {FEATURES.map((feature) => ( - -
- -
- - {feature} - -
- ))} -
- - - © 2026 Ethiopian Maritime Authority - -
-
-
-
-
+
+
+
+ + ); }