mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-08 19:28:18 +00:00
feat: add landing page copy and styles for English and Amharic versions
- Introduced `landing-copy.ts` containing text for the public landing page, supporting both English and Amharic languages. - Added `landing.css` for styling the landing page, ensuring scoped styles to prevent conflicts with other app styles. - Implemented smooth scrolling and animations for a better user experience.
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
|
import { landingAm } from "@ema-platform/ui";
|
||||||
import type { Translations } from "./en";
|
import type { Translations } from "./en";
|
||||||
|
|
||||||
export const am: Translations = {
|
export const am: Translations = {
|
||||||
|
landing: landingAm,
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
name: "ኢማ አስተዳደር",
|
name: "ኢማ አስተዳደር",
|
||||||
shortName: "ኢማ",
|
shortName: "ኢማ",
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import { landingEn } from '@ema-platform/ui';
|
||||||
|
|
||||||
export const en = {
|
export const en = {
|
||||||
|
landing: landingEn,
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
name: 'EMA Admin',
|
name: 'EMA Admin',
|
||||||
shortName: 'EMA',
|
shortName: 'EMA',
|
||||||
|
|||||||
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 { AuthLayout } from '../layouts/AuthLayout';
|
||||||
import { BackofficeLayout } from '../layouts/BackofficeLayout';
|
import { BackofficeLayout } from '../layouts/BackofficeLayout';
|
||||||
import { ProtectedRoute } from './ProtectedRoute';
|
import { ProtectedRoute } from './ProtectedRoute';
|
||||||
|
import { LandingRoute } from './LandingRoute';
|
||||||
import { DashboardPage } from '../features/dashboard/pages/DashboardPage';
|
import { DashboardPage } from '../features/dashboard/pages/DashboardPage';
|
||||||
import UserManagementPage from '../features/user-management/UserManagementPage';
|
import UserManagementPage from '../features/user-management/UserManagementPage';
|
||||||
import { ProfilePage } from '../features/profile/pages/ProfilePage';
|
import { ProfilePage } from '../features/profile/pages/ProfilePage';
|
||||||
@@ -61,7 +62,7 @@ const router = createBrowserRouter([
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: '/um/*', element: <UserManagementPage /> },
|
{ path: '/um/*', element: <UserManagementPage /> },
|
||||||
{ path: '/', element: <Navigate to="/dashboard" replace /> },
|
{ path: '/', element: <LandingRoute /> },
|
||||||
{ path: '/profile-setup', element: <Navigate to="/dashboard" replace /> },
|
{ path: '/profile-setup', element: <Navigate to="/dashboard" replace /> },
|
||||||
{
|
{
|
||||||
element: <ProtectedRoute />,
|
element: <ProtectedRoute />,
|
||||||
@@ -69,7 +70,6 @@ const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
element: <BackofficeLayout />,
|
element: <BackofficeLayout />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <Navigate to="/dashboard" replace /> },
|
|
||||||
{ path: 'dashboard', element: <DashboardPage /> },
|
{ path: 'dashboard', element: <DashboardPage /> },
|
||||||
{ path: 'vessel-registration-head-dashboard', element: guard([P.VIEW_VESSEL_REGISTRY], <VesselRegistrationHeadDashboardPage />) },
|
{ path: 'vessel-registration-head-dashboard', element: guard([P.VIEW_VESSEL_REGISTRY], <VesselRegistrationHeadDashboardPage />) },
|
||||||
{ path: 'logistics-head-dashboard', element: guard(APPLICATION_QUEUE, <LogisticsHeadDashboardPage />) },
|
{ path: 'logistics-head-dashboard', element: guard(APPLICATION_QUEUE, <LogisticsHeadDashboardPage />) },
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ window.__USER_MANAGEMENT_BRANDING__ = {
|
|||||||
organizationName: 'Ethiopian Maritime Authority',
|
organizationName: 'Ethiopian Maritime Authority',
|
||||||
logoSrc: '/assets/emaLogo.jpg',
|
logoSrc: '/assets/emaLogo.jpg',
|
||||||
logoAlt: 'EMA Logo',
|
logoAlt: 'EMA Logo',
|
||||||
homePath: '/',
|
homePath: '/dashboard',
|
||||||
moduleBasePath: '/user-management',
|
moduleBasePath: '/user-management',
|
||||||
backToAppPath: '/dashboard',
|
backToAppPath: '/dashboard',
|
||||||
backToAppLabel: 'Back to 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" verifyHref="/verify" />;
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import { landingAm } from '@ema-platform/ui';
|
||||||
import type { Translations } from './en';
|
import type { Translations } from './en';
|
||||||
|
|
||||||
export const am: Translations = {
|
export const am: Translations = {
|
||||||
|
landing: landingAm,
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
name: 'ኢባባ ፖርታል',
|
name: 'ኢባባ ፖርታል',
|
||||||
authority: 'የኢትዮጵያ ባሕር ጉዳዮች ባለሥልጣን',
|
authority: 'የኢትዮጵያ ባሕር ጉዳዮች ባለሥልጣን',
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import { landingEn } from '@ema-platform/ui';
|
||||||
|
|
||||||
export const en = {
|
export const en = {
|
||||||
|
landing: landingEn,
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
name: 'EMA Portal',
|
name: 'EMA Portal',
|
||||||
authority: 'Ethiopian Maritime Authority',
|
authority: 'Ethiopian Maritime Authority',
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { AuthBootstrap, AuthConfigProvider } from '@ema-platform/auth';
|
import { AuthBootstrap, AuthConfigProvider } from '@ema-platform/auth';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { store } from '../store';
|
import { store } from '../store';
|
||||||
|
import { i18n } from '../i18n/config';
|
||||||
import { MantineThemeProvider } from './MantineThemeProvider';
|
import { MantineThemeProvider } from './MantineThemeProvider';
|
||||||
import { ErrorBoundary } from '../components/ErrorBoundary';
|
import { ErrorBoundary } from '../components/ErrorBoundary';
|
||||||
|
|
||||||
@@ -26,9 +28,11 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
|||||||
enableForgotPassword: true,
|
enableForgotPassword: true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MantineThemeProvider>
|
<I18nextProvider i18n={i18n}>
|
||||||
<AuthBootstrap>{children}</AuthBootstrap>
|
<MantineThemeProvider>
|
||||||
</MantineThemeProvider>
|
<AuthBootstrap>{children}</AuthBootstrap>
|
||||||
|
</MantineThemeProvider>
|
||||||
|
</I18nextProvider>
|
||||||
</AuthConfigProvider>
|
</AuthConfigProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { createBrowserRouter, Navigate } from "react-router-dom";
|
import { createBrowserRouter, Navigate } from "react-router-dom";
|
||||||
import { I18nextProvider } from "react-i18next";
|
|
||||||
import { i18n } from "./i18n/config";
|
|
||||||
import { PortalLayout } from "./layouts/PortalLayout";
|
import { PortalLayout } from "./layouts/PortalLayout";
|
||||||
import { ProtectedRoute } from "./components/ProtectedRoute";
|
import { ProtectedRoute } from "./components/ProtectedRoute";
|
||||||
|
import { LandingRoute } from "./components/LandingRoute";
|
||||||
|
|
||||||
// Auth (standalone pages, no portal chrome)
|
// Auth (standalone pages, no portal chrome)
|
||||||
import {
|
import {
|
||||||
@@ -58,6 +57,9 @@ import { WaiverPage } from "./features/waiver/pages/WaiverPage";
|
|||||||
import { VesselRegistrationStatusPage } from "./features/vessel-registration/pages/VesselRegistrationStatusPage";
|
import { VesselRegistrationStatusPage } from "./features/vessel-registration/pages/VesselRegistrationStatusPage";
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
|
// Public landing page — institutional overview + role-based entry points.
|
||||||
|
{ path: "/", element: <LandingRoute /> },
|
||||||
|
|
||||||
// Public auth pages
|
// Public auth pages
|
||||||
{ path: "/login", element: <LoginPage /> },
|
{ path: "/login", element: <LoginPage /> },
|
||||||
{ path: "/signup", element: <SignupPage /> },
|
{ path: "/signup", element: <SignupPage /> },
|
||||||
@@ -100,17 +102,14 @@ export const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
element: (
|
element: (
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<I18nextProvider i18n={i18n}>
|
{/* Asks what the applicant operates as before the rest of the
|
||||||
{/* Asks what the applicant operates as before the rest of the
|
portal, which is filtered by that answer. */}
|
||||||
portal, which is filtered by that answer. */}
|
<RequireOperations>
|
||||||
<RequireOperations>
|
<PortalLayout />
|
||||||
<PortalLayout />
|
</RequireOperations>
|
||||||
</RequireOperations>
|
|
||||||
</I18nextProvider>
|
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
{ path: "/", element: <Navigate to="/dashboard" replace /> },
|
|
||||||
{ path: "/dashboard", element: <DashboardPage /> },
|
{ path: "/dashboard", element: <DashboardPage /> },
|
||||||
{ path: "/onboarding/operations", element: <OperationsOnboardingPage /> },
|
{ path: "/onboarding/operations", element: <OperationsOnboardingPage /> },
|
||||||
|
|
||||||
@@ -409,5 +408,7 @@ export const router = createBrowserRouter([
|
|||||||
element: <Navigate to="/vessel-ownership-transfer" replace />,
|
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 /> },
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -21,3 +21,5 @@ export * from "./lib/input/phone";
|
|||||||
export * from "./lib/data/AdvancedTable";
|
export * from "./lib/data/AdvancedTable";
|
||||||
export * from "./lib/feedback/use-error-handler";
|
export * from "./lib/feedback/use-error-handler";
|
||||||
export * from "./lib/data/useServerTable";
|
export * from "./lib/data/useServerTable";
|
||||||
|
export * from "./lib/landing/LandingPage";
|
||||||
|
export * from "./lib/landing/landing-copy";
|
||||||
|
|||||||
589
libs/ui/src/lib/landing/LandingPage.tsx
Normal file
589
libs/ui/src/lib/landing/LandingPage.tsx
Normal file
@@ -0,0 +1,589 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
Anchor,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
SimpleGrid,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
Title,
|
||||||
|
useMantineTheme,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import {
|
||||||
|
IconAnchor,
|
||||||
|
IconAward,
|
||||||
|
IconBriefcase,
|
||||||
|
IconCertificate,
|
||||||
|
IconClipboardCheck,
|
||||||
|
IconFileCertificate,
|
||||||
|
IconGavel,
|
||||||
|
IconMail,
|
||||||
|
IconMapPin,
|
||||||
|
IconPhone,
|
||||||
|
IconSchool,
|
||||||
|
IconShip,
|
||||||
|
IconShieldCheck,
|
||||||
|
IconUserCheck,
|
||||||
|
IconUsers,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import { LanguageSwitcher } from '../layout/LanguageSwitcher';
|
||||||
|
import { ColorSchemeToggle } from '../layout/ColorSchemeToggle';
|
||||||
|
import { EmptyState } from '../feedback/EmptyState';
|
||||||
|
import './landing.css';
|
||||||
|
|
||||||
|
export interface LandingPageProps {
|
||||||
|
/** Primary CTA target. Each app passes '/dashboard' when a session token
|
||||||
|
* exists, else '/login'. Authenticated header state is derived from this
|
||||||
|
* (anything other than '/login' counts as signed in) rather than a
|
||||||
|
* separate prop, since libs/ui cannot import @ema-platform/auth. */
|
||||||
|
primaryHref: string;
|
||||||
|
/** Renders the "Create account" CTA only when set. Portal passes '/signup'; backoffice omits it (enableSignup: false). */
|
||||||
|
signupHref?: string;
|
||||||
|
/** Renders the Certificate Verification nav item + quick-access tile only when set. Portal passes '/verify'; backoffice omits it. */
|
||||||
|
verifyHref?: string;
|
||||||
|
supportedLanguages?: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const HEADER_HEIGHT = 72;
|
||||||
|
|
||||||
|
export function LandingPage({
|
||||||
|
primaryHref,
|
||||||
|
signupHref,
|
||||||
|
verifyHref,
|
||||||
|
supportedLanguages = ['en', 'am'],
|
||||||
|
}: LandingPageProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const isAuthed = primaryHref !== '/login';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="ema-landing" style={{ minHeight: '100dvh', background: 'var(--mantine-color-body)' }}>
|
||||||
|
<Anchor
|
||||||
|
href="#main"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 8,
|
||||||
|
top: -60,
|
||||||
|
zIndex: 1000,
|
||||||
|
padding: '10px 16px',
|
||||||
|
background: 'var(--mantine-color-body)',
|
||||||
|
border: '1px solid var(--mantine-color-default-border)',
|
||||||
|
borderRadius: 8,
|
||||||
|
transition: 'top 120ms ease',
|
||||||
|
}}
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.currentTarget.style.top = '8px';
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
e.currentTarget.style.top = '-60px';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('landing.meta.skipToContent')}
|
||||||
|
</Anchor>
|
||||||
|
|
||||||
|
<Header
|
||||||
|
isAuthed={isAuthed}
|
||||||
|
primaryHref={primaryHref}
|
||||||
|
signupHref={signupHref}
|
||||||
|
verifyHref={verifyHref}
|
||||||
|
supportedLanguages={supportedLanguages}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<main id="main">
|
||||||
|
<Hero primaryHref={primaryHref} isAuthed={isAuthed} />
|
||||||
|
<About />
|
||||||
|
<QuickAccess verifyHref={verifyHref} />
|
||||||
|
<Services />
|
||||||
|
<Roles />
|
||||||
|
<HowItWorks />
|
||||||
|
<Notices />
|
||||||
|
<Faq />
|
||||||
|
<Contact />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavAnchor({ href, children }: { href: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Anchor
|
||||||
|
href={href}
|
||||||
|
underline="never"
|
||||||
|
c="var(--mantine-color-text)"
|
||||||
|
fw={500}
|
||||||
|
fz="sm"
|
||||||
|
style={{ whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Anchor>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header({
|
||||||
|
isAuthed,
|
||||||
|
primaryHref,
|
||||||
|
signupHref,
|
||||||
|
verifyHref,
|
||||||
|
supportedLanguages,
|
||||||
|
}: {
|
||||||
|
isAuthed: boolean;
|
||||||
|
primaryHref: string;
|
||||||
|
signupHref?: string;
|
||||||
|
verifyHref?: string;
|
||||||
|
supportedLanguages: readonly string[];
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
component="header"
|
||||||
|
style={{
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 100,
|
||||||
|
height: HEADER_HEIGHT,
|
||||||
|
background: 'var(--mantine-color-body)',
|
||||||
|
borderBottom: '1px solid var(--mantine-color-default-border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container size="xl" h="100%">
|
||||||
|
<Group h="100%" justify="space-between" wrap="nowrap" gap="md">
|
||||||
|
<Anchor href="#main" underline="never" c="var(--mantine-color-text)">
|
||||||
|
<Group gap="xs" wrap="nowrap">
|
||||||
|
<Box component="img" src="/ema-logo.png" alt="" w={34} h={34} style={{ objectFit: 'contain' }} />
|
||||||
|
<Text fw={700} fz="sm" visibleFrom="sm">
|
||||||
|
{t('app.name')}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Anchor>
|
||||||
|
|
||||||
|
<Group gap="lg" wrap="nowrap" visibleFrom="md" component="nav" aria-label={t('landing.nav.home')}>
|
||||||
|
<NavAnchor href="#main">{t('landing.nav.home')}</NavAnchor>
|
||||||
|
<NavAnchor href="#about">{t('landing.nav.about')}</NavAnchor>
|
||||||
|
<NavAnchor href="#services">{t('landing.nav.services')}</NavAnchor>
|
||||||
|
<NavAnchor href="#notices">{t('landing.nav.notices')}</NavAnchor>
|
||||||
|
{verifyHref && (
|
||||||
|
<Anchor
|
||||||
|
component={Link}
|
||||||
|
to={verifyHref}
|
||||||
|
underline="never"
|
||||||
|
c="var(--mantine-color-text)"
|
||||||
|
fw={500}
|
||||||
|
fz="sm"
|
||||||
|
style={{ whiteSpace: 'nowrap' }}
|
||||||
|
>
|
||||||
|
{t('landing.nav.verify')}
|
||||||
|
</Anchor>
|
||||||
|
)}
|
||||||
|
<NavAnchor href="#contact">{t('landing.nav.contact')}</NavAnchor>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group gap="xs" wrap="nowrap">
|
||||||
|
<LanguageSwitcher supportedLanguages={supportedLanguages} />
|
||||||
|
<ColorSchemeToggle />
|
||||||
|
{isAuthed ? (
|
||||||
|
<Button component={Link} to={primaryHref} size="sm" visibleFrom="xs">
|
||||||
|
{t('landing.auth.dashboard')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button component={Link} to={primaryHref} variant="subtle" size="sm" visibleFrom="xs">
|
||||||
|
{t('landing.auth.login')}
|
||||||
|
</Button>
|
||||||
|
{signupHref && (
|
||||||
|
<Button component={Link} to={signupHref} size="sm" visibleFrom="xs">
|
||||||
|
{t('landing.auth.signup')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Hero({ primaryHref, isAuthed }: { primaryHref: string; isAuthed: boolean }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
background: theme.other?.heroGradient as string,
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container size="xl" py={80}>
|
||||||
|
<Stack gap="md" align="center" ta="center" className="ema-landing-fade">
|
||||||
|
<Text fz="sm" fw={600} style={{ opacity: 0.85, letterSpacing: 0.5 }}>
|
||||||
|
{t('landing.hero.eyebrow')}
|
||||||
|
</Text>
|
||||||
|
<Title order={1} fz={{ base: 32, sm: 44 }} maw={780}>
|
||||||
|
{t('landing.hero.title')}
|
||||||
|
</Title>
|
||||||
|
<Text fz={{ base: 'md', sm: 'lg' }} maw={640} style={{ opacity: 0.92 }}>
|
||||||
|
{t('landing.hero.subtitle')}
|
||||||
|
</Text>
|
||||||
|
<Group gap="md" mt="md" justify="center">
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to={isAuthed ? primaryHref : '/signup'}
|
||||||
|
size="md"
|
||||||
|
variant="white"
|
||||||
|
color="dark"
|
||||||
|
leftSection={<IconShip size={18} />}
|
||||||
|
>
|
||||||
|
{t('landing.cta.seafarerPortal')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to={isAuthed ? primaryHref : '/signup'}
|
||||||
|
size="md"
|
||||||
|
variant="outline"
|
||||||
|
color="white"
|
||||||
|
leftSection={<IconAnchor size={18} />}
|
||||||
|
>
|
||||||
|
{t('landing.cta.vesselOwnerPortal')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionHeading({ title, subtitle }: { title: string; subtitle?: string }) {
|
||||||
|
return (
|
||||||
|
<Stack gap={6} align="center" ta="center" mb="xl" maw={640} mx="auto">
|
||||||
|
<Title order={2} fz={{ base: 24, sm: 30 }}>
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
{subtitle && (
|
||||||
|
<Text c="dimmed" fz="md">
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function About() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Box id="about" component="section" py={72}>
|
||||||
|
<Container size="xl">
|
||||||
|
<SectionHeading title={t('landing.about.title')} />
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="xl">
|
||||||
|
<Paper withBorder radius="lg" p="xl">
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fw={700} c="var(--mantine-primary-color-filled)" tt="uppercase" fz="xs" style={{ letterSpacing: 0.5 }}>
|
||||||
|
{t('landing.about.visionLabel')}
|
||||||
|
</Text>
|
||||||
|
<Text>{t('landing.about.vision')}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
<Paper withBorder radius="lg" p="xl">
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fw={700} c="var(--mantine-primary-color-filled)" tt="uppercase" fz="xs" style={{ letterSpacing: 0.5 }}>
|
||||||
|
{t('landing.about.missionLabel')}
|
||||||
|
</Text>
|
||||||
|
<Text>{t('landing.about.mission')}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function QuickAccessCard({
|
||||||
|
icon: Icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
href,
|
||||||
|
}: {
|
||||||
|
icon: typeof IconCertificate;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
href?: string;
|
||||||
|
}) {
|
||||||
|
const body = (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<ThemeIcon size={44} radius="md" variant="light">
|
||||||
|
<Icon size={22} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={600}>{title}</Text>
|
||||||
|
<Text c="dimmed" fz="sm">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
const cardStyle = { display: 'block', height: '100%', textDecoration: 'none', color: 'inherit' } as const;
|
||||||
|
|
||||||
|
return href ? (
|
||||||
|
<Paper component={Link} to={href} withBorder radius="lg" p="lg" className="ema-landing-hover" style={cardStyle}>
|
||||||
|
{body}
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
<Paper withBorder radius="lg" p="lg" className="ema-landing-hover" style={cardStyle}>
|
||||||
|
{body}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function QuickAccess({ verifyHref }: { verifyHref?: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Box id="quick-access" component="section" py={72} style={{ background: 'var(--mantine-color-default-hover)' }}>
|
||||||
|
<Container size="xl">
|
||||||
|
<SectionHeading title={t('landing.quickAccess.title')} subtitle={t('landing.quickAccess.subtitle')} />
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="lg">
|
||||||
|
<QuickAccessCard
|
||||||
|
icon={IconFileCertificate}
|
||||||
|
title={t('landing.quickAccess.verify.title')}
|
||||||
|
description={t('landing.quickAccess.verify.description')}
|
||||||
|
href={verifyHref}
|
||||||
|
/>
|
||||||
|
<QuickAccessCard
|
||||||
|
icon={IconShip}
|
||||||
|
title={t('landing.quickAccess.vesselRegistration.title')}
|
||||||
|
description={t('landing.quickAccess.vesselRegistration.description')}
|
||||||
|
/>
|
||||||
|
<QuickAccessCard
|
||||||
|
icon={IconAnchor}
|
||||||
|
title={t('landing.quickAccess.notices.title')}
|
||||||
|
description={t('landing.quickAccess.notices.description')}
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SERVICE_ICONS = {
|
||||||
|
seafarerRegistration: IconUserCheck,
|
||||||
|
vesselRegistration: IconShip,
|
||||||
|
licensing: IconBriefcase,
|
||||||
|
examinations: IconSchool,
|
||||||
|
certificateVerification: IconCertificate,
|
||||||
|
waivers: IconShieldCheck,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function Services() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const items = Object.keys(SERVICE_ICONS) as (keyof typeof SERVICE_ICONS)[];
|
||||||
|
return (
|
||||||
|
<Box id="services" component="section" py={72}>
|
||||||
|
<Container size="xl">
|
||||||
|
<SectionHeading title={t('landing.services.title')} subtitle={t('landing.services.subtitle')} />
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
|
||||||
|
{items.map((key) => {
|
||||||
|
const Icon = SERVICE_ICONS[key];
|
||||||
|
return (
|
||||||
|
<Paper key={key} withBorder radius="lg" p="lg">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<ThemeIcon size={44} radius="md" variant="light">
|
||||||
|
<Icon size={22} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={600}>{t(`landing.services.items.${key}.title`)}</Text>
|
||||||
|
<Text c="dimmed" fz="sm">
|
||||||
|
{t(`landing.services.items.${key}.description`)}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROLE_ICONS = {
|
||||||
|
seafarers: IconShip,
|
||||||
|
vesselOwners: IconAnchor,
|
||||||
|
agents: IconBriefcase,
|
||||||
|
reviewers: IconGavel,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function Roles() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const items = Object.keys(ROLE_ICONS) as (keyof typeof ROLE_ICONS)[];
|
||||||
|
return (
|
||||||
|
<Box id="roles" component="section" py={72} style={{ background: 'var(--mantine-color-default-hover)' }}>
|
||||||
|
<Container size="xl">
|
||||||
|
<SectionHeading title={t('landing.roles.title')} subtitle={t('landing.roles.subtitle')} />
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="lg">
|
||||||
|
{items.map((key) => {
|
||||||
|
const Icon = ROLE_ICONS[key];
|
||||||
|
return (
|
||||||
|
<Paper key={key} withBorder radius="lg" p="lg" ta="center">
|
||||||
|
<Stack gap="sm" align="center">
|
||||||
|
<ThemeIcon size={48} radius="xl" variant="light">
|
||||||
|
<Icon size={24} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={600}>{t(`landing.roles.${key}.title`)}</Text>
|
||||||
|
<Text c="dimmed" fz="sm">
|
||||||
|
{t(`landing.roles.${key}.description`)}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const STEP_KEYS = ['selectRole', 'createAccount', 'submitApplication', 'trackStatus', 'receiveApproval'] as const;
|
||||||
|
const STEP_ICONS = [IconUsers, IconUserCheck, IconClipboardCheck, IconAward, IconCertificate];
|
||||||
|
|
||||||
|
function HowItWorks() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Box id="how-it-works" component="section" py={72}>
|
||||||
|
<Container size="xl">
|
||||||
|
<SectionHeading title={t('landing.howItWorks.title')} subtitle={t('landing.howItWorks.subtitle')} />
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 5 }} spacing="lg">
|
||||||
|
{STEP_KEYS.map((key, i) => {
|
||||||
|
const Icon = STEP_ICONS[i];
|
||||||
|
return (
|
||||||
|
<Stack key={key} gap="xs" align="center" ta="center">
|
||||||
|
<ThemeIcon size={48} radius="xl" variant="filled" color="var(--mantine-primary-color-filled)">
|
||||||
|
<Icon size={22} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={600} fz="sm">
|
||||||
|
{i + 1}. {t(`landing.howItWorks.steps.${key}`)}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Notices() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Box id="notices" component="section" py={72} style={{ background: 'var(--mantine-color-default-hover)' }}>
|
||||||
|
<Container size="xl">
|
||||||
|
<SectionHeading title={t('landing.notices.title')} subtitle={t('landing.notices.subtitle')} />
|
||||||
|
<Box maw={560} mx="auto">
|
||||||
|
<EmptyState
|
||||||
|
icon={IconAnchor}
|
||||||
|
title={t('landing.notices.emptyTitle')}
|
||||||
|
description={t('landing.notices.emptyDescription')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FAQ_KEYS = ['whatIsPortal', 'whoCanUse', 'howToApply', 'howToVerify', 'isFree'] as const;
|
||||||
|
|
||||||
|
function Faq() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Box component="section" py={72}>
|
||||||
|
<Container size="sm">
|
||||||
|
<SectionHeading title={t('landing.faq.title')} />
|
||||||
|
<Accordion variant="separated" radius="lg">
|
||||||
|
{FAQ_KEYS.map((key) => (
|
||||||
|
<Accordion.Item key={key} value={key}>
|
||||||
|
<Accordion.Control>{t(`landing.faq.items.${key}.question`)}</Accordion.Control>
|
||||||
|
<Accordion.Panel>{t(`landing.faq.items.${key}.answer`)}</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Contact() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Box id="contact" component="section" py={72} style={{ background: 'var(--mantine-color-default-hover)' }}>
|
||||||
|
<Container size="xl">
|
||||||
|
<SectionHeading title={t('landing.contact.title')} subtitle={t('landing.contact.subtitle')} />
|
||||||
|
<Paper withBorder radius="lg" p="xl" maw={560} mx="auto" component="address" style={{ fontStyle: 'normal' }}>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Group gap="md" wrap="nowrap" align="flex-start">
|
||||||
|
<ThemeIcon size={38} radius="md" variant="light">
|
||||||
|
<IconMapPin size={18} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text fw={600} fz="sm">
|
||||||
|
{t('landing.contact.addressLabel')}
|
||||||
|
</Text>
|
||||||
|
<Text c="dimmed" fz="sm">
|
||||||
|
{t('landing.contact.address')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
<Group gap="md" wrap="nowrap" align="flex-start">
|
||||||
|
<ThemeIcon size={38} radius="md" variant="light">
|
||||||
|
<IconPhone size={18} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text fw={600} fz="sm">
|
||||||
|
{t('landing.contact.phoneLabel')}
|
||||||
|
</Text>
|
||||||
|
<Anchor href="tel:+251115150299" fz="sm">
|
||||||
|
+251 011 515 0299
|
||||||
|
</Anchor>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
<Group gap="md" wrap="nowrap" align="flex-start">
|
||||||
|
<ThemeIcon size={38} radius="md" variant="light">
|
||||||
|
<IconMail size={18} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text fw={600} fz="sm">
|
||||||
|
{t('landing.contact.websiteLabel')}
|
||||||
|
</Text>
|
||||||
|
<Anchor href="https://etmaritime.com" target="_blank" rel="noreferrer" fz="sm">
|
||||||
|
etmaritime.com
|
||||||
|
</Anchor>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Footer() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Box component="footer" py="xl" style={{ borderTop: '1px solid var(--mantine-color-default-border)' }}>
|
||||||
|
<Container size="xl">
|
||||||
|
<Group justify="space-between" wrap="wrap" gap="sm">
|
||||||
|
<Group gap="xs">
|
||||||
|
<Box component="img" src="/ema-logo.png" alt="" w={24} h={24} style={{ objectFit: 'contain' }} />
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
{t('landing.hero.title')}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
© 2026 {t('landing.hero.title')} — {t('landing.footer.rights')}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
360
libs/ui/src/lib/landing/landing-copy.ts
Normal file
360
libs/ui/src/lib/landing/landing-copy.ts
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
/**
|
||||||
|
* Copy for the public landing page (`LandingPage.tsx`), shared by both apps.
|
||||||
|
* Written once here; each app's `en.ts`/`am.ts` re-exports it under the
|
||||||
|
* `landing` key so it flows through that app's own i18next instance.
|
||||||
|
*
|
||||||
|
* `landingAm: LandingCopy` enforces English/Amharic parity right here — a
|
||||||
|
* missing Amharic key is a compile error, same contract as each app's own
|
||||||
|
* `Translations = typeof en`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const landingEn = {
|
||||||
|
meta: {
|
||||||
|
skipToContent: 'Skip to content',
|
||||||
|
},
|
||||||
|
|
||||||
|
nav: {
|
||||||
|
home: 'Home',
|
||||||
|
about: 'About EMA',
|
||||||
|
services: 'Services',
|
||||||
|
notices: 'Maritime Notices',
|
||||||
|
verify: 'Certificate Verification',
|
||||||
|
contact: 'Contact',
|
||||||
|
},
|
||||||
|
|
||||||
|
auth: {
|
||||||
|
login: 'Login',
|
||||||
|
signup: 'Sign Up',
|
||||||
|
dashboard: 'Go to Dashboard',
|
||||||
|
},
|
||||||
|
|
||||||
|
hero: {
|
||||||
|
eyebrow: 'Federal Democratic Republic of Ethiopia',
|
||||||
|
title: 'Ethiopian Maritime Authority',
|
||||||
|
subtitle: 'Digital Maritime Services for Seafarers, Vessel Owners and Logistics Operators',
|
||||||
|
},
|
||||||
|
|
||||||
|
about: {
|
||||||
|
title: 'About EMA',
|
||||||
|
visionLabel: 'Vision',
|
||||||
|
vision:
|
||||||
|
'To make Ethiopia the leading logistics performer in Africa and one of the five supplying seafarer nations in the world by 2030.',
|
||||||
|
missionLabel: 'Mission',
|
||||||
|
mission:
|
||||||
|
"Transform Ethiopia's logistics system and unlock the blue economy — strengthening legal frameworks, building infrastructure, and ensuring vessel safety and seafarer qualification.",
|
||||||
|
},
|
||||||
|
|
||||||
|
cta: {
|
||||||
|
seafarerPortal: 'Seafarer Portal',
|
||||||
|
vesselOwnerPortal: 'Vessel Owner Portal',
|
||||||
|
},
|
||||||
|
|
||||||
|
quickAccess: {
|
||||||
|
title: 'Quick Access',
|
||||||
|
subtitle: 'The most requested services, one click away.',
|
||||||
|
verify: {
|
||||||
|
title: 'Certificate Verification',
|
||||||
|
description: 'Confirm a seafarer certificate or licence is genuine and current.',
|
||||||
|
},
|
||||||
|
vesselRegistration: {
|
||||||
|
title: 'Vessel Registration',
|
||||||
|
description: 'Register a vessel or transfer ownership.',
|
||||||
|
},
|
||||||
|
notices: {
|
||||||
|
title: 'Marine Notices',
|
||||||
|
description: 'Official notices to seafarers, owners and operators.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
services: {
|
||||||
|
title: 'Maritime Services',
|
||||||
|
subtitle: 'Every licence, certificate and registration EMA issues, in one digital system.',
|
||||||
|
items: {
|
||||||
|
seafarerRegistration: {
|
||||||
|
title: 'Seafarer Registration & Certification',
|
||||||
|
description:
|
||||||
|
"Register as a seafarer, apply for a Certificate of Competency or Proficiency, and manage your seaman's book online.",
|
||||||
|
},
|
||||||
|
vesselRegistration: {
|
||||||
|
title: 'Vessel Registration',
|
||||||
|
description:
|
||||||
|
'Register inland or sea-going vessels and manage ownership transfers with full document tracking.',
|
||||||
|
},
|
||||||
|
licensing: {
|
||||||
|
title: 'Operator Licensing',
|
||||||
|
description:
|
||||||
|
'Apply for freight forwarder, shipping agent, combined and multimodal transport operator licences.',
|
||||||
|
},
|
||||||
|
examinations: {
|
||||||
|
title: 'Examinations',
|
||||||
|
description: 'Sit competency examinations and track your results as part of certification.',
|
||||||
|
},
|
||||||
|
certificateVerification: {
|
||||||
|
title: 'Certificate Verification',
|
||||||
|
description: 'Let employers, port authorities and the public verify any EMA-issued certificate instantly.',
|
||||||
|
},
|
||||||
|
waivers: {
|
||||||
|
title: 'Waivers',
|
||||||
|
description: 'Apply for a maritime waiver where standard requirements do not apply.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
roles: {
|
||||||
|
title: 'Built for Every User',
|
||||||
|
subtitle: 'One portal, tailored to what you do.',
|
||||||
|
seafarers: {
|
||||||
|
title: 'Seafarers',
|
||||||
|
description: 'Register, certify, and manage your sea service records.',
|
||||||
|
},
|
||||||
|
vesselOwners: {
|
||||||
|
title: 'Vessel Owners',
|
||||||
|
description: 'Register vessels and manage ownership and licensing.',
|
||||||
|
},
|
||||||
|
agents: {
|
||||||
|
title: 'Agents & Logistics Operators',
|
||||||
|
description: 'Apply for and renew operator licences for freight and shipping.',
|
||||||
|
},
|
||||||
|
reviewers: {
|
||||||
|
title: 'EMA Reviewers',
|
||||||
|
description: 'Review, verify and approve applications from the backoffice.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
howItWorks: {
|
||||||
|
title: 'How It Works',
|
||||||
|
subtitle: 'From application to approval, in five steps.',
|
||||||
|
steps: {
|
||||||
|
selectRole: 'Select role',
|
||||||
|
createAccount: 'Create account',
|
||||||
|
submitApplication: 'Submit application',
|
||||||
|
trackStatus: 'Track status',
|
||||||
|
receiveApproval: 'Receive approval',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notices: {
|
||||||
|
title: 'Maritime Notices',
|
||||||
|
subtitle: 'Official notices to seafarers, vessel owners and operators.',
|
||||||
|
emptyTitle: 'No notices published yet',
|
||||||
|
emptyDescription: 'Check back here for official updates from EMA.',
|
||||||
|
},
|
||||||
|
|
||||||
|
faq: {
|
||||||
|
title: 'Frequently Asked Questions',
|
||||||
|
items: {
|
||||||
|
whatIsPortal: {
|
||||||
|
question: 'What is the EMA portal?',
|
||||||
|
answer:
|
||||||
|
"The EMA portal is the Ethiopian Maritime Authority's official digital system for seafarer registration, vessel registration, licensing, and certificate verification.",
|
||||||
|
},
|
||||||
|
whoCanUse: {
|
||||||
|
question: 'Who can use this portal?',
|
||||||
|
answer:
|
||||||
|
'Seafarers, vessel owners, shipping agents, freight forwarders and logistics operators can all apply for and manage their licences here.',
|
||||||
|
},
|
||||||
|
howToApply: {
|
||||||
|
question: 'How do I apply for a licence or certificate?',
|
||||||
|
answer:
|
||||||
|
'Create an account, select your role, and follow the application wizard for the licence or certificate you need. You can track its status at any time.',
|
||||||
|
},
|
||||||
|
howToVerify: {
|
||||||
|
question: 'How do I verify a certificate?',
|
||||||
|
answer:
|
||||||
|
'Use Certificate Verification and enter the certificate reference or scan its QR code. No account is required.',
|
||||||
|
},
|
||||||
|
isFree: {
|
||||||
|
question: 'Is registration free?',
|
||||||
|
answer:
|
||||||
|
'Creating a portal account is free. Statutory fees apply to specific licences and certificates, and are shown before you submit an application.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
contact: {
|
||||||
|
title: 'Contact EMA',
|
||||||
|
subtitle: 'Reach the Ethiopian Maritime Authority head office.',
|
||||||
|
addressLabel: 'Address',
|
||||||
|
address: 'Meskel Square, behind Hyatt Regency Hotel, Sunshine Building No. 4, Addis Ababa, Ethiopia',
|
||||||
|
phoneLabel: 'Phone',
|
||||||
|
websiteLabel: 'Website',
|
||||||
|
},
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
rights: 'All rights reserved.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LandingCopy = typeof landingEn;
|
||||||
|
|
||||||
|
export const landingAm: LandingCopy = {
|
||||||
|
meta: {
|
||||||
|
skipToContent: 'ወደ ዋናው ይዘት ዝለል',
|
||||||
|
},
|
||||||
|
|
||||||
|
nav: {
|
||||||
|
home: 'ዋና ገጽ',
|
||||||
|
about: 'ስለ ባለስልጣኑ',
|
||||||
|
services: 'አገልግሎቶች',
|
||||||
|
notices: 'የባህር ማስታወቂያዎች',
|
||||||
|
verify: 'የምስክር ወረቀት ማረጋገጫ',
|
||||||
|
contact: 'እኛን ያግኙ',
|
||||||
|
},
|
||||||
|
|
||||||
|
auth: {
|
||||||
|
login: 'ግባ',
|
||||||
|
signup: 'ይመዝገቡ',
|
||||||
|
dashboard: 'ወደ ዳሽቦርድ ይሂዱ',
|
||||||
|
},
|
||||||
|
|
||||||
|
hero: {
|
||||||
|
eyebrow: 'የኢትዮጵያ ፌዴራላዊ ዲሞክራሲያዊ ሪፐብሊክ',
|
||||||
|
title: 'የኢትዮጵያ ማሪታይም ባለስልጣን',
|
||||||
|
subtitle: 'ለመርከበኞች፣ ለመርከብ ባለቤቶች እና ለሎጂስቲክስ ኦፕሬተሮች የተዘጋጁ ዲጂታል የባህር አገልግሎቶች',
|
||||||
|
},
|
||||||
|
|
||||||
|
about: {
|
||||||
|
title: 'ስለ ባለስልጣኑ',
|
||||||
|
visionLabel: 'ራዕይ',
|
||||||
|
vision:
|
||||||
|
'በ2030 ኢትዮጵያ በአፍሪካ ግንባር ቀደም የሎጂስቲክስ አገልግሎት ሰጪ እንድትሆን፣ እንዲሁም ከዓለም አምስት መርከበኞችን ወደ ውጭ ከሚልኩ ሀገራት አንዷ እንድትሆን ማድረግ።',
|
||||||
|
missionLabel: 'ተልዕኮ',
|
||||||
|
mission:
|
||||||
|
'የኢትዮጵያን የሎጂስቲክስ ስርዓት መለወጥ እንዲሁም ከባህር ኢኮኖሚ ተጠቃሚ መሆን — በህግ ማዕቀፎች ማጠናከር፣ መሠረተ ልማት በመገንባትና የመርከብ ደህንነትንና የመርከበኛ ብቃትን በማረጋገጥ።',
|
||||||
|
},
|
||||||
|
|
||||||
|
cta: {
|
||||||
|
seafarerPortal: 'የመርከበኞች ፖርታል',
|
||||||
|
vesselOwnerPortal: 'የመርከብ ባለቤቶች ፖርታል',
|
||||||
|
},
|
||||||
|
|
||||||
|
quickAccess: {
|
||||||
|
title: 'ፈጣን መዳረሻ',
|
||||||
|
subtitle: 'በብዛት የሚፈለጉ አገልግሎቶች፣ በአንድ ቦታ።',
|
||||||
|
verify: {
|
||||||
|
title: 'የምስክር ወረቀት ማረጋገጫ',
|
||||||
|
description: 'የመርከበኛ ምስክር ወረቀት ወይም ፈቃድ ትክክለኛ እና የሚሰራ መሆኑን ያረጋግጡ።',
|
||||||
|
},
|
||||||
|
vesselRegistration: {
|
||||||
|
title: 'የመርከብ ምዝገባ',
|
||||||
|
description: 'መርከብ ይመዝገቡ ወይም ባለቤትነት ያስተላልፉ።',
|
||||||
|
},
|
||||||
|
notices: {
|
||||||
|
title: 'የባህር ማስታወቂያዎች',
|
||||||
|
description: 'ለመርከበኞች፣ ለመርከብ ባለቤቶችና ለኦፕሬተሮች የተሰጡ ማስታወቂያዎች።',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
services: {
|
||||||
|
title: 'የባህር አገልግሎቶች',
|
||||||
|
subtitle: 'ባለስልጣኑ የሚሰጣቸው ሁሉም ፈቃድ፣ ምስክር ወረቀትና ምዝገባ በአንድ ዲጂታል ስርዓት ውስጥ።',
|
||||||
|
items: {
|
||||||
|
seafarerRegistration: {
|
||||||
|
title: 'የመርከበኞች ምዝገባና ማረጋገጫ',
|
||||||
|
description:
|
||||||
|
'እንደ መርከበኛ ይመዝገቡ፣ ለብቃት ወይም ችሎታ ማረጋገጫ ምስክር ወረቀት ያመልክቱ፣ እንዲሁም የመርከበኛ መዝገብ መጽሐፍዎን በመስመር ላይ ያስተዳድሩ።',
|
||||||
|
},
|
||||||
|
vesselRegistration: {
|
||||||
|
title: 'የመርከብ ምዝገባ',
|
||||||
|
description: 'የውስጥ ውሃ ወይም የባህር ማዶ መርከቦችን ይመዝገቡ፣ የባለቤትነት ዝውውርንም ሙሉ በሙሉ በሰነድ ክትትል ያስተዳድሩ።',
|
||||||
|
},
|
||||||
|
licensing: {
|
||||||
|
title: 'የኦፕሬተር ፈቃድ',
|
||||||
|
description: 'ለጭነት አስተላላፊ፣ ለመርከብ ወኪል፣ ለተቀናጀ እና ለብዙ-ዘዴ ትራንስፖርት ኦፕሬተር ፈቃድ ያመልክቱ።',
|
||||||
|
},
|
||||||
|
examinations: {
|
||||||
|
title: 'ፈተናዎች',
|
||||||
|
description: 'የብቃት ፈተናዎችን ይውሰዱ እንዲሁም ውጤቶችዎን እንደ ማረጋገጫ ሂደት አካል ይከታተሉ።',
|
||||||
|
},
|
||||||
|
certificateVerification: {
|
||||||
|
title: 'የምስክር ወረቀት ማረጋገጫ',
|
||||||
|
description: 'ቀጣሪዎች፣ የወደብ ባለስልጣናትና ህዝብ ማንኛውንም በባለስልጣኑ የተሰጠ ምስክር ወረቀት ወዲያውኑ እንዲያረጋግጡ ያስችላል።',
|
||||||
|
},
|
||||||
|
waivers: {
|
||||||
|
title: 'ነፃ ፈቃዶች',
|
||||||
|
description: 'መደበኛ መስፈርቶች በማይሟሉበት ጊዜ ለባህር ትራንስፖርት ነፃ ፈቃድ ያመልክቱ።',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
roles: {
|
||||||
|
title: 'ለሁሉም ተጠቃሚ የተዘጋጀ',
|
||||||
|
subtitle: 'አንድ ፖርታል፣ ለሚናዎ የተዘጋጀ።',
|
||||||
|
seafarers: {
|
||||||
|
title: 'መርከበኞች',
|
||||||
|
description: 'ይመዝገቡ፣ ይረጋገጡ እንዲሁም የባህር አገልግሎት መዝገብዎን ያስተዳድሩ።',
|
||||||
|
},
|
||||||
|
vesselOwners: {
|
||||||
|
title: 'የመርከብ ባለቤቶች',
|
||||||
|
description: 'መርከብ ይመዝገቡ እንዲሁም ባለቤትነትና ፈቃድ ያስተዳድሩ።',
|
||||||
|
},
|
||||||
|
agents: {
|
||||||
|
title: 'ወኪሎችና ሎጂስቲክስ ኦፕሬተሮች',
|
||||||
|
description: 'ለጭነትና ለመላኪያ ፈቃድ ያመልክቱ እንዲሁም ያድሱ።',
|
||||||
|
},
|
||||||
|
reviewers: {
|
||||||
|
title: 'የባለስልጣኑ ገምጋሚዎች',
|
||||||
|
description: 'ማመልከቻዎችን ይገመግሙ፣ ያረጋግጡ እንዲሁም ይፍቀዱ።',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
howItWorks: {
|
||||||
|
title: 'እንዴት እንደሚሰራ',
|
||||||
|
subtitle: 'ከማመልከቻ እስከ ፈቃድ፣ በአምስት ደረጃዎች።',
|
||||||
|
steps: {
|
||||||
|
selectRole: 'ሚና ይምረጡ',
|
||||||
|
createAccount: 'መለያ ይፍጠሩ',
|
||||||
|
submitApplication: 'ማመልከቻ ያስገቡ',
|
||||||
|
trackStatus: 'ሁኔታ ይከታተሉ',
|
||||||
|
receiveApproval: 'ፍቃድ ይቀበሉ',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
notices: {
|
||||||
|
title: 'የባህር ማስታወቂያዎች',
|
||||||
|
subtitle: 'ለመርከበኞች፣ ለመርከብ ባለቤቶችና ለኦፕሬተሮች የተሰጡ ማስታወቂያዎች።',
|
||||||
|
emptyTitle: 'እስካሁን የወጣ ማስታወቂያ የለም',
|
||||||
|
emptyDescription: 'አዲስ መረጃ ሲኖር እዚህ ያገኙታል።',
|
||||||
|
},
|
||||||
|
|
||||||
|
faq: {
|
||||||
|
title: 'ተደጋጋሚ ጥያቄዎች',
|
||||||
|
items: {
|
||||||
|
whatIsPortal: {
|
||||||
|
question: 'EMA ፖርታል ምንድን ነው?',
|
||||||
|
answer:
|
||||||
|
'EMA ፖርታል የኢትዮጵያ ማሪታይም ባለስልጣን ለመርከበኛ ምዝገባ፣ ለመርከብ ምዝገባ፣ ፈቃድና ለምስክር ወረቀት ማረጋገጫ የሚጠቀምበት ዲጂታል ስርዓት ነው።',
|
||||||
|
},
|
||||||
|
whoCanUse: {
|
||||||
|
question: 'ይህን ፖርታል ማን ሊጠቀም ይችላል?',
|
||||||
|
answer: 'መርከበኞች፣ የመርከብ ባለቤቶች፣ ወኪሎችና ሎጂስቲክስ ኦፕሬተሮች ሁሉም እዚህ ፈቃዳቸውን ማመልከትና ማስተዳደር ይችላሉ።',
|
||||||
|
},
|
||||||
|
howToApply: {
|
||||||
|
question: 'ለፈቃድ ወይም ለምስክር ወረቀት እንዴት አመለክታለሁ?',
|
||||||
|
answer:
|
||||||
|
'መለያ ይፍጠሩ፣ ሚናዎን ይምረጡ፣ እንዲሁም የሚያስፈልግዎትን ፈቃድ ወይም ምስክር ወረቀት ደረጃዎች ይከተሉ። ሁኔታውን በማንኛውም ጊዜ መከታተል ይችላሉ።',
|
||||||
|
},
|
||||||
|
howToVerify: {
|
||||||
|
question: 'ምስክር ወረቀት እንዴት አረጋግጣለሁ?',
|
||||||
|
answer: 'የምስክር ወረቀት ማረጋገጫን ይክፈቱና የምስክር ወረቀቱን ቁጥር ያስገቡ ወይም QR ኮዱን ያንብቡ። መለያ አያስፈልግም።',
|
||||||
|
},
|
||||||
|
isFree: {
|
||||||
|
question: 'ምዝገባ ነፃ ነው?',
|
||||||
|
answer: 'የፖርታል መለያ መክፈት ነፃ ነው። ለተወሰኑ ፈቃዶችና ምስክር ወረቀቶች የመንግስት ክፍያ ይኖራል፣ ማመልከቻ ከማስገባትዎ በፊት ይታያል።',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
contact: {
|
||||||
|
title: 'ባለስልጣኑን ያግኙ',
|
||||||
|
subtitle: 'የኢትዮጵያ ማሪታይም ባለስልጣን ዋና መሥሪያ ቤትን ያግኙ።',
|
||||||
|
addressLabel: 'አድራሻ',
|
||||||
|
address: 'መስቀል አደባባይ፣ ከHyatt Regency ሆቴል ጀርባ፣ Sunshine ህንፃ ቁጥር 4፣ አዲስ አበባ፣ ኢትዮጵያ',
|
||||||
|
phoneLabel: 'ስልክ',
|
||||||
|
websiteLabel: 'ድረ ገጽ',
|
||||||
|
},
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
rights: 'ሁሉም መብቶች የተጠበቁ ናቸው።',
|
||||||
|
},
|
||||||
|
};
|
||||||
56
libs/ui/src/lib/landing/landing.css
Normal file
56
libs/ui/src/lib/landing/landing.css
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/* Public landing page — scoped under .ema-landing so nothing leaks into the
|
||||||
|
rest of either app. Neither app's global CSS (portal's .ema-page-enter /
|
||||||
|
.ema-hover-lift, portal's --ema-surface-*) is available here, so this file
|
||||||
|
is self-contained. */
|
||||||
|
|
||||||
|
.ema-landing {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ema-landing section[id] {
|
||||||
|
/* Offsets anchor jumps by the sticky header height so the heading isn't
|
||||||
|
hidden underneath it. Keep in sync with the header's fixed height. */
|
||||||
|
scroll-margin-top: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ema-landing .ema-landing-fade {
|
||||||
|
animation: ema-landing-fade-up 360ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ema-landing-fade-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ema-landing .ema-landing-hover {
|
||||||
|
transition:
|
||||||
|
transform 160ms ease,
|
||||||
|
box-shadow 160ms ease,
|
||||||
|
border-color 160ms ease;
|
||||||
|
}
|
||||||
|
.ema-landing .ema-landing-hover:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Amharic runs 20-40% longer than English and falls back to a different font
|
||||||
|
(Inter has no Ethiopic glyphs), so it needs more vertical room. */
|
||||||
|
.ema-landing:lang(am) {
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.ema-landing {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
.ema-landing .ema-landing-fade,
|
||||||
|
.ema-landing .ema-landing-hover {
|
||||||
|
animation: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,10 +27,10 @@ export function ColorSchemeToggle() {
|
|||||||
border: 'none',
|
border: 'none',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = 'var(--mantine-color-primary-light)';
|
e.currentTarget.style.background = 'var(--mantine-primary-color-light)';
|
||||||
e.currentTarget.style.color = 'var(--mantine-color-primary-6)';
|
e.currentTarget.style.color = 'var(--mantine-primary-color-6)';
|
||||||
e.currentTarget.style.boxShadow =
|
e.currentTarget.style.boxShadow =
|
||||||
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px var(--mantine-color-primary-2)';
|
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px var(--mantine-primary-color-2)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.background = 'var(--mantine-color-body)';
|
e.currentTarget.style.background = 'var(--mantine-color-body)';
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ export function LanguageSwitcher({ supportedLanguages, variant = 'icon' }: Langu
|
|||||||
border: 'none',
|
border: 'none',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = 'var(--mantine-color-primary-light)';
|
e.currentTarget.style.background = 'var(--mantine-primary-color-light)';
|
||||||
e.currentTarget.style.color = 'var(--mantine-color-primary-6)';
|
e.currentTarget.style.color = 'var(--mantine-primary-color-6)';
|
||||||
e.currentTarget.style.boxShadow =
|
e.currentTarget.style.boxShadow =
|
||||||
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px var(--mantine-color-primary-2)';
|
'0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px var(--mantine-primary-color-2)';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.background = 'var(--mantine-color-body)';
|
e.currentTarget.style.background = 'var(--mantine-color-body)';
|
||||||
|
|||||||
Reference in New Issue
Block a user