From 7c79f4d644d848d6c9c27957abc6048925fe618c Mon Sep 17 00:00:00 2001 From: estifanos Date: Fri, 14 Aug 2026 09:27:37 +0000 Subject: [PATCH 01/58] feat: implement useAuthToken hook for consistent auth token retrieval across components.comment from nati --- apps/backoffice/src/app/router/LandingRoute.tsx | 5 ++--- apps/portal/src/app/components/LandingRoute.tsx | 11 +++++------ libs/auth/src/index.ts | 1 + libs/auth/src/lib/components/ProtectedRoute.tsx | 5 ++--- libs/auth/src/lib/hooks/useAuthToken.ts | 15 +++++++++++++++ 5 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 libs/auth/src/lib/hooks/useAuthToken.ts diff --git a/apps/backoffice/src/app/router/LandingRoute.tsx b/apps/backoffice/src/app/router/LandingRoute.tsx index 776bf01f8..c353e91d5 100644 --- a/apps/backoffice/src/app/router/LandingRoute.tsx +++ b/apps/backoffice/src/app/router/LandingRoute.tsx @@ -1,13 +1,12 @@ -import Cookies from 'js-cookie'; import { LandingPage } from '@ema-platform/ui'; -import { authStorage } from '@ema-platform/auth'; +import { useAuthToken } 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'); + const token = useAuthToken(); return ; } diff --git a/apps/portal/src/app/components/LandingRoute.tsx b/apps/portal/src/app/components/LandingRoute.tsx index 39f419253..8bab1d394 100644 --- a/apps/portal/src/app/components/LandingRoute.tsx +++ b/apps/portal/src/app/components/LandingRoute.tsx @@ -1,15 +1,14 @@ -import Cookies from 'js-cookie'; import { LandingPage } from '@ema-platform/ui'; -import { authStorage } from '@ema-platform/auth'; +import { useAuthToken } 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. + * Auth state is read via `useAuthToken`, the same hook ProtectedRoute uses, + * 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'); + const token = useAuthToken(); return ; } diff --git a/libs/auth/src/index.ts b/libs/auth/src/index.ts index 886e79d3b..a0f46f25f 100644 --- a/libs/auth/src/index.ts +++ b/libs/auth/src/index.ts @@ -25,6 +25,7 @@ export { resetSignup, } from "./lib/store/signup.slice"; export { usePermissions } from "./lib/hooks/usePermissions"; +export { useAuthToken } from "./lib/hooks/useAuthToken"; export type { PermissionSet } from "./lib/hooks/usePermissions"; export { RequirePermission } from "./lib/components/RequirePermission"; export { diff --git a/libs/auth/src/lib/components/ProtectedRoute.tsx b/libs/auth/src/lib/components/ProtectedRoute.tsx index 9c55d85ee..5321a5889 100644 --- a/libs/auth/src/lib/components/ProtectedRoute.tsx +++ b/libs/auth/src/lib/components/ProtectedRoute.tsx @@ -1,7 +1,6 @@ -import Cookies from 'js-cookie'; import { Navigate, Outlet, useLocation } from 'react-router-dom'; import type { ReactNode } from 'react'; -import { authStorage } from '../utils/auth-storage'; +import { useAuthToken } from '../hooks/useAuthToken'; interface ProtectedRouteProps { children?: ReactNode; @@ -10,7 +9,7 @@ interface ProtectedRouteProps { export function ProtectedRoute({ children, loginPath = '/login' }: ProtectedRouteProps) { const location = useLocation(); - const token = authStorage.getToken() ?? Cookies.get('auth-token'); + const token = useAuthToken(); if (!token) { return ; diff --git a/libs/auth/src/lib/hooks/useAuthToken.ts b/libs/auth/src/lib/hooks/useAuthToken.ts new file mode 100644 index 000000000..c6b75bdb9 --- /dev/null +++ b/libs/auth/src/lib/hooks/useAuthToken.ts @@ -0,0 +1,15 @@ +import { useSelector } from 'react-redux'; +import { authStorage } from '../utils/auth-storage'; +import type { AuthState } from '../types/auth.types'; + +/** + * The current auth token, preferring Redux so components re-render on login + * and logout. Falls back to storage for the one case Redux misses: both + * `hydrateAuth` and the stores' `preloadedState` only populate `auth.token` + * when a token *and* a cached user are present, so a session with a token but + * no cached user would otherwise read as signed out. + */ +export function useAuthToken(): string | undefined { + const token = useSelector((state: { auth: AuthState }) => state.auth.token); + return token ?? authStorage.getToken(); +} From f31e75c3def822c745cd713ff2fd49116b71fcf0 Mon Sep 17 00:00:00 2001 From: estifanos Date: Fri, 14 Aug 2026 11:35:34 +0000 Subject: [PATCH 02/58] feat: implement PageLoader component and register maritime loader as the default theme loader --- apps/backoffice/index.html | 67 +++++++++++++++++++ .../pages/SeafarerRegistryPage/index.tsx | 4 +- .../VesselRegistrationQueuePage/index.tsx | 2 +- .../app/providers/MantineThemeProvider.tsx | 7 +- apps/portal/index.html | 60 +++++++++++++++++ .../licensing/components/DocumentSlots.tsx | 2 +- .../licensing/components/StaffEvidence.tsx | 2 +- .../components/OperationsFormContent.tsx | 2 +- .../profile/components/ProfileFormContent.tsx | 7 +- .../seafarer/pages/MySeaRecordsPage/index.tsx | 2 +- .../app/providers/MantineThemeProvider.tsx | 7 +- .../auth/src/lib/components/AuthBootstrap.tsx | 3 +- libs/ui/src/index.ts | 3 + libs/ui/src/lib/components/MaritimeLoader.tsx | 30 +++------ libs/ui/src/lib/feedback/PageLoader.tsx | 26 +++++++ .../ui/src/lib/theme/maritime-loader-theme.ts | 31 +++++++++ 16 files changed, 219 insertions(+), 36 deletions(-) create mode 100644 libs/ui/src/lib/feedback/PageLoader.tsx create mode 100644 libs/ui/src/lib/theme/maritime-loader-theme.ts diff --git a/apps/backoffice/index.html b/apps/backoffice/index.html index 8d1d8072c..f473aea4c 100644 --- a/apps/backoffice/index.html +++ b/apps/backoffice/index.html @@ -6,9 +6,76 @@ EMA Backoffice + +
+ +
+ +
diff --git a/apps/backoffice/src/app/features/seafarer-registry/pages/SeafarerRegistryPage/index.tsx b/apps/backoffice/src/app/features/seafarer-registry/pages/SeafarerRegistryPage/index.tsx index c39f5c95f..34f6b3dab 100644 --- a/apps/backoffice/src/app/features/seafarer-registry/pages/SeafarerRegistryPage/index.tsx +++ b/apps/backoffice/src/app/features/seafarer-registry/pages/SeafarerRegistryPage/index.tsx @@ -137,7 +137,7 @@ function SeafarerDetailDrawer({ {loadingSea ? ( - + ) : (seaService ?? []).length === 0 ? ( {t('seafarerRegistry.drawer.noSeaService', 'No sea-service records.')} @@ -184,7 +184,7 @@ function SeafarerDetailDrawer({ {loadingMedical ? ( - + ) : (medical ?? []).length === 0 ? ( {t('seafarerRegistry.drawer.noMedical', 'No medical certificates.')} diff --git a/apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationQueuePage/index.tsx b/apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationQueuePage/index.tsx index 2df4cd9a6..f7a504651 100644 --- a/apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationQueuePage/index.tsx +++ b/apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationQueuePage/index.tsx @@ -109,7 +109,7 @@ function VesselDetailDrawer({ {loadingIncidents ? ( - + ) : (incidents ?? []).length === 0 ? ( No incidents recorded. diff --git a/apps/backoffice/src/app/providers/MantineThemeProvider.tsx b/apps/backoffice/src/app/providers/MantineThemeProvider.tsx index 54bd52447..b28c844c0 100644 --- a/apps/backoffice/src/app/providers/MantineThemeProvider.tsx +++ b/apps/backoffice/src/app/providers/MantineThemeProvider.tsx @@ -1,11 +1,14 @@ -import { MantineProvider } from '@mantine/core'; +import { MantineProvider, mergeThemeOverrides } from '@mantine/core'; import { Notifications } from '@mantine/notifications'; import { emaTheme } from '@ema-platform/shared'; +import { maritimeLoaderTheme } from '@ema-platform/ui'; import type { ReactNode } from 'react'; +const theme = mergeThemeOverrides(emaTheme, maritimeLoaderTheme); + export function MantineThemeProvider({ children }: { children: ReactNode }) { return ( - + {children} diff --git a/apps/portal/index.html b/apps/portal/index.html index aaa28d481..66a56efb0 100644 --- a/apps/portal/index.html +++ b/apps/portal/index.html @@ -13,9 +13,69 @@ document.documentElement.setAttribute('data-mantine-color-scheme', s); } catch (e) {} +
+ +
+ +
diff --git a/apps/portal/src/app/features/licensing/components/DocumentSlots.tsx b/apps/portal/src/app/features/licensing/components/DocumentSlots.tsx index 2f28c6635..85a36d046 100644 --- a/apps/portal/src/app/features/licensing/components/DocumentSlots.tsx +++ b/apps/portal/src/app/features/licensing/components/DocumentSlots.tsx @@ -175,7 +175,7 @@ export function DocumentSlots({ variant={uploaded ? 'light' : 'filled'} leftSection={ busy === requirement.key ? ( - + ) : ( ) diff --git a/apps/portal/src/app/features/licensing/components/StaffEvidence.tsx b/apps/portal/src/app/features/licensing/components/StaffEvidence.tsx index 8ef08a415..672b1cf73 100644 --- a/apps/portal/src/app/features/licensing/components/StaffEvidence.tsx +++ b/apps/portal/src/app/features/licensing/components/StaffEvidence.tsx @@ -74,7 +74,7 @@ export function StaffEvidence({ staffId, evidence, readOnly, onUploaded }: Props disabled={readOnly || busy === item.docKey} leftSection={ busy === item.docKey ? ( - + ) : uploaded ? ( ) : undefined diff --git a/apps/portal/src/app/features/profile/components/OperationsFormContent.tsx b/apps/portal/src/app/features/profile/components/OperationsFormContent.tsx index 75699d5fb..63247b262 100644 --- a/apps/portal/src/app/features/profile/components/OperationsFormContent.tsx +++ b/apps/portal/src/app/features/profile/components/OperationsFormContent.tsx @@ -92,7 +92,7 @@ export function OperationsFormContent({ } if (loadingTypes || loadingMine) { - return ; + return ; } const removedNames = options diff --git a/apps/portal/src/app/features/profile/components/ProfileFormContent.tsx b/apps/portal/src/app/features/profile/components/ProfileFormContent.tsx index 41323e747..4540554a9 100644 --- a/apps/portal/src/app/features/profile/components/ProfileFormContent.tsx +++ b/apps/portal/src/app/features/profile/components/ProfileFormContent.tsx @@ -1,6 +1,7 @@ import { Loader, Select, SimpleGrid, TextInput } from '@mantine/core'; import type { FieldErrors, UseFormRegister, UseFormSetValue, UseFormWatch, UseFormTrigger } from 'react-hook-form'; import { z } from 'zod'; +import { useTranslation } from 'react-i18next'; import { AmharicDatePicker } from '@ema-platform/ui'; export const profileSchema = z.object({ @@ -39,11 +40,13 @@ export function ProfileFormContent({ professionsLoading, professionOptions, }: ProfileFormContentProps) { + const { t } = useTranslation(); + return (