From d6b813b6970fda3f94afd37591dfc3d7589fc0f2 Mon Sep 17 00:00:00 2001 From: estifanos Date: Fri, 14 Aug 2026 07:20:42 +0000 Subject: [PATCH 01/23] 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. --- apps/backoffice/src/app/i18n/locales/am.ts | 3 + apps/backoffice/src/app/i18n/locales/en.ts | 4 + .../src/app/router/LandingRoute.tsx | 13 + apps/backoffice/src/app/router/index.tsx | 4 +- apps/backoffice/src/main.tsx | 2 +- .../src/app/components/LandingRoute.tsx | 15 + apps/portal/src/app/i18n/locales/am.ts | 3 + apps/portal/src/app/i18n/locales/en.ts | 4 + .../portal/src/app/providers/AppProviders.tsx | 10 +- apps/portal/src/app/router.tsx | 23 +- libs/ui/src/index.ts | 2 + libs/ui/src/lib/landing/LandingPage.tsx | 589 ++++++++++++++++++ libs/ui/src/lib/landing/landing-copy.ts | 360 +++++++++++ libs/ui/src/lib/landing/landing.css | 56 ++ libs/ui/src/lib/layout/ColorSchemeToggle.tsx | 6 +- libs/ui/src/lib/layout/LanguageSwitcher.tsx | 6 +- 16 files changed, 1077 insertions(+), 23 deletions(-) create mode 100644 apps/backoffice/src/app/router/LandingRoute.tsx create mode 100644 apps/portal/src/app/components/LandingRoute.tsx create mode 100644 libs/ui/src/lib/landing/LandingPage.tsx create mode 100644 libs/ui/src/lib/landing/landing-copy.ts create mode 100644 libs/ui/src/lib/landing/landing.css diff --git a/apps/backoffice/src/app/i18n/locales/am.ts b/apps/backoffice/src/app/i18n/locales/am.ts index 92400f9ae..da2e6234b 100644 --- a/apps/backoffice/src/app/i18n/locales/am.ts +++ b/apps/backoffice/src/app/i18n/locales/am.ts @@ -1,6 +1,9 @@ +import { landingAm } from "@ema-platform/ui"; import type { Translations } from "./en"; export const am: Translations = { + landing: landingAm, + app: { name: "ኢማ አስተዳደር", shortName: "ኢማ", diff --git a/apps/backoffice/src/app/i18n/locales/en.ts b/apps/backoffice/src/app/i18n/locales/en.ts index 338a88e68..9b51214c2 100644 --- a/apps/backoffice/src/app/i18n/locales/en.ts +++ b/apps/backoffice/src/app/i18n/locales/en.ts @@ -1,4 +1,8 @@ +import { landingEn } from '@ema-platform/ui'; + export const en = { + landing: landingEn, + app: { name: 'EMA Admin', shortName: 'EMA', diff --git a/apps/backoffice/src/app/router/LandingRoute.tsx b/apps/backoffice/src/app/router/LandingRoute.tsx new file mode 100644 index 000000000..776bf01f8 --- /dev/null +++ b/apps/backoffice/src/app/router/LandingRoute.tsx @@ -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 ; +} diff --git a/apps/backoffice/src/app/router/index.tsx b/apps/backoffice/src/app/router/index.tsx index a5a2c5c5b..e04280c63 100644 --- a/apps/backoffice/src/app/router/index.tsx +++ b/apps/backoffice/src/app/router/index.tsx @@ -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: }, - { path: '/', element: }, + { path: '/', element: }, { path: '/profile-setup', element: }, { element: , @@ -69,7 +70,6 @@ const router = createBrowserRouter([ { element: , children: [ - { index: true, element: }, { path: 'dashboard', element: }, { path: 'vessel-registration-head-dashboard', element: guard([P.VIEW_VESSEL_REGISTRY], ) }, { path: 'logistics-head-dashboard', element: guard(APPLICATION_QUEUE, ) }, diff --git a/apps/backoffice/src/main.tsx b/apps/backoffice/src/main.tsx index 5c3165aa8..5d991e0df 100644 --- a/apps/backoffice/src/main.tsx +++ b/apps/backoffice/src/main.tsx @@ -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', diff --git a/apps/portal/src/app/components/LandingRoute.tsx b/apps/portal/src/app/components/LandingRoute.tsx new file mode 100644 index 000000000..16f7050c4 --- /dev/null +++ b/apps/portal/src/app/components/LandingRoute.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/i18n/locales/am.ts b/apps/portal/src/app/i18n/locales/am.ts index 5de71b937..4a0bea10f 100644 --- a/apps/portal/src/app/i18n/locales/am.ts +++ b/apps/portal/src/app/i18n/locales/am.ts @@ -1,6 +1,9 @@ +import { landingAm } from '@ema-platform/ui'; import type { Translations } from './en'; export const am: Translations = { + landing: landingAm, + app: { name: 'ኢባባ ፖርታል', authority: 'የኢትዮጵያ ባሕር ጉዳዮች ባለሥልጣን', diff --git a/apps/portal/src/app/i18n/locales/en.ts b/apps/portal/src/app/i18n/locales/en.ts index f05f83645..175d3918e 100644 --- a/apps/portal/src/app/i18n/locales/en.ts +++ b/apps/portal/src/app/i18n/locales/en.ts @@ -1,4 +1,8 @@ +import { landingEn } from '@ema-platform/ui'; + export const en = { + landing: landingEn, + app: { name: 'EMA Portal', authority: 'Ethiopian Maritime Authority', diff --git a/apps/portal/src/app/providers/AppProviders.tsx b/apps/portal/src/app/providers/AppProviders.tsx index f21fab6b8..886fd7138 100644 --- a/apps/portal/src/app/providers/AppProviders.tsx +++ b/apps/portal/src/app/providers/AppProviders.tsx @@ -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, }} > - - {children} - + + + {children} + + diff --git a/apps/portal/src/app/router.tsx b/apps/portal/src/app/router.tsx index e6ec53024..d58a12a16 100644 --- a/apps/portal/src/app/router.tsx +++ b/apps/portal/src/app/router.tsx @@ -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 { @@ -58,6 +57,9 @@ 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: }, + // Public auth pages { path: "/login", element: }, { path: "/signup", element: }, @@ -100,17 +102,14 @@ export const router = createBrowserRouter([ { element: ( - - {/* Asks what the applicant operates as before the rest of the - portal, which is filtered by that answer. */} - - - - + {/* Asks what the applicant operates as before the rest of the + portal, which is filtered by that answer. */} + + + ), children: [ - { path: "/", element: }, { path: "/dashboard", element: }, { path: "/onboarding/operations", element: }, @@ -409,5 +408,7 @@ export const router = createBrowserRouter([ element: , }, - { path: "*", element: }, + // 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: }, ]); diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index e713c8ee3..17f6e386a 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -21,3 +21,5 @@ export * from "./lib/input/phone"; export * from "./lib/data/AdvancedTable"; export * from "./lib/feedback/use-error-handler"; export * from "./lib/data/useServerTable"; +export * from "./lib/landing/LandingPage"; +export * from "./lib/landing/landing-copy"; diff --git a/libs/ui/src/lib/landing/LandingPage.tsx b/libs/ui/src/lib/landing/LandingPage.tsx new file mode 100644 index 000000000..f746244c3 --- /dev/null +++ b/libs/ui/src/lib/landing/LandingPage.tsx @@ -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 ( + + { + e.currentTarget.style.top = '8px'; + }} + onBlur={(e) => { + e.currentTarget.style.top = '-60px'; + }} + > + {t('landing.meta.skipToContent')} + + +
+ +
+ + + + + + + + + +
+ +