mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
user managment ui
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -23,3 +23,8 @@ coverage/
|
|||||||
branch_structure.json
|
branch_structure.json
|
||||||
temp_auto_push.bat
|
temp_auto_push.bat
|
||||||
temp_interactive_push.bat
|
temp_interactive_push.bat
|
||||||
|
.nx
|
||||||
|
|
||||||
|
apps/backoffice/public/_um/
|
||||||
|
apps/backoffice/public/tinymce/
|
||||||
|
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "user-management"]
|
||||||
|
path = user-management
|
||||||
|
url = git@github.com:Tria-plc/iamui.git
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same-origin host for the user-management module.
|
||||||
|
*
|
||||||
|
* The host app (React 19 / Mantine 8 / Tailwind 3) embeds the module (React 18 /
|
||||||
|
* Mantine 7 / Tailwind 4) via an iframe so the two never share a React tree,
|
||||||
|
* router, or CSS — the version mismatch is fully isolated by the document
|
||||||
|
* boundary. The module is built into apps/backoffice/public/_um and served by
|
||||||
|
* THIS same server at <origin>/_um/, so there is no second server and no second
|
||||||
|
* port. Override the mount path with VITE_USER_MANAGEMENT_BASE (default /_um).
|
||||||
|
*
|
||||||
|
* SSO: the module and host authenticate against the SAME backend, so the host's
|
||||||
|
* token is valid in the module. The module posts `UM_REQUEST_AUTH`; we reply with
|
||||||
|
* our stored token. Route-sync mirrors the module's internal route into the host
|
||||||
|
* URL (/um/<path>) so a refresh deep-links back to the selected menu.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function readToken(): string | null {
|
||||||
|
return (
|
||||||
|
localStorage.getItem('fhc-backoffice-auth-token') ??
|
||||||
|
(() => {
|
||||||
|
const escaped = 'auth-token'.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1');
|
||||||
|
const match = document.cookie.match(new RegExp('(?:^|; )' + escaped + '=([^;]*)'));
|
||||||
|
return match ? decodeURIComponent(match[1]) : null;
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRefreshToken(): string | null {
|
||||||
|
return localStorage.getItem('fhc-backoffice-auth-refresh-token') ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserManagementHostPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
|
||||||
|
// Same-origin sub-path the module is served from (matches the module's Vite
|
||||||
|
// `base` + the apps/backoffice/public/_um build). Same origin ⇒ no second port.
|
||||||
|
const mountBase = (
|
||||||
|
(import.meta.env.VITE_USER_MANAGEMENT_BASE as string | undefined) ?? '/_um'
|
||||||
|
).replace(/\/$/, '');
|
||||||
|
const moduleOrigin = window.location.origin;
|
||||||
|
|
||||||
|
// Deep-link: the host route is /um/*, so whatever follows /um is the module's
|
||||||
|
// own route. Compute src ONCE (frozen) so later parent-URL updates don't reload.
|
||||||
|
const [iframeSrc] = useState(() => {
|
||||||
|
const sub = location.pathname.replace(/^\/um(?=\/|$)/, '');
|
||||||
|
return mountBase + sub + location.search;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onMessage = (event: MessageEvent) => {
|
||||||
|
if (event.origin !== moduleOrigin) return;
|
||||||
|
const data = event.data as { type?: string; path?: string } | undefined;
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
if (data.type === 'UM_REQUEST_AUTH') {
|
||||||
|
const token = readToken();
|
||||||
|
const refreshToken = readRefreshToken();
|
||||||
|
const target = iframeRef.current?.contentWindow;
|
||||||
|
if (token && target) {
|
||||||
|
target.postMessage({ type: 'UM_AUTH_TOKEN', token, refreshToken }, moduleOrigin);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'UM_ROUTE_CHANGED' && typeof data.path === 'string') {
|
||||||
|
const target = '/um' + data.path;
|
||||||
|
if (window.location.pathname + window.location.search !== target) {
|
||||||
|
navigate(target, { replace: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', onMessage);
|
||||||
|
return () => window.removeEventListener('message', onMessage);
|
||||||
|
}, [moduleOrigin, navigate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'fixed', inset: 0 }}>
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
title="User Management"
|
||||||
|
src={iframeSrc}
|
||||||
|
style={{ width: '100%', height: '100%', border: 0, display: 'block' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
|
# The user-management module is a self-contained SPA built into /_um. Deep links
|
||||||
|
# / refreshes under /_um must fall back to the MODULE's own index.html, not the
|
||||||
|
# host's. Longest-prefix match wins, so this takes precedence over `location /`.
|
||||||
|
# Mirrors the dev SPA-fallback plugin in apps/backoffice/vite.config.mts.
|
||||||
|
location /_um/ {
|
||||||
|
try_files $uri $uri/ /_um/index.html;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,16 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"backoffice": "nx serve @ema-platform/backoffice",
|
"backoffice": "npm run build:user-management && nx serve @fhc-platform/backoffice",
|
||||||
"portal": "nx serve @ema-platform/portal",
|
"portal": "nx serve @ema-platform/portal",
|
||||||
"dev:all": "nx run-many -t serve -p @ema-platform/portal @ema-platform/backoffice --parallel=2",
|
"dev:all": "nx run-many -t serve -p @ema-platform/portal @ema-platform/backoffice --parallel=2",
|
||||||
"build:backoffice": "nx build @ema-platform/backoffice",
|
"build:backoffice": "nx build @ema-platform/backoffice",
|
||||||
"build:portal": "nx build @ema-platform/portal",
|
"build:portal": "nx build @ema-platform/portal",
|
||||||
"lint": "nx run-many -t lint",
|
"lint": "nx run-many -t lint",
|
||||||
"test": "nx run-many -t test",
|
"test": "nx run-many -t test",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write .",
|
||||||
|
"build:user-management": "cd user-management-config && npm run build",
|
||||||
|
"backoffice:no-build": "nx serve @fhc-platform/backoffice"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
|
|||||||
1
user-management
Submodule
1
user-management
Submodule
Submodule user-management added at 3f09900200
223
user-management-config/fhc.theme.ts
Normal file
223
user-management-config/fhc.theme.ts
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* fhc.theme.ts — Federal Housing Corporation (FHC) look & feel preset.
|
||||||
|
*
|
||||||
|
* ┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
* │ HOST-OWNED config. Lives in app-config/, NOT inside the user-management │
|
||||||
|
* │ module. At submodule-split time this whole folder moves to the host repo. │
|
||||||
|
* │ It is fully self-contained — no imports from the module. │
|
||||||
|
* └─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*
|
||||||
|
* WHAT IT GIVES YOU
|
||||||
|
* - The FHC Mantine color palettes: fhcBlue, fhcBrick, fhcGold, fhcGray
|
||||||
|
* - The FHC layout design tokens (brick-gradient sidebar, glassy header,
|
||||||
|
* page background, brand colors, sizes) under `theme.other.fhcLayout`
|
||||||
|
* (light) and `theme.other.fhcLayoutDark` (dark) — the classic shell reads
|
||||||
|
* these via useFhcLayout()
|
||||||
|
* - FHC typography (Plus Jakarta Sans), radii, shadows and component defaults
|
||||||
|
*
|
||||||
|
* HOW TO USE — in app-config/project.theme.ts:
|
||||||
|
*
|
||||||
|
* import { fhcMantineTheme } from "./fhc.theme";
|
||||||
|
*
|
||||||
|
* export const projectTheme: DesignConfig = {
|
||||||
|
* typography: { fontFamily: "Plus Jakarta Sans, sans-serif" },
|
||||||
|
* mantineTheme: fhcMantineTheme, // escape hatch — merges the FHC theme in
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* Load the font once in index.html:
|
||||||
|
* <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800;900&display=swap" rel="stylesheet" />
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { MantineColorsTuple, MantineThemeOverride } from "@mantine/core";
|
||||||
|
|
||||||
|
/** Mantine 10-shade color scales used across the FHC UI. */
|
||||||
|
export const FHC_COLORS = {
|
||||||
|
fhcBlue: [
|
||||||
|
"#EEF4FC",
|
||||||
|
"#D9E8FA",
|
||||||
|
"#BCD5F5",
|
||||||
|
"#96BDEB",
|
||||||
|
"#6FA4E0",
|
||||||
|
"#4A90E2",
|
||||||
|
"#357ABD",
|
||||||
|
"#2C669D",
|
||||||
|
"#224F7A",
|
||||||
|
"#173654",
|
||||||
|
],
|
||||||
|
fhcBrick: [
|
||||||
|
"#F6ECE8",
|
||||||
|
"#EACFC4",
|
||||||
|
"#DBAD99",
|
||||||
|
"#C9876B",
|
||||||
|
"#B86B49",
|
||||||
|
"#A85735",
|
||||||
|
"#8C462B",
|
||||||
|
"#703622",
|
||||||
|
"#55281A",
|
||||||
|
"#3D1E14",
|
||||||
|
],
|
||||||
|
fhcGold: [
|
||||||
|
"#FFFBE6",
|
||||||
|
"#FFF3BF",
|
||||||
|
"#FEE98A",
|
||||||
|
"#FCDD57",
|
||||||
|
"#F9CF2F",
|
||||||
|
"#FFD700",
|
||||||
|
"#D9B700",
|
||||||
|
"#B39400",
|
||||||
|
"#8C7300",
|
||||||
|
"#665300",
|
||||||
|
],
|
||||||
|
fhcGray: [
|
||||||
|
"#F8FAFC",
|
||||||
|
"#F1F5F9",
|
||||||
|
"#E2E8F0",
|
||||||
|
"#CBD5E1",
|
||||||
|
"#94A3B8",
|
||||||
|
"#64748B",
|
||||||
|
"#475569",
|
||||||
|
"#334155",
|
||||||
|
"#1E293B",
|
||||||
|
"#0F172A",
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout design tokens — the brick-gradient sidebar, glassy header, page
|
||||||
|
* surfaces, brand colors and sizes. Mirrored under `theme.other.fhcLayout`.
|
||||||
|
*/
|
||||||
|
export const FHC_LAYOUT = {
|
||||||
|
sidebar: {
|
||||||
|
bg: "linear-gradient(180deg, #5D2E1F 0%, #3D1E14 100%)",
|
||||||
|
headerBg: "rgba(93, 46, 31, 0.82)",
|
||||||
|
footerBg: "rgba(61, 30, 20, 0.62)",
|
||||||
|
border: "rgba(255,255,255,0.10)",
|
||||||
|
text: "rgba(255,255,255,0.76)",
|
||||||
|
mutedText: "rgba(255,255,255,0.42)",
|
||||||
|
childText: "rgba(255,255,255,0.68)",
|
||||||
|
activeText: "#FFFFFF",
|
||||||
|
iconBg: "rgba(255,255,255,0.06)",
|
||||||
|
iconActiveBg: "rgba(255,255,255,0.12)",
|
||||||
|
hoverBg: "rgba(255,255,255,0.08)",
|
||||||
|
activeBg: "rgba(255,255,255,0.15)",
|
||||||
|
activeBorder: "rgba(255,255,255,0.14)",
|
||||||
|
sectionLine: "rgba(255,255,255,0.10)",
|
||||||
|
rail: "linear-gradient(180deg, #FFD700 0%, #4A90E2 100%)",
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
bg: "rgba(255,255,255,0.92)",
|
||||||
|
border: "rgba(15, 23, 42, 0.08)",
|
||||||
|
searchBg: "#F9FAFB",
|
||||||
|
searchBorder: "#E5E7EB",
|
||||||
|
title: "#1F2937",
|
||||||
|
subtitle: "#6B7280",
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
bg: "#F8FAFC",
|
||||||
|
cardBg: "rgba(255,255,255,0.92)",
|
||||||
|
},
|
||||||
|
brand: {
|
||||||
|
brick: "#5D2E1F",
|
||||||
|
brickDark: "#3D1E14",
|
||||||
|
blue: "#4A90E2",
|
||||||
|
blueDark: "#357ABD",
|
||||||
|
gold: "#FFD700",
|
||||||
|
text: "#1F2937",
|
||||||
|
},
|
||||||
|
sizes: {
|
||||||
|
sidebarExpanded: 288,
|
||||||
|
sidebarCollapsed: 80,
|
||||||
|
headerHeight: 64,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dark-mode counterpart of FHC_LAYOUT. The brick-gradient sidebar, accent rail
|
||||||
|
* and sizes are intentionally kept (they already read well on dark), while the
|
||||||
|
* glassy white header, page background, card surfaces and dark text are flipped
|
||||||
|
* to dark equivalents.
|
||||||
|
*/
|
||||||
|
export const FHC_LAYOUT_DARK = {
|
||||||
|
...FHC_LAYOUT,
|
||||||
|
header: {
|
||||||
|
bg: "rgba(26, 27, 30, 0.92)",
|
||||||
|
border: "rgba(255,255,255,0.08)",
|
||||||
|
searchBg: "#25262B",
|
||||||
|
searchBorder: "#2C2E33",
|
||||||
|
title: "#F1F5F9",
|
||||||
|
subtitle: "#9CA3AF",
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
bg: "#141517",
|
||||||
|
cardBg: "rgba(26, 27, 30, 0.92)",
|
||||||
|
},
|
||||||
|
brand: {
|
||||||
|
...FHC_LAYOUT.brand,
|
||||||
|
text: "#F1F5F9",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full Mantine theme override carrying the FHC palettes, layout tokens,
|
||||||
|
* typography, radii, shadows and component defaults. Pass this as the
|
||||||
|
* `mantineTheme` escape hatch in project.theme.ts.
|
||||||
|
*
|
||||||
|
* Note: BOTH `fhcLayout` (light) and `fhcLayoutDark` (dark) are published under
|
||||||
|
* `other` — the module's useFhcLayout() reads the matching one per color scheme.
|
||||||
|
*/
|
||||||
|
export const fhcMantineTheme: MantineThemeOverride = {
|
||||||
|
fontFamily: "Plus Jakarta Sans, sans-serif",
|
||||||
|
headings: {
|
||||||
|
fontFamily: "Plus Jakarta Sans, sans-serif",
|
||||||
|
},
|
||||||
|
defaultRadius: "md",
|
||||||
|
radius: {
|
||||||
|
xs: "6px",
|
||||||
|
sm: "8px",
|
||||||
|
md: "10px",
|
||||||
|
lg: "14px",
|
||||||
|
xl: "18px",
|
||||||
|
},
|
||||||
|
shadows: {
|
||||||
|
xs: "0 1px 2px rgba(15, 23, 42, 0.04)",
|
||||||
|
sm: "0 2px 8px rgba(15, 23, 42, 0.06)",
|
||||||
|
md: "0 4px 20px rgba(15, 23, 42, 0.08)",
|
||||||
|
lg: "0 8px 30px rgba(15, 23, 42, 0.12)",
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
fhcBlue: FHC_COLORS.fhcBlue as unknown as MantineColorsTuple,
|
||||||
|
fhcBrick: FHC_COLORS.fhcBrick as unknown as MantineColorsTuple,
|
||||||
|
fhcGold: FHC_COLORS.fhcGold as unknown as MantineColorsTuple,
|
||||||
|
fhcGray: FHC_COLORS.fhcGray as unknown as MantineColorsTuple,
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
fhcLayout: FHC_LAYOUT,
|
||||||
|
fhcLayoutDark: FHC_LAYOUT_DARK,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Paper: {
|
||||||
|
defaultProps: {
|
||||||
|
radius: "lg",
|
||||||
|
shadow: "sm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NavLink: {
|
||||||
|
defaultProps: {
|
||||||
|
radius: "md",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional convenience: the bits of a DesignConfig that carry the FHC look.
|
||||||
|
* Spread this into your projectTheme if you also want FHC as the primary brand
|
||||||
|
* (this re-tints buttons/links to fhcBlue). Leave it out to keep your own brand
|
||||||
|
* color while still getting the fhc* palettes + layout tokens via `mantineTheme`.
|
||||||
|
*/
|
||||||
|
export const fhcDesignPreset = {
|
||||||
|
colors: { primary: "#357ABD" },
|
||||||
|
typography: { fontFamily: "Plus Jakarta Sans, sans-serif" },
|
||||||
|
shape: { radius: "10px" },
|
||||||
|
mantineTheme: fhcMantineTheme,
|
||||||
|
};
|
||||||
16
user-management-config/index.html
Normal file
16
user-management-config/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no"
|
||||||
|
/>
|
||||||
|
<title>User Management</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
user-management-config/main.tsx
Normal file
13
user-management-config/main.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
// Consume the reusable module via its public barrel (@/ → ../user-management/src).
|
||||||
|
import { UserManagementApp } from "@/index";
|
||||||
|
// Your project's config lives HERE in the host folder (resolved via @app-config).
|
||||||
|
// The module never imports it; the host passes it in.
|
||||||
|
import { projectTheme } from "@app-config/project.theme";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<UserManagementApp config={projectTheme} />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
1
user-management-config/node_modules
Symbolic link
1
user-management-config/node_modules
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../user-management/node_modules
|
||||||
12
user-management-config/package.json
Normal file
12
user-management-config/package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "user-management-host",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"description": "Host wrapper for the user-management module. Owns branding/theme (project.theme.ts / fhc.theme.ts), the Vite build (vite.config.ts), the HTML shell (index.html) and the entry (main.tsx). Consumes the module from ../user-management/src.",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port 4202 --host 0.0.0.0",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview --port 4202 --host"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
user-management-config/postcss.config.js
Normal file
7
user-management-config/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// Tailwind is handled by the @tailwindcss/vite plugin (see vite.config.ts), so
|
||||||
|
// PostCSS needs no plugins here. This local config exists to stop Vite from
|
||||||
|
// walking up to the monorepo root postcss.config.js (Tailwind v3), which would
|
||||||
|
// conflict with this package's Tailwind v4 setup.
|
||||||
|
export default {
|
||||||
|
plugins: {},
|
||||||
|
};
|
||||||
216
user-management-config/project.theme.ts
Normal file
216
user-management-config/project.theme.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
/**
|
||||||
|
* project.theme.ts — HOST-OWNED config for THIS project / organisation (FHC).
|
||||||
|
*
|
||||||
|
* ┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
|
* │ Lives in app-config/, OUTSIDE the user-management module. The module │
|
||||||
|
* │ never imports this file — the host passes it in via │
|
||||||
|
* │ <UserManagementApp config={projectTheme} /> (see ../src/main.tsx). │
|
||||||
|
* │ At submodule-split time, this whole folder moves to the host repo. │
|
||||||
|
* └─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
*
|
||||||
|
* Every field is optional — remove lines you don't need to override.
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* project.theme.ts → design.config.ts (module engine) → CSS vars + Mantine theme
|
||||||
|
* TenantConfig.ts → overrides --primary at runtime per hostname
|
||||||
|
*
|
||||||
|
* The TenantConfig layer runs AFTER this, so per-hostname primary-color overrides
|
||||||
|
* still work on top of whatever you set here.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { DesignConfig } from "@/config/design.config";
|
||||||
|
import { fhcMantineTheme } from "./fhc.theme";
|
||||||
|
|
||||||
|
export const projectTheme: DesignConfig = {
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// BRANDING
|
||||||
|
// Replace with your organisation's assets.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
brand: {
|
||||||
|
appName: "Federal Housing Corporation", // FHC — shown in the browser tab
|
||||||
|
// Drop the FHC logo at this path in /public to show it in the sidebar brand
|
||||||
|
// and as the favicon. Until then the sidebar falls back to a building icon.
|
||||||
|
// logoUrl: "/assets/logo/fhc.png",
|
||||||
|
// faviconUrl: "/favicon.ico", // optional — defaults to logoUrl
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// COLORS
|
||||||
|
// Change `primary` to your brand hex and everything cascades automatically.
|
||||||
|
// Shades primary-50 → primary-950 are computed via CSS color-mix in index.css.
|
||||||
|
// TenantConfig overrides this per-hostname, so localhost vs edrsc.com can
|
||||||
|
// still have different colors.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
colors: {
|
||||||
|
primary: "#357ABD", // FHC blue (fhcBlue-6) — buttons, links, active states
|
||||||
|
// // "#5D2E1F" brick (FHC chrome) is used by the sidebar/modal skin below
|
||||||
|
// // "#2563eb" blue | "#7c3aed" purple
|
||||||
|
// // "#16a34a" green | "#dc2626" red
|
||||||
|
// // "#f59e0b" amber | "#0284c7" sky
|
||||||
|
|
||||||
|
// primaryForeground: "#ffffff", // text on primary-colored bg — rarely needs changing
|
||||||
|
|
||||||
|
// secondary: "#f1f5f9", // TODO: subtle secondary UI color
|
||||||
|
// background: "#ffffff", // TODO: page background
|
||||||
|
// foreground: "#0f172a", // TODO: main text color
|
||||||
|
// border: "#e2e8f0", // TODO: input / card borders
|
||||||
|
// muted: "#f8fafc", // TODO: disabled input / tag backgrounds
|
||||||
|
// mutedForeground: "#94a3b8", // TODO: placeholder / helper text
|
||||||
|
// card: "#ffffff", // TODO: card background (if different from page)
|
||||||
|
// sidebar: "#f8fafc", // TODO: sidebar background
|
||||||
|
// danger: "#dc2626", // TODO: error / destructive color
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// TYPOGRAPHY
|
||||||
|
// Load the font FIRST in index.html (Google Fonts link or @font-face) then
|
||||||
|
// set fontFamily here. The fallback chain is used if the custom font fails.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
typography: {
|
||||||
|
fontFamily: "Plus Jakarta Sans, Inter, ui-sans-serif, system-ui, sans-serif",
|
||||||
|
// // TODO: "Poppins, Inter, sans-serif"
|
||||||
|
// // TODO: "Cairo, Inter, sans-serif" (Arabic)
|
||||||
|
// // TODO: "Noto Serif Ethiopic, serif" (Amharic)
|
||||||
|
|
||||||
|
// headingFontFamily: undefined, // TODO: separate heading font if desired
|
||||||
|
// baseFontSize: "16px", // TODO: "14px" for compact dashboards
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// SHAPE
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
shape: {
|
||||||
|
radius: "0.625rem", // TODO: "0" sharp | "0.5rem" subtle | "1rem" very rounded
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// SHADOWS
|
||||||
|
// Leave commented to use Mantine/Tailwind defaults.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// shadows: {
|
||||||
|
// card: "0 1px 3px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.06)",
|
||||||
|
// dropdown: "0 8px 30px rgba(0,0,0,0.12)",
|
||||||
|
// modal: "0 20px 60px rgba(0,0,0,0.16)",
|
||||||
|
// },
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// MANTINE COMPONENT DEFAULTS
|
||||||
|
// These become the <MantineProvider theme> defaults for every component.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
components: {
|
||||||
|
buttonDefaultVariant: "filled", // TODO: "light" | "outline" | "subtle"
|
||||||
|
inputDefaultSize: "sm", // TODO: "xs" | "md" | "lg"
|
||||||
|
inputRadius: "md", // TODO: "xs" | "lg" | "xl"
|
||||||
|
modalRadius: "lg", // TODO: "md" | "xl"
|
||||||
|
tableHighlightOnHover: true,
|
||||||
|
tableStriped: false, // TODO: "odd" | "even" | true
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// USER-MANAGEMENT LAYOUT / NAVIGATION
|
||||||
|
// Pick the navigation chrome and style the side menu — all from here.
|
||||||
|
// "classic" → app-wide SIDE MENU, no top tabs
|
||||||
|
// "legacy" → top TAB bar, no side menu
|
||||||
|
// Each value is also exposed as a --um-* CSS var, so tweaks apply instantly.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
layout: {
|
||||||
|
userManagementView: "classic", // TODO: "legacy" for the top-tab UI
|
||||||
|
// showTopBar: false, // TODO: overrides VITE_SHOW_TOP_BAR
|
||||||
|
|
||||||
|
// ── Dimensions ──────────────────────────────────────────────────────────
|
||||||
|
sidebarWidth: "288px", // TODO: expanded side-menu width
|
||||||
|
sidebarCollapsedWidth: "80px", // TODO: icon-only width
|
||||||
|
headerHeight: "64px", // TODO: top bar height
|
||||||
|
// contentMaxWidth: "1440px", // TODO: cap the content column
|
||||||
|
|
||||||
|
// ── Side-menu skin (defaults follow the FHC brick theme) ───────────────
|
||||||
|
sidebarBackground: "linear-gradient(180deg, #5D2E1F 0%, #3D1E14 100%)",
|
||||||
|
sidebarColor: "rgba(255,255,255,0.76)",
|
||||||
|
sidebarMutedColor: "rgba(255,255,255,0.42)",
|
||||||
|
sidebarActiveBackground: "rgba(255,255,255,0.15)",
|
||||||
|
sidebarActiveColor: "#FFFFFF",
|
||||||
|
sidebarHoverBackground: "rgba(255,255,255,0.08)",
|
||||||
|
sidebarBorder: "rgba(255,255,255,0.10)",
|
||||||
|
sidebarRail: "linear-gradient(180deg, #FFD700 0%, #4A90E2 100%)",
|
||||||
|
sidebarBrandLabel: "User Management",
|
||||||
|
sidebarBrandSublabel: "Federal Housing",
|
||||||
|
|
||||||
|
// ── THE MENU (data, shared by the side menu AND the top tabs) ──────────
|
||||||
|
// Edit/add/remove freely. `icon` is a name from the registry in
|
||||||
|
// navConfig.tsx (users, dashboard, content, position, settings, excel,
|
||||||
|
// archive, units, activity, organizations, …). `label` is an i18n key
|
||||||
|
// under "organization.<label>" (raw string shown if no translation).
|
||||||
|
// Remove this array entirely to fall back to the built-in defaults.
|
||||||
|
//
|
||||||
|
// SHOW / HIDE A MENU: set `enabled: false` on any item to stop it
|
||||||
|
// rendering in BOTH the side menu and the top tabs — without deleting it.
|
||||||
|
// Omitting `enabled` (or `true`) keeps it visible. Toggle these per project.
|
||||||
|
navItems: [
|
||||||
|
// The menu is ROLE-FILTERED (see navConfig.tsx): each role sees only its own
|
||||||
|
// block. Every href below has a matching route in the embedded router
|
||||||
|
// (src/App.tsx), which is now a superset of org-admin + super-admin routes.
|
||||||
|
|
||||||
|
// ── Org-admin / unit-admin surface ──
|
||||||
|
{ label: "dashboard", href: "/user-management/user_management-dashboard", icon: "dashboard", roles: ["admin", "unit_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "userManagement", href: "/user-management/user_management", icon: "users", roles: ["admin", "unit_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "contentManagement", displayLabel: "contentManagement", href: "/user-management/content-management", icon: "content", roles: ["admin", "unit_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "Position", displayLabel: "positionTypes", href: "/user-management/position-management", icon: "position", roles: ["admin", "unit_admin", "super_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "settings", displayLabel: "settings", href: "/user-management/organization-settings", icon: "settings", roles: ["admin", "unit_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "Bulk", displayLabel: "bulkUpload", href: "/user-management/bulk-upload", icon: "excel", roles: ["admin", "unit_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "Archive Users", displayLabel: "Archive Users", href: "/user-management/archives", icon: "archive", roles: ["admin", "unit_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "Archived Units & Positions", displayLabel: "Archived Units & Positions", href: "/user-management/archived", icon: "units", roles: ["admin", "unit_admin"], isPrimary: true, enabled: true },
|
||||||
|
|
||||||
|
// ── Super-admin surface (routes now wired in App.tsx) ──
|
||||||
|
{ label: "dashboard", href: "/user-management/dashboard", icon: "dashboard", roles: ["super_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "organizations", href: "/user-management/organizations", icon: "organizations", roles: ["super_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "organizationAdmins", href: "/user-management/organization_admins", icon: "admins", roles: ["super_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "externalUsers", href: "/user-management/external_users", icon: "admins", roles: ["super_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "Migrated Records", displayLabel: "migratedRecords", href: "/user-management/migrated-records-management", icon: "file", roles: ["super_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "Archive Users", displayLabel: "Archive Users", href: "/user-management/archive-users", icon: "archive", roles: ["super_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "Archived Organizations", displayLabel: "Archived Organizations", href: "/user-management/archived-organizations", icon: "archive", roles: ["super_admin"], isPrimary: true, enabled: true },
|
||||||
|
{ label: "activityLog", href: "/user-management/activity_log", icon: "activity", roles: ["super_admin"], isPrimary: false, enabled: true },
|
||||||
|
{ label: "setting", href: "/user-management/settings", icon: "settings", roles: ["super_admin"], isPrimary: false, enabled: true },
|
||||||
|
{ label: "Letter Template", href: "/user-management/templates", icon: "file", roles: ["super_admin"], isPrimary: false, enabled: true },
|
||||||
|
],
|
||||||
|
|
||||||
|
// ── Top tab bar skin (legacy view) ─────────────────────────────────────
|
||||||
|
menuBackground: "#ffffff", // TODO: tab bar background
|
||||||
|
menuColor: "#334155", // inactive tab text
|
||||||
|
menuActiveColor: "#357ABD", // active tab text (FHC blue)
|
||||||
|
menuActiveBorderColor: "#357ABD", // active tab underline
|
||||||
|
menuHoverColor: "#357ABD", // tab hover text
|
||||||
|
|
||||||
|
// ── Create / edit modal skin (shared BackofficeModal) ──────────────────
|
||||||
|
modalAccentColor: "#5D2E1F", // brick top strip
|
||||||
|
modalHeaderBackground: "#F6ECE8", // header bg (view)
|
||||||
|
modalHeaderEditBackground: "#EACFC4", // header bg (edit)
|
||||||
|
modalIconBackground: "#EACFC4", // header icon chip bg
|
||||||
|
modalIconColor: "#5D2E1F", // header icon chip color
|
||||||
|
modalTitleColor: "#1F2937", // modal title text
|
||||||
|
modalFocusColor: "#357ABD", // input focus ring inside modals
|
||||||
|
modalSurface: "#ffffff", // modal body surface
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// EXTRA CSS VARS
|
||||||
|
// Inject any CSS custom property that isn't covered above.
|
||||||
|
// Keys are variable names WITHOUT the leading "--".
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// cssVars: {
|
||||||
|
// "sidebar-width": "260px",
|
||||||
|
// "header-height": "64px",
|
||||||
|
// "content-max-width": "1440px",
|
||||||
|
// "custom-gradient": "linear-gradient(135deg, #18aa9d 0%, #0f7a70 100%)",
|
||||||
|
// },
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// MANTINE THEME ESCAPE HATCH
|
||||||
|
// Any Mantine theme key — merged on top of everything above.
|
||||||
|
// Full list: https://mantine.dev/theming/theme-object/
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
// The FHC look & feel preset — provides the fhcBlue/fhcBrick/fhcGold/fhcGray
|
||||||
|
// palettes and the `other.fhcLayout` / `other.fhcLayoutDark` tokens that the
|
||||||
|
// "classic" user-management view renders with. Lives alongside this file in
|
||||||
|
// app-config/ so the whole host config moves together at submodule-split time.
|
||||||
|
mantineTheme: fhcMantineTheme,
|
||||||
|
};
|
||||||
25
user-management-config/tsconfig.json
Normal file
25
user-management-config/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["../user-management/src/*"],
|
||||||
|
"@app-config/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["main.tsx", "project.theme.ts", "fhc.theme.ts", "../user-management/src"]
|
||||||
|
}
|
||||||
17
user-management-config/tsconfig.node.json
Normal file
17
user-management-config/tsconfig.node.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
107
user-management-config/vite.config.ts
Normal file
107
user-management-config/vite.config.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { defineConfig, loadEnv } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
const env = loadEnv(mode, process.cwd(), "");
|
||||||
|
|
||||||
|
// Same-origin sub-path the host server serves the module from. `base` makes
|
||||||
|
// built asset URLs resolve under it AND is read back inside the module
|
||||||
|
// (import.meta.env.BASE_URL) to set the router basename. Override with UM_BASE.
|
||||||
|
const base = env.UM_BASE || "/_um/";
|
||||||
|
|
||||||
|
// Build straight into the host app's public dir so its ONE server serves the
|
||||||
|
// module at <origin>/_um/ — no second server, same origin as the host.
|
||||||
|
const outDir = path.resolve(__dirname, "../apps/backoffice/public/_um");
|
||||||
|
|
||||||
|
return {
|
||||||
|
base,
|
||||||
|
plugins: [
|
||||||
|
tailwindcss(),
|
||||||
|
react(),
|
||||||
|
{
|
||||||
|
// TinyMCE is self-hosted; the module references it at the ABSOLUTE path
|
||||||
|
// /tinymce/..., which resolves at the host origin. Mirror the assets into
|
||||||
|
// BOTH the module public dir AND the host public root. Regenerated on
|
||||||
|
// build, so neither copy is a hand-managed artifact.
|
||||||
|
name: "copy-tinymce-assets",
|
||||||
|
buildStart() {
|
||||||
|
const src = path.resolve(__dirname, "node_modules/tinymce");
|
||||||
|
const dests = [
|
||||||
|
path.resolve(__dirname, "../user-management/public/tinymce"),
|
||||||
|
path.resolve(__dirname, "../apps/backoffice/public/tinymce"),
|
||||||
|
];
|
||||||
|
const runtimeEntries = [
|
||||||
|
"tinymce.min.js",
|
||||||
|
"icons",
|
||||||
|
"models",
|
||||||
|
"plugins",
|
||||||
|
"skins",
|
||||||
|
"themes",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!fs.existsSync(src)) {
|
||||||
|
throw new Error(
|
||||||
|
"[copy-tinymce-assets] node_modules/tinymce not found in this app. Run `npm install` here first."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dest of dests) {
|
||||||
|
fs.mkdirSync(dest, { recursive: true });
|
||||||
|
for (const entry of runtimeEntries) {
|
||||||
|
const entrySrc = path.resolve(src, entry);
|
||||||
|
const entryDest = path.resolve(dest, entry);
|
||||||
|
if (!fs.existsSync(entrySrc)) continue;
|
||||||
|
fs.cpSync(entrySrc, entryDest, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
assetsInclude: ["**/*.TTF"],
|
||||||
|
// Static assets (incl. tinymce/) live in the module's public dir.
|
||||||
|
publicDir: path.resolve(__dirname, "../user-management/public"),
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
external: [
|
||||||
|
"file-type",
|
||||||
|
"readable-web-to-node-stream",
|
||||||
|
"strtok3",
|
||||||
|
"token-types",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
outDir,
|
||||||
|
assetsDir: "assets",
|
||||||
|
sourcemap: false,
|
||||||
|
emptyOutDir: true,
|
||||||
|
minify: "esbuild",
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: [
|
||||||
|
// @app-config = this host folder itself (project.theme.ts / fhc.theme.ts).
|
||||||
|
{ find: /^@app-config\//, replacement: path.resolve(__dirname) + "/" },
|
||||||
|
// @/ = the reusable module's source in the SIBLING module folder.
|
||||||
|
{ find: /^@\//, replacement: path.resolve(__dirname, "../user-management/src") + "/" },
|
||||||
|
],
|
||||||
|
// node_modules is linked to the module's, but pin the singletons so the host
|
||||||
|
// entry and the module code share ONE React 18 (no "invalid hook call").
|
||||||
|
dedupe: ["react", "react-dom", "react-router-dom", "@tanstack/react-query", "@mantine/core", "@mantine/hooks"],
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: env.DEV_PORT ? Number(env.DEV_PORT) : 5173,
|
||||||
|
strictPort: true,
|
||||||
|
fs: { allow: [path.resolve(__dirname, "..")] },
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ["file-type", "readable-web-to-node-stream", "strtok3", "token-types"],
|
||||||
|
},
|
||||||
|
esbuild: {
|
||||||
|
drop: mode === "production" ? ["console", "debugger"] : [],
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
global: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user