diff --git a/apps/portal/src/app/components/LandingRoute.tsx b/apps/portal/src/app/components/LandingRoute.tsx index 16f7050c4..39f419253 100644 --- a/apps/portal/src/app/components/LandingRoute.tsx +++ b/apps/portal/src/app/components/LandingRoute.tsx @@ -11,5 +11,5 @@ import { authStorage } from '@ema-platform/auth'; export function LandingRoute() { const token = authStorage.getToken() ?? Cookies.get('auth-token'); - return ; + return ; } diff --git a/apps/portal/src/app/features/verify/pages/VerifyCertificatePage.tsx b/apps/portal/src/app/features/verify/pages/VerifyCertificatePage.tsx deleted file mode 100644 index ed3ffd906..000000000 --- a/apps/portal/src/app/features/verify/pages/VerifyCertificatePage.tsx +++ /dev/null @@ -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 ( - - - - - EMA Certificate Verification - - - {!code && ( - - - - Scan the QR code on a certificate, or enter its verification - code below. - - - setManualCode(e.currentTarget.value)} - /> - - - - - )} - - {code && isLoading && ( -
- -
- )} - - {code && !isLoading && (isError || !data) && ( - - - Verification is temporarily unavailable. Please try again. - - - )} - - {code && data && ( - - - {data.valid ? ( - - -
- - Valid certificate - - - Issued by the Ethiopian Maritime Authority. - -
-
- ) : ( - - -
- - - Not valid - - {data.status && {data.status}} - - - {data.reason === 'not_found' - ? 'No certificate matches this code.' - : 'This certificate is no longer valid.'} - -
-
- )} - - {rows.length > 0 && ( - - - {rows - .filter(([, value]) => value) - .map(([label, value]) => ( - - {label} - {value} - - ))} - -
- )} - - -
-
- )} -
-
- ); -} diff --git a/apps/portal/src/app/router.tsx b/apps/portal/src/app/router.tsx index d58a12a16..7ab630b08 100644 --- a/apps/portal/src/app/router.tsx +++ b/apps/portal/src/app/router.tsx @@ -29,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"; // Phase 1 pages @@ -64,11 +63,6 @@ export const router = createBrowserRouter([ { path: "/login", element: }, { path: "/signup", element: }, - // Public certificate verification — the target of every printed QR code. - // No auth: a verifier scanning a certificate has no portal account. - { path: "/verify", element: }, - { path: "/verify/:code", element: }, - // 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. diff --git a/libs/api/src/lib/features/licensing/licensing-api.ts b/libs/api/src/lib/features/licensing/licensing-api.ts index 07e2a4c54..485277781 100644 --- a/libs/api/src/lib/features/licensing/licensing-api.ts +++ b/libs/api/src/lib/features/licensing/licensing-api.ts @@ -308,27 +308,6 @@ export const licensingApi = baseApi }), // ------------------------------------------------------------ licences - /** - * Public QR-code verification (US-PORTAL-008). No auth required — - * the endpoint returns only what a verifier needs to trust the - * document, never the holder's contact details. - */ - verifyCertificate: builder.query< - { - valid: boolean; - reason?: string; - certificateNumber?: string; - licenseType?: string | null; - companyName?: string | null; - issueDate?: string; - expiryDate?: string; - status?: string; - }, - string - >({ - query: (code) => ({ url: `/licenses/verify/${code}` }), - }), - /** The licence register for enforcement officers. */ getLicenses: builder.query< Paginated, @@ -769,7 +748,6 @@ export const licensingApi = baseApi }); export const { - useVerifyCertificateQuery, useGetLicenseTypesQuery, useGetLicenseCategoriesQuery, useUpdateLicenseFeesMutation, diff --git a/libs/ui/src/lib/landing/LandingPage.tsx b/libs/ui/src/lib/landing/LandingPage.tsx index f746244c3..57516f753 100644 --- a/libs/ui/src/lib/landing/LandingPage.tsx +++ b/libs/ui/src/lib/landing/LandingPage.tsx @@ -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(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} />
- + - +
@@ -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 ( {children} @@ -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 ( - + @@ -165,24 +208,21 @@ function Header({ - {t('landing.nav.home')} - {t('landing.nav.about')} - {t('landing.nav.services')} - {t('landing.nav.notices')} - {verifyHref && ( - - {t('landing.nav.verify')} - - )} - {t('landing.nav.contact')} + + {t('landing.nav.home')} + + + {t('landing.nav.about')} + + + {t('landing.nav.services')} + + + {t('landing.nav.system')} + + + {t('landing.nav.contact')} + @@ -217,6 +257,8 @@ function Hero({ primaryHref, isAuthed }: { primaryHref: string; isAuthed: boolea return ( - - + + - - - - + + + {items.map((key) => { + const Icon = SYSTEM_ICONS[key]; + return ( + + + + + + {t(`landing.system.items.${key}.title`)} + + {t(`landing.system.items.${key}.description`)} + + + + ); + })} + ); } -const FAQ_KEYS = ['whatIsPortal', 'whoCanUse', 'howToApply', 'howToVerify', 'isFree'] as const; +const FAQ_KEYS = ['whatIsPortal', 'whoCanUse', 'howToApply', 'isFree'] as const; function Faq() { const { t } = useTranslation(); diff --git a/libs/ui/src/lib/landing/landing-copy.ts b/libs/ui/src/lib/landing/landing-copy.ts index 8b17e1b36..522a36d88 100644 --- a/libs/ui/src/lib/landing/landing-copy.ts +++ b/libs/ui/src/lib/landing/landing-copy.ts @@ -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: 'የፖርታል መለያ መክፈት ነፃ ነው። ለተወሰኑ ፈቃዶችና ምስክር ወረቀቶች የመንግስት ክፍያ ይኖራል፣ ማመልከቻ ከማስገባትዎ በፊት ይታያል።',