mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-29 15:18:12 +00:00
Merge branch 'dev' of github.com:Tria-plc/emaui into feature/exam-attempt-domain
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { landingAm } from "@ema-platform/ui";
|
||||
import type { Translations } from "./en";
|
||||
|
||||
export const am: Translations = {
|
||||
landing: landingAm,
|
||||
|
||||
app: {
|
||||
name: "ኢማ አስተዳደር",
|
||||
shortName: "ኢማ",
|
||||
@@ -1294,4 +1297,68 @@ export const am: Translations = {
|
||||
view: "ይመልከቱ",
|
||||
noUrl: "URL የለም",
|
||||
},
|
||||
|
||||
authShell: {
|
||||
toggleTheme: "ገጽታ ቀይር",
|
||||
brandTitle: "የባህር ትራንስፖርት ፈቃድ አሰጣጥ ቀላል ሆኗል።",
|
||||
brandSubtitle: "ለመርከብ እና ለመርከበኛ ፈቃዶች ያመልክቱ፣ ሰነዶችን ይስቀሉ፣ እና እያንዳንዱን ማመልከቻ በአንድ ደህንነቱ በተጠበቀ መተግበሪያ ላይ ይከታተሉ።",
|
||||
feature1: "ማመልከቻዎችን በመስመር ላይ 24/7 ያስገቡ",
|
||||
feature2: "የቅጽበት ሁኔታ ክትትል እና ማንቂያ",
|
||||
feature3: "በእንግሊዝኛ እና በአማርኛ ይገኛል",
|
||||
copyright: "© 2026 የኢትዮጵያ የባህር ትራንስፖርት ባለስልጣን",
|
||||
},
|
||||
|
||||
login: {
|
||||
emailOrPhoneInvalid: "ትክክለኛ ኢሜይል ወይም ስልክ ቁጥር ያስገቡ (+2519xxxxxxxx)",
|
||||
passwordMinLength: "የይለፍ ቃል ቢያንስ 8 ቁምፊዎች ሊኖረው ይገባል",
|
||||
welcome: "እንኳን ወደ {{appName}} በደህና መጡ",
|
||||
subtitle: "መለያዎን ለመድረስ ይግቡ።",
|
||||
emailOrPhoneLabel: "ኢሜይል ወይም ስልክ",
|
||||
emailOrPhonePlaceholder: "you@example.com",
|
||||
passwordLabel: "የይለፍ ቃል",
|
||||
passwordPlaceholder: "የይለፍ ቃልዎ",
|
||||
rememberMe: "አስታውሰኝ",
|
||||
forgotPassword: "የይለፍ ቃል ረሱ?",
|
||||
signIn: "ግባ",
|
||||
noAccount: "መለያ የለዎትም? ",
|
||||
createOne: "አንድ ይፍጠሩ",
|
||||
},
|
||||
|
||||
signup: {
|
||||
usernameMinLength: "የተጠቃሚ ስም ቢያንስ 3 ቁምፊዎች ሊኖረው ይገባል",
|
||||
nameEnRequired: "ስም (እንግሊዝኛ) ያስፈልጋል",
|
||||
phoneRequired: "ስልክ ቁጥር ያስፈልጋል",
|
||||
confirmPasswordRequired: "የይለፍ ቃልዎን ያረጋግጡ",
|
||||
passwordsDontMatch: "የይለፍ ቃላት አይመሳሰሉም",
|
||||
brandTitle: "የ{{appName}} ማህበረሰብን ይቀላቀሉ።",
|
||||
brandSubtitle: "የ{{appName}} አገልግሎቶችን ለመድረስ መለያዎን ይፍጠሩ።",
|
||||
title: "መለያ ይፍጠሩ",
|
||||
subtitle: "ለመጀመር አንድ ደቂቃ ብቻ ይወስዳል።",
|
||||
nameEnLabel: "ስም (እንግሊዝኛ)",
|
||||
nameEnPlaceholder: "አበበ በቀለ",
|
||||
nameAmLabel: "ስም (አማርኛ)",
|
||||
nameAmPlaceholder: "ስም",
|
||||
emailLabel: "ኢሜይል አድራሻ",
|
||||
emailPlaceholder: "you@example.com",
|
||||
usernameLabel: "የተጠቃሚ ስም",
|
||||
usernamePlaceholder: "የተጠቃሚ ስም ይምረጡ",
|
||||
phoneLabel: "ስልክ ቁጥር",
|
||||
phonePlaceholder: "+251 911 234 567",
|
||||
passwordLabel: "የይለፍ ቃል",
|
||||
passwordPlaceholder: "ቢያንስ 8 ቁምፊዎች",
|
||||
confirmPasswordLabel: "የይለፍ ቃል ያረጋግጡ",
|
||||
confirmPasswordPlaceholder: "የይለፍ ቃል እንደገና ያስገቡ",
|
||||
agreeToThe: "እስማማለሁ ከ",
|
||||
termsAndPrivacy: "ውሎች እና የግላዊነት ፖሊሲ",
|
||||
createAccount: "መለያ ይፍጠሩ",
|
||||
haveAccount: "አስቀድሞ መለያ አለዎት? ",
|
||||
signIn: "ግባ",
|
||||
passwordRule: {
|
||||
minLength: "ቢያንስ {{min}} ቁምፊዎች",
|
||||
lowercase: "አንድ ትንሽ ፊደል",
|
||||
uppercase: "አንድ ትልቅ ፊደል",
|
||||
number: "አንድ ቁጥር",
|
||||
special: "አንድ ልዩ ምልክት",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { landingEn } from '@ema-platform/ui';
|
||||
|
||||
export const en = {
|
||||
landing: landingEn,
|
||||
|
||||
app: {
|
||||
name: 'EMA Admin',
|
||||
shortName: 'EMA',
|
||||
@@ -1290,6 +1294,70 @@ export const en = {
|
||||
view: 'View',
|
||||
noUrl: 'No URL',
|
||||
},
|
||||
|
||||
authShell: {
|
||||
toggleTheme: 'Toggle theme',
|
||||
brandTitle: 'Maritime licensing, made simple.',
|
||||
brandSubtitle: 'Apply for vessel and seafarer licenses, upload documents, and track every application in one secure portal.',
|
||||
feature1: 'Submit applications online, 24/7',
|
||||
feature2: 'Real-time status tracking & alerts',
|
||||
feature3: 'Available in English & አማርኛ',
|
||||
copyright: '© 2026 Ethiopian Maritime Authority',
|
||||
},
|
||||
|
||||
login: {
|
||||
emailOrPhoneInvalid: 'Enter a valid email or phone number (+2519xxxxxxxx)',
|
||||
passwordMinLength: 'Password must be at least 8 characters',
|
||||
welcome: 'Welcome to {{appName}}',
|
||||
subtitle: 'Sign in to access your account.',
|
||||
emailOrPhoneLabel: 'Email or phone',
|
||||
emailOrPhonePlaceholder: 'you@example.com',
|
||||
passwordLabel: 'Password',
|
||||
passwordPlaceholder: 'Your password',
|
||||
rememberMe: 'Remember me',
|
||||
forgotPassword: 'Forgot password?',
|
||||
signIn: 'Sign in',
|
||||
noAccount: "Don't have an account? ",
|
||||
createOne: 'Create one',
|
||||
},
|
||||
|
||||
signup: {
|
||||
usernameMinLength: 'Username must be at least 3 characters',
|
||||
nameEnRequired: 'Name (English) is required',
|
||||
phoneRequired: 'Phone number is required',
|
||||
confirmPasswordRequired: 'Confirm your password',
|
||||
passwordsDontMatch: 'Passwords do not match',
|
||||
brandTitle: "Join {{appName}}'s community.",
|
||||
brandSubtitle: 'Create your account to access {{appName}} features.',
|
||||
title: 'Create account',
|
||||
subtitle: 'It only takes a minute to get started.',
|
||||
nameEnLabel: 'Name (English)',
|
||||
nameEnPlaceholder: 'Abebe Bekele',
|
||||
nameAmLabel: 'Name (Amharic)',
|
||||
nameAmPlaceholder: 'ስም',
|
||||
emailLabel: 'Email address',
|
||||
emailPlaceholder: 'you@example.com',
|
||||
usernameLabel: 'Username',
|
||||
usernamePlaceholder: 'Choose a username',
|
||||
phoneLabel: 'Phone number',
|
||||
phonePlaceholder: '+251 911 234 567',
|
||||
passwordLabel: 'Password',
|
||||
passwordPlaceholder: 'At least 8 characters',
|
||||
confirmPasswordLabel: 'Confirm password',
|
||||
confirmPasswordPlaceholder: 'Re-enter password',
|
||||
agreeToThe: 'I agree to the ',
|
||||
termsAndPrivacy: 'Terms & Privacy Policy',
|
||||
createAccount: 'Create account',
|
||||
haveAccount: 'Already have an account? ',
|
||||
signIn: 'Sign in',
|
||||
passwordRule: {
|
||||
minLength: 'At least {{min}} characters',
|
||||
lowercase: 'One lowercase letter',
|
||||
uppercase: 'One uppercase letter',
|
||||
number: 'One number',
|
||||
special: 'One special character',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type Translations = typeof en;
|
||||
|
||||
13
apps/backoffice/src/app/router/LandingRoute.tsx
Normal file
13
apps/backoffice/src/app/router/LandingRoute.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import { LandingPage } from '@ema-platform/ui';
|
||||
import { authStorage } from '@ema-platform/auth';
|
||||
|
||||
/**
|
||||
* Public `/` — mounts the shared landing page. Backoffice has no /signup
|
||||
* (enableSignup: false) and no /verify route, so those props are omitted.
|
||||
*/
|
||||
export function LandingRoute() {
|
||||
const token = authStorage.getToken() ?? Cookies.get('auth-token');
|
||||
|
||||
return <LandingPage primaryHref={token ? '/dashboard' : '/login'} />;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { AuthLayout } from '../layouts/AuthLayout';
|
||||
import { BackofficeLayout } from '../layouts/BackofficeLayout';
|
||||
import { ProtectedRoute } from './ProtectedRoute';
|
||||
import { LandingRoute } from './LandingRoute';
|
||||
import { DashboardPage } from '../features/dashboard/pages/DashboardPage';
|
||||
import UserManagementPage from '../features/user-management/UserManagementPage';
|
||||
import { ProfilePage } from '../features/profile/pages/ProfilePage';
|
||||
@@ -61,7 +62,7 @@ const router = createBrowserRouter([
|
||||
],
|
||||
},
|
||||
{ path: '/um/*', element: <UserManagementPage /> },
|
||||
{ path: '/', element: <Navigate to="/dashboard" replace /> },
|
||||
{ path: '/', element: <LandingRoute /> },
|
||||
{ path: '/profile-setup', element: <Navigate to="/dashboard" replace /> },
|
||||
{
|
||||
element: <ProtectedRoute />,
|
||||
@@ -69,7 +70,6 @@ const router = createBrowserRouter([
|
||||
{
|
||||
element: <BackofficeLayout />,
|
||||
children: [
|
||||
{ index: true, element: <Navigate to="/dashboard" replace /> },
|
||||
{ path: 'dashboard', element: <DashboardPage /> },
|
||||
{ path: 'vessel-registration-head-dashboard', element: guard([P.VIEW_VESSEL_REGISTRY], <VesselRegistrationHeadDashboardPage />) },
|
||||
{ path: 'logistics-head-dashboard', element: guard(APPLICATION_QUEUE, <LogisticsHeadDashboardPage />) },
|
||||
|
||||
@@ -39,7 +39,7 @@ window.__USER_MANAGEMENT_BRANDING__ = {
|
||||
organizationName: 'Ethiopian Maritime Authority',
|
||||
logoSrc: '/assets/emaLogo.jpg',
|
||||
logoAlt: 'EMA Logo',
|
||||
homePath: '/',
|
||||
homePath: '/dashboard',
|
||||
moduleBasePath: '/user-management',
|
||||
backToAppPath: '/dashboard',
|
||||
backToAppLabel: 'Back to dashboard',
|
||||
|
||||
15
apps/portal/src/app/components/LandingRoute.tsx
Normal file
15
apps/portal/src/app/components/LandingRoute.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import { LandingPage } from '@ema-platform/ui';
|
||||
import { authStorage } from '@ema-platform/auth';
|
||||
|
||||
/**
|
||||
* Public `/` — mounts the shared landing page with portal-specific routes.
|
||||
* Auth state is read the same way ProtectedRoute does (token cookie or
|
||||
* storage fallback) so the header can show "Go to dashboard" instead of
|
||||
* Login/Sign Up without gating the route itself.
|
||||
*/
|
||||
export function LandingRoute() {
|
||||
const token = authStorage.getToken() ?? Cookies.get('auth-token');
|
||||
|
||||
return <LandingPage primaryHref={token ? '/dashboard' : '/login'} signupHref="/signup" />;
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Container,
|
||||
Group,
|
||||
Loader,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconCircleCheck,
|
||||
IconCircleX,
|
||||
IconSearch,
|
||||
IconShieldCheck,
|
||||
} from '@tabler/icons-react';
|
||||
import { useVerifyCertificateQuery } from '@ema-platform/api';
|
||||
|
||||
/**
|
||||
* Public certificate verification (US-PORTAL-008) — the page every printed
|
||||
* QR code points at. Deliberately public and chrome-free: a bank clerk or
|
||||
* port official scanning a certificate has no portal account.
|
||||
*/
|
||||
export function VerifyCertificatePage() {
|
||||
const { code } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [manualCode, setManualCode] = useState('');
|
||||
const { data, isLoading, isError } = useVerifyCertificateQuery(code ?? '', {
|
||||
skip: !code,
|
||||
});
|
||||
|
||||
const rows: [string, string | null | undefined][] = data?.valid
|
||||
? [
|
||||
['Certificate number', data.certificateNumber],
|
||||
['Licence / registration', data.licenseType],
|
||||
['Holder', data.companyName],
|
||||
['Issued', data.issueDate],
|
||||
['Expires', data.expiryDate],
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Container size="xs" py="xl">
|
||||
<Stack align="center" gap="lg">
|
||||
<Group gap="xs">
|
||||
<IconShieldCheck size={28} color="var(--mantine-color-blue-6)" />
|
||||
<Title order={3}>EMA Certificate Verification</Title>
|
||||
</Group>
|
||||
|
||||
{!code && (
|
||||
<Card withBorder radius="md" p="lg" w="100%">
|
||||
<Stack>
|
||||
<Text size="sm" c="dimmed">
|
||||
Scan the QR code on a certificate, or enter its verification
|
||||
code below.
|
||||
</Text>
|
||||
<Group>
|
||||
<TextInput
|
||||
flex={1}
|
||||
placeholder="Verification code"
|
||||
value={manualCode}
|
||||
onChange={(e) => setManualCode(e.currentTarget.value)}
|
||||
/>
|
||||
<Button
|
||||
leftSection={<IconSearch size={16} />}
|
||||
disabled={!manualCode.trim()}
|
||||
onClick={() => navigate(`/verify/${manualCode.trim()}`)}
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{code && isLoading && (
|
||||
<Center py="xl">
|
||||
<Loader />
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{code && !isLoading && (isError || !data) && (
|
||||
<Card withBorder radius="md" p="lg" w="100%">
|
||||
<Text c="red" ta="center">
|
||||
Verification is temporarily unavailable. Please try again.
|
||||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{code && data && (
|
||||
<Card withBorder radius="md" p="lg" w="100%">
|
||||
<Stack>
|
||||
{data.valid ? (
|
||||
<Group>
|
||||
<IconCircleCheck
|
||||
size={40}
|
||||
color="var(--mantine-color-green-6)"
|
||||
/>
|
||||
<div>
|
||||
<Text fw={700} size="lg" c="green">
|
||||
Valid certificate
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Issued by the Ethiopian Maritime Authority.
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
) : (
|
||||
<Group>
|
||||
<IconCircleX size={40} color="var(--mantine-color-red-6)" />
|
||||
<div>
|
||||
<Group gap="xs">
|
||||
<Text fw={700} size="lg" c="red">
|
||||
Not valid
|
||||
</Text>
|
||||
{data.status && <Badge color="red">{data.status}</Badge>}
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
{data.reason === 'not_found'
|
||||
? 'No certificate matches this code.'
|
||||
: 'This certificate is no longer valid.'}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{rows.length > 0 && (
|
||||
<Table variant="vertical" layout="fixed">
|
||||
<Table.Tbody>
|
||||
{rows
|
||||
.filter(([, value]) => value)
|
||||
.map(([label, value]) => (
|
||||
<Table.Tr key={label}>
|
||||
<Table.Th w={170}>{label}</Table.Th>
|
||||
<Table.Td>{value}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="compact-sm"
|
||||
onClick={() => {
|
||||
setManualCode('');
|
||||
navigate('/verify');
|
||||
}}
|
||||
>
|
||||
Verify another certificate
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { landingAm } from '@ema-platform/ui';
|
||||
import type { Translations } from './en';
|
||||
|
||||
export const am: Translations = {
|
||||
landing: landingAm,
|
||||
|
||||
app: {
|
||||
name: 'ኢባባ ፖርታል',
|
||||
authority: 'የኢትዮጵያ ባሕር ጉዳዮች ባለሥልጣን',
|
||||
@@ -386,4 +389,68 @@ export const am: Translations = {
|
||||
SUPERSEDED: "ተተክቷል",
|
||||
},
|
||||
},
|
||||
|
||||
authShell: {
|
||||
toggleTheme: "ገጽታ ቀይር",
|
||||
brandTitle: "የባህር ትራንስፖርት ፈቃድ አሰጣጥ ቀላል ሆኗል።",
|
||||
brandSubtitle: "ለመርከብ እና ለመርከበኛ ፈቃዶች ያመልክቱ፣ ሰነዶችን ይስቀሉ፣ እና እያንዳንዱን ማመልከቻ በአንድ ደህንነቱ በተጠበቀ መተግበሪያ ላይ ይከታተሉ።",
|
||||
feature1: "ማመልከቻዎችን በመስመር ላይ 24/7 ያስገቡ",
|
||||
feature2: "የቅጽበት ሁኔታ ክትትል እና ማንቂያ",
|
||||
feature3: "በእንግሊዝኛ እና በአማርኛ ይገኛል",
|
||||
copyright: "© 2026 የኢትዮጵያ የባህር ትራንስፖርት ባለስልጣን",
|
||||
},
|
||||
|
||||
login: {
|
||||
emailOrPhoneInvalid: "ትክክለኛ ኢሜይል ወይም ስልክ ቁጥር ያስገቡ (+2519xxxxxxxx)",
|
||||
passwordMinLength: "የይለፍ ቃል ቢያንስ 8 ቁምፊዎች ሊኖረው ይገባል",
|
||||
welcome: "እንኳን ወደ {{appName}} በደህና መጡ",
|
||||
subtitle: "መለያዎን ለመድረስ ይግቡ።",
|
||||
emailOrPhoneLabel: "ኢሜይል ወይም ስልክ",
|
||||
emailOrPhonePlaceholder: "you@example.com",
|
||||
passwordLabel: "የይለፍ ቃል",
|
||||
passwordPlaceholder: "የይለፍ ቃልዎ",
|
||||
rememberMe: "አስታውሰኝ",
|
||||
forgotPassword: "የይለፍ ቃል ረሱ?",
|
||||
signIn: "ግባ",
|
||||
noAccount: "መለያ የለዎትም? ",
|
||||
createOne: "አንድ ይፍጠሩ",
|
||||
},
|
||||
|
||||
signup: {
|
||||
usernameMinLength: "የተጠቃሚ ስም ቢያንስ 3 ቁምፊዎች ሊኖረው ይገባል",
|
||||
nameEnRequired: "ስም (እንግሊዝኛ) ያስፈልጋል",
|
||||
phoneRequired: "ስልክ ቁጥር ያስፈልጋል",
|
||||
confirmPasswordRequired: "የይለፍ ቃልዎን ያረጋግጡ",
|
||||
passwordsDontMatch: "የይለፍ ቃላት አይመሳሰሉም",
|
||||
brandTitle: "የ{{appName}} ማህበረሰብን ይቀላቀሉ።",
|
||||
brandSubtitle: "የ{{appName}} አገልግሎቶችን ለመድረስ መለያዎን ይፍጠሩ።",
|
||||
title: "መለያ ይፍጠሩ",
|
||||
subtitle: "ለመጀመር አንድ ደቂቃ ብቻ ይወስዳል።",
|
||||
nameEnLabel: "ስም (እንግሊዝኛ)",
|
||||
nameEnPlaceholder: "አበበ በቀለ",
|
||||
nameAmLabel: "ስም (አማርኛ)",
|
||||
nameAmPlaceholder: "ስም",
|
||||
emailLabel: "ኢሜይል አድራሻ",
|
||||
emailPlaceholder: "you@example.com",
|
||||
usernameLabel: "የተጠቃሚ ስም",
|
||||
usernamePlaceholder: "የተጠቃሚ ስም ይምረጡ",
|
||||
phoneLabel: "ስልክ ቁጥር",
|
||||
phonePlaceholder: "+251 911 234 567",
|
||||
passwordLabel: "የይለፍ ቃል",
|
||||
passwordPlaceholder: "ቢያንስ 8 ቁምፊዎች",
|
||||
confirmPasswordLabel: "የይለፍ ቃል ያረጋግጡ",
|
||||
confirmPasswordPlaceholder: "የይለፍ ቃል እንደገና ያስገቡ",
|
||||
agreeToThe: "እስማማለሁ ከ",
|
||||
termsAndPrivacy: "ውሎች እና የግላዊነት ፖሊሲ",
|
||||
createAccount: "መለያ ይፍጠሩ",
|
||||
haveAccount: "አስቀድሞ መለያ አለዎት? ",
|
||||
signIn: "ግባ",
|
||||
passwordRule: {
|
||||
minLength: "ቢያንስ {{min}} ቁምፊዎች",
|
||||
lowercase: "አንድ ትንሽ ፊደል",
|
||||
uppercase: "አንድ ትልቅ ፊደል",
|
||||
number: "አንድ ቁጥር",
|
||||
special: "አንድ ልዩ ምልክት",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { landingEn } from '@ema-platform/ui';
|
||||
|
||||
export const en = {
|
||||
landing: landingEn,
|
||||
|
||||
app: {
|
||||
name: 'EMA Portal',
|
||||
authority: 'Ethiopian Maritime Authority',
|
||||
@@ -385,6 +389,70 @@ export const en = {
|
||||
SUPERSEDED: 'Superseded',
|
||||
},
|
||||
},
|
||||
|
||||
authShell: {
|
||||
toggleTheme: 'Toggle theme',
|
||||
brandTitle: 'Maritime licensing, made simple.',
|
||||
brandSubtitle: 'Apply for vessel and seafarer licenses, upload documents, and track every application in one secure portal.',
|
||||
feature1: 'Submit applications online, 24/7',
|
||||
feature2: 'Real-time status tracking & alerts',
|
||||
feature3: 'Available in English & አማርኛ',
|
||||
copyright: '© 2026 Ethiopian Maritime Authority',
|
||||
},
|
||||
|
||||
login: {
|
||||
emailOrPhoneInvalid: 'Enter a valid email or phone number (+2519xxxxxxxx)',
|
||||
passwordMinLength: 'Password must be at least 8 characters',
|
||||
welcome: 'Welcome to {{appName}}',
|
||||
subtitle: 'Sign in to access your account.',
|
||||
emailOrPhoneLabel: 'Email or phone',
|
||||
emailOrPhonePlaceholder: 'you@example.com',
|
||||
passwordLabel: 'Password',
|
||||
passwordPlaceholder: 'Your password',
|
||||
rememberMe: 'Remember me',
|
||||
forgotPassword: 'Forgot password?',
|
||||
signIn: 'Sign in',
|
||||
noAccount: "Don't have an account? ",
|
||||
createOne: 'Create one',
|
||||
},
|
||||
|
||||
signup: {
|
||||
usernameMinLength: 'Username must be at least 3 characters',
|
||||
nameEnRequired: 'Name (English) is required',
|
||||
phoneRequired: 'Phone number is required',
|
||||
confirmPasswordRequired: 'Confirm your password',
|
||||
passwordsDontMatch: 'Passwords do not match',
|
||||
brandTitle: "Join {{appName}}'s community.",
|
||||
brandSubtitle: 'Create your account to access {{appName}} features.',
|
||||
title: 'Create account',
|
||||
subtitle: 'It only takes a minute to get started.',
|
||||
nameEnLabel: 'Name (English)',
|
||||
nameEnPlaceholder: 'Abebe Bekele',
|
||||
nameAmLabel: 'Name (Amharic)',
|
||||
nameAmPlaceholder: 'ስም',
|
||||
emailLabel: 'Email address',
|
||||
emailPlaceholder: 'you@example.com',
|
||||
usernameLabel: 'Username',
|
||||
usernamePlaceholder: 'Choose a username',
|
||||
phoneLabel: 'Phone number',
|
||||
phonePlaceholder: '+251 911 234 567',
|
||||
passwordLabel: 'Password',
|
||||
passwordPlaceholder: 'At least 8 characters',
|
||||
confirmPasswordLabel: 'Confirm password',
|
||||
confirmPasswordPlaceholder: 'Re-enter password',
|
||||
agreeToThe: 'I agree to the ',
|
||||
termsAndPrivacy: 'Terms & Privacy Policy',
|
||||
createAccount: 'Create account',
|
||||
haveAccount: 'Already have an account? ',
|
||||
signIn: 'Sign in',
|
||||
passwordRule: {
|
||||
minLength: 'At least {{min}} characters',
|
||||
lowercase: 'One lowercase letter',
|
||||
uppercase: 'One uppercase letter',
|
||||
number: 'One number',
|
||||
special: 'One special character',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type Translations = typeof en;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { AuthBootstrap, AuthConfigProvider } from '@ema-platform/auth';
|
||||
import type { ReactNode } from 'react';
|
||||
import { store } from '../store';
|
||||
import { i18n } from '../i18n/config';
|
||||
import { MantineThemeProvider } from './MantineThemeProvider';
|
||||
import { ErrorBoundary } from '../components/ErrorBoundary';
|
||||
|
||||
@@ -26,9 +28,11 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
||||
enableForgotPassword: true,
|
||||
}}
|
||||
>
|
||||
<MantineThemeProvider>
|
||||
<AuthBootstrap>{children}</AuthBootstrap>
|
||||
</MantineThemeProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<MantineThemeProvider>
|
||||
<AuthBootstrap>{children}</AuthBootstrap>
|
||||
</MantineThemeProvider>
|
||||
</I18nextProvider>
|
||||
</AuthConfigProvider>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { createBrowserRouter, Navigate } from "react-router-dom";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { i18n } from "./i18n/config";
|
||||
import { PortalLayout } from "./layouts/PortalLayout";
|
||||
import { ProtectedRoute } from "./components/ProtectedRoute";
|
||||
import { LandingRoute } from "./components/LandingRoute";
|
||||
|
||||
// Auth (standalone pages, no portal chrome)
|
||||
import {
|
||||
@@ -30,7 +29,6 @@ import { ProfilePage } from "./features/profile/pages/ProfilePage";
|
||||
import { SupportPage } from "./features/support/pages/SupportPage";
|
||||
import { SeafarerRegistrationPage } from "./features/seafarer/pages/SeafarerRegistrationPage";
|
||||
import { MySeaRecordsPage } from "./features/seafarer/pages/MySeaRecordsPage";
|
||||
import { VerifyCertificatePage } from "./features/verify/pages/VerifyCertificatePage";
|
||||
import { ExamsPage } from "./features/exams/pages/ExamsPage";
|
||||
import { ExamAttemptPage } from "./features/exam-attempt/pages/ExamAttemptPage";
|
||||
|
||||
@@ -59,15 +57,13 @@ import { WaiverPage } from "./features/waiver/pages/WaiverPage";
|
||||
import { VesselRegistrationStatusPage } from "./features/vessel-registration/pages/VesselRegistrationStatusPage";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
// Public landing page — institutional overview + role-based entry points.
|
||||
{ path: "/", element: <LandingRoute /> },
|
||||
|
||||
// Public auth pages
|
||||
{ path: "/login", element: <LoginPage /> },
|
||||
{ path: "/signup", element: <SignupPage /> },
|
||||
|
||||
// Public certificate verification — the target of every printed QR code.
|
||||
// No auth: a verifier scanning a certificate has no portal account.
|
||||
{ path: "/verify", element: <VerifyCertificatePage /> },
|
||||
{ path: "/verify/:code", element: <VerifyCertificatePage /> },
|
||||
|
||||
// Completes the forgot-password flow; the reset message links here. The
|
||||
// IAM package generates `/reset-password` links, `/set-password` is the
|
||||
// first-time-credential variant — one page serves both.
|
||||
@@ -101,17 +97,14 @@ export const router = createBrowserRouter([
|
||||
{
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
{/* Asks what the applicant operates as before the rest of the
|
||||
portal, which is filtered by that answer. */}
|
||||
<RequireOperations>
|
||||
<PortalLayout />
|
||||
</RequireOperations>
|
||||
</I18nextProvider>
|
||||
{/* Asks what the applicant operates as before the rest of the
|
||||
portal, which is filtered by that answer. */}
|
||||
<RequireOperations>
|
||||
<PortalLayout />
|
||||
</RequireOperations>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
children: [
|
||||
{ path: "/", element: <Navigate to="/dashboard" replace /> },
|
||||
{ path: "/dashboard", element: <DashboardPage /> },
|
||||
{ path: "/onboarding/operations", element: <OperationsOnboardingPage /> },
|
||||
|
||||
@@ -418,5 +411,7 @@ export const router = createBrowserRouter([
|
||||
element: <Navigate to="/vessel-ownership-transfer" replace />,
|
||||
},
|
||||
|
||||
{ path: "*", element: <Navigate to="/" replace /> },
|
||||
// A typo'd URL should not maroon a signed-in user on the marketing page —
|
||||
// ProtectedRoute sends anonymous visitors on to /login exactly as before.
|
||||
{ path: "*", element: <Navigate to="/dashboard" replace /> },
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user