feat: remove certificate verification page and related API, update landing page links

This commit is contained in:
estifanos
2026-08-14 07:34:35 +00:00
parent d6b813b697
commit de04ed64b3
6 changed files with 153 additions and 286 deletions

View File

@@ -1,3 +1,4 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
@@ -16,13 +17,14 @@ import {
useMantineTheme,
} from '@mantine/core';
import {
IconActivity,
IconAnchor,
IconAward,
IconBriefcase,
IconCertificate,
IconClipboardCheck,
IconFileCertificate,
IconGavel,
IconLanguage,
IconMail,
IconMapPin,
IconPhone,
@@ -30,11 +32,11 @@ import {
IconShip,
IconShieldCheck,
IconUserCheck,
IconUserCircle,
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 {
@@ -45,17 +47,46 @@ export interface LandingPageProps {
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;
// Sections with a nav item, in page order — drives the scrollspy underline.
const NAV_SECTION_IDS = ['home', 'about', 'services', 'system', 'contact'] as const;
/** Tracks which nav section is currently under the sticky header, so the
* header can underline it. Native IntersectionObserver — no scroll listener. */
function useActiveSection(ids: readonly string[]) {
const [active, setActive] = useState<string>(ids[0]);
useEffect(() => {
const elements = ids
.map((id) => document.getElementById(id))
.filter((el): el is HTMLElement => el !== null);
if (!elements.length) return;
const observer = new IntersectionObserver(
(entries) => {
const visible = entries.filter((entry) => entry.isIntersecting);
if (visible.length === 0) return;
const topMost = visible.reduce((a, b) => (a.boundingClientRect.top <= b.boundingClientRect.top ? a : b));
setActive(topMost.target.id);
},
// Counts a section only once it has cleared the sticky header, and only
// while it's still in the top 30% of the viewport.
{ rootMargin: `-${HEADER_HEIGHT}px 0px -70% 0px`, threshold: 0 },
);
elements.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, [ids]);
return active;
}
export function LandingPage({
primaryHref,
signupHref,
verifyHref,
supportedLanguages = ['en', 'am'],
}: LandingPageProps) {
const { t } = useTranslation();
@@ -90,18 +121,17 @@ export function LandingPage({
isAuthed={isAuthed}
primaryHref={primaryHref}
signupHref={signupHref}
verifyHref={verifyHref}
supportedLanguages={supportedLanguages}
/>
<main id="main">
<Hero primaryHref={primaryHref} isAuthed={isAuthed} />
<About />
<QuickAccess verifyHref={verifyHref} />
<QuickAccess />
<Services />
<Roles />
<HowItWorks />
<Notices />
<SystemHighlights />
<Faq />
<Contact />
</main>
@@ -111,15 +141,29 @@ export function LandingPage({
);
}
function NavAnchor({ href, children }: { href: string; children: React.ReactNode }) {
function NavAnchor({
href,
active,
children,
}: {
href: string;
active?: boolean;
children: React.ReactNode;
}) {
return (
<Anchor
href={href}
underline="never"
c="var(--mantine-color-text)"
aria-current={active ? 'true' : undefined}
c={active ? 'var(--mantine-primary-color-filled)' : 'var(--mantine-color-text)'}
fw={500}
fz="sm"
style={{ whiteSpace: 'nowrap' }}
style={{
whiteSpace: 'nowrap',
paddingBottom: 4,
borderBottom: active ? '2px solid var(--mantine-primary-color-filled)' : '2px solid transparent',
transition: 'color 120ms ease, border-color 120ms ease',
}}
>
{children}
</Anchor>
@@ -130,16 +174,15 @@ function Header({
isAuthed,
primaryHref,
signupHref,
verifyHref,
supportedLanguages,
}: {
isAuthed: boolean;
primaryHref: string;
signupHref?: string;
verifyHref?: string;
supportedLanguages: readonly string[];
}) {
const { t } = useTranslation();
const activeSection = useActiveSection(NAV_SECTION_IDS);
return (
<Box
@@ -155,7 +198,7 @@ function Header({
>
<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)">
<Anchor href="#home" 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">
@@ -165,24 +208,21 @@ function Header({
</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>
<NavAnchor href="#home" active={activeSection === 'home'}>
{t('landing.nav.home')}
</NavAnchor>
<NavAnchor href="#about" active={activeSection === 'about'}>
{t('landing.nav.about')}
</NavAnchor>
<NavAnchor href="#services" active={activeSection === 'services'}>
{t('landing.nav.services')}
</NavAnchor>
<NavAnchor href="#system" active={activeSection === 'system'}>
{t('landing.nav.system')}
</NavAnchor>
<NavAnchor href="#contact" active={activeSection === 'contact'}>
{t('landing.nav.contact')}
</NavAnchor>
</Group>
<Group gap="xs" wrap="nowrap">
@@ -217,6 +257,8 @@ function Hero({ primaryHref, isAuthed }: { primaryHref: string; isAuthed: boolea
return (
<Box
id="home"
component="section"
style={{
background: theme.other?.heroGradient as string,
color: 'white',
@@ -340,19 +382,13 @@ function QuickAccessCard({
);
}
function QuickAccess({ verifyHref }: { verifyHref?: string }) {
function QuickAccess() {
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}
/>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
<QuickAccessCard
icon={IconShip}
title={t('landing.quickAccess.vesselRegistration.title')}
@@ -374,7 +410,6 @@ const SERVICE_ICONS = {
vesselRegistration: IconShip,
licensing: IconBriefcase,
examinations: IconSchool,
certificateVerification: IconCertificate,
waivers: IconShieldCheck,
} as const;
@@ -474,25 +509,44 @@ function HowItWorks() {
);
}
function Notices() {
const SYSTEM_ICONS = {
secure: IconShieldCheck,
bilingual: IconLanguage,
tracking: IconActivity,
singleAccount: IconUserCircle,
} as const;
function SystemHighlights() {
const { t } = useTranslation();
const items = Object.keys(SYSTEM_ICONS) as (keyof typeof SYSTEM_ICONS)[];
return (
<Box id="notices" component="section" py={72} style={{ background: 'var(--mantine-color-default-hover)' }}>
<Box id="system" 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>
<SectionHeading title={t('landing.system.title')} subtitle={t('landing.system.subtitle')} />
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="lg">
{items.map((key) => {
const Icon = SYSTEM_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.system.items.${key}.title`)}</Text>
<Text c="dimmed" fz="sm">
{t(`landing.system.items.${key}.description`)}
</Text>
</Stack>
</Paper>
);
})}
</SimpleGrid>
</Container>
</Box>
);
}
const FAQ_KEYS = ['whatIsPortal', 'whoCanUse', 'howToApply', 'howToVerify', 'isFree'] as const;
const FAQ_KEYS = ['whatIsPortal', 'whoCanUse', 'howToApply', 'isFree'] as const;
function Faq() {
const { t } = useTranslation();

View File

@@ -17,8 +17,7 @@ export const landingEn = {
home: 'Home',
about: 'About EMA',
services: 'Services',
notices: 'Maritime Notices',
verify: 'Certificate Verification',
system: 'Our System',
contact: 'Contact',
},
@@ -52,10 +51,6 @@ export const landingEn = {
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.',
@@ -89,10 +84,6 @@ export const landingEn = {
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.',
@@ -133,11 +124,27 @@ export const landingEn = {
},
},
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.',
system: {
title: 'A Modern Digital System',
subtitle: 'Built to make maritime services faster, safer and accessible from anywhere.',
items: {
secure: {
title: 'Secure & Verified',
description: 'Every application and certificate is digitally recorded and verifiable.',
},
bilingual: {
title: 'Bilingual by Design',
description: 'Use the system fully in English or Amharic — switch anytime.',
},
tracking: {
title: 'Real-Time Tracking',
description: 'Follow your application from submission to approval, step by step.',
},
singleAccount: {
title: 'One Account, Every Service',
description: 'Register once and access all EMA licensing and certification services.',
},
},
},
faq: {
@@ -146,7 +153,7 @@ export const landingEn = {
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.",
"The EMA portal is the Ethiopian Maritime Authority's official digital system for seafarer registration, vessel registration and licensing.",
},
whoCanUse: {
question: 'Who can use this portal?',
@@ -158,11 +165,6 @@ export const landingEn = {
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:
@@ -196,8 +198,7 @@ export const landingAm: LandingCopy = {
home: 'ዋና ገጽ',
about: 'ስለ ባለስልጣኑ',
services: 'አገልግሎቶች',
notices: 'የባህር ማስታወቂያዎች',
verify: 'የምስክር ወረቀት ማረጋገጫ',
system: 'ስርዓታችን',
contact: 'እኛን ያግኙ',
},
@@ -231,10 +232,6 @@ export const landingAm: LandingCopy = {
quickAccess: {
title: 'ፈጣን መዳረሻ',
subtitle: 'በብዛት የሚፈለጉ አገልግሎቶች፣ በአንድ ቦታ።',
verify: {
title: 'የምስክር ወረቀት ማረጋገጫ',
description: 'የመርከበኛ ምስክር ወረቀት ወይም ፈቃድ ትክክለኛ እና የሚሰራ መሆኑን ያረጋግጡ።',
},
vesselRegistration: {
title: 'የመርከብ ምዝገባ',
description: 'መርከብ ይመዝገቡ ወይም ባለቤትነት ያስተላልፉ።',
@@ -266,10 +263,6 @@ export const landingAm: LandingCopy = {
title: 'ፈተናዎች',
description: 'የብቃት ፈተናዎችን ይውሰዱ እንዲሁም ውጤቶችዎን እንደ ማረጋገጫ ሂደት አካል ይከታተሉ።',
},
certificateVerification: {
title: 'የምስክር ወረቀት ማረጋገጫ',
description: 'ቀጣሪዎች፣ የወደብ ባለስልጣናትና ህዝብ ማንኛውንም በባለስልጣኑ የተሰጠ ምስክር ወረቀት ወዲያውኑ እንዲያረጋግጡ ያስችላል።',
},
waivers: {
title: 'ነፃ ፈቃዶች',
description: 'መደበኛ መስፈርቶች በማይሟሉበት ጊዜ ለባህር ትራንስፖርት ነፃ ፈቃድ ያመልክቱ።',
@@ -310,11 +303,27 @@ export const landingAm: LandingCopy = {
},
},
notices: {
title: 'የባህር ማስታወቂያዎች',
subtitle: 'ለመርከበኞች፣ ለመርከብ ባለቤቶችና ለኦፕሬተሮች የተሰጡ ማስታወቂያዎች።',
emptyTitle: 'እስካሁን የወጣ ማስታወቂያ የለም',
emptyDescription: 'አዲስ መረጃ ሲኖር እዚህ ያገኙታል።',
system: {
title: 'ዘመናዊ ዲጂታል ስርዓት',
subtitle: 'የባህር አገልግሎቶችን ፈጣን፣ ደህንነቱ የተጠበቀና ከየትኛውም ቦታ ተደራሽ ለማድረግ የተዘጋጀ።',
items: {
secure: {
title: 'ደህንነቱ የተጠበቀ እና የተረጋገጠ',
description: 'እያንዳንዱ ማመልከቻና ምስክር ወረቀት በዲጂታል መልኩ ተመዝግቦ ሊረጋገጥ የሚችል ነው።',
},
bilingual: {
title: 'በሁለት ቋንቋ የተዘጋጀ',
description: 'ስርዓቱን ሙሉ በሙሉ በአማርኛ ወይም በእንግሊዝኛ ይጠቀሙ — በማንኛውም ጊዜ ይቀይሩ።',
},
tracking: {
title: 'የቀጥታ ሁኔታ ክትትል',
description: 'ማመልከቻዎን ከማስገባት እስከ ማጽደቅ ደረጃ በደረጃ ይከታተሉ።',
},
singleAccount: {
title: 'አንድ መለያ፣ ሁሉም አገልግሎት',
description: 'አንድ ጊዜ ይመዝገቡና ሁሉንም የEMA ፈቃድና የምስክር ወረቀት አገልግሎቶች ይድረሱ።',
},
},
},
faq: {
@@ -323,7 +332,7 @@ export const landingAm: LandingCopy = {
whatIsPortal: {
question: 'EMA ፖርታል ምንድን ነው?',
answer:
'EMA ፖርታል የኢትዮጵያ ማሪታይም ባለስልጣን ለመርከበኛ ምዝገባ፣ ለመርከብ ምዝገባ ፈቃድና ለምስክር ወረቀት ማረጋገጫ የሚጠቀምበት ዲጂታል ስርዓት ነው።',
'EMA ፖርታል የኢትዮጵያ ማሪታይም ባለስልጣን ለመርከበኛ ምዝገባ፣ ለመርከብ ምዝገባ ፈቃድ የሚጠቀምበት ዲጂታል ስርዓት ነው።',
},
whoCanUse: {
question: 'ይህን ፖርታል ማን ሊጠቀም ይችላል?',
@@ -334,10 +343,6 @@ export const landingAm: LandingCopy = {
answer:
'መለያ ይፍጠሩ፣ ሚናዎን ይምረጡ፣ እንዲሁም የሚያስፈልግዎትን ፈቃድ ወይም ምስክር ወረቀት ደረጃዎች ይከተሉ። ሁኔታውን በማንኛውም ጊዜ መከታተል ይችላሉ።',
},
howToVerify: {
question: 'ምስክር ወረቀት እንዴት አረጋግጣለሁ?',
answer: 'የምስክር ወረቀት ማረጋገጫን ይክፈቱና የምስክር ወረቀቱን ቁጥር ያስገቡ ወይም QR ኮዱን ያንብቡ። መለያ አያስፈልግም።',
},
isFree: {
question: 'ምዝገባ ነፃ ነው?',
answer: 'የፖርታል መለያ መክፈት ነፃ ነው። ለተወሰኑ ፈቃዶችና ምስክር ወረቀቶች የመንግስት ክፍያ ይኖራል፣ ማመልከቻ ከማስገባትዎ በፊት ይታያል።',