Frontend
Frontend responsibilities, component tree, module registration, routing, build pipeline, and CI/CD for the SPA.
Frontend
What Frontend Owns
| Concern | Frontend responsibility |
|---|---|
| Provider stack | Initialize and share Redux, i18n, LaunchDarkly, Sentry, AG Grid, ErrorBoundary across all modules — once per SPA session |
| Module registration | Each module exports useXxxRoutes() and useXxxSidebarMenu() hooks consumed by the unified Routes component |
| Module resolution | Determine the active module from the URL path via matchingModule logic |
| Route rendering | Render only the active module's routes in the Switch, with AuthRoute permission checks |
| Sidebar | Render a unified sidebar with the active module's menu items and a module switcher dropdown |
| SPA bootstrap | Eager-load all module settings (session, operations, finance, registration, CRM, inventory) during initial mount |
| Code splitting | Use React.lazy() + Suspense for per-route code splitting |
| Spotlight search | Cmd+K cross-module search integration |
| Build pipeline | Webpack configuration for SPA, single-build, and all-modules build modes |
Component Tree
Entry Point: CrelioDashboard/index.tsx
Source: src/modules/CrelioDashboard/index.tsx
The SPA entry point renders <Main> with its <Routes>:
import ReactDOM from "react-dom";
import { Main } from "../../index";
import { Routes } from "./RoutesAndMenus/routes";
import "src/stylesheets/viewport.scss";
const Container = () => {
return <Main Wrapped={<Routes />} />;
};
ReactDOM.render(<Container />, document.getElementById("root"));The difference from standalone modules is that <Routes> here contains all module routes, not just a single module's routes.
Main — The Provider Stack
Source: src/index.tsx
Main is shared across all modules (SPA and standalone). It initializes:
| Provider | Purpose |
|---|---|
Suspense | Fallback loading spinner for React.lazy() components |
LDProvider | LaunchDarkly feature flag context |
Provider (Redux) | Global state store |
I18nextProvider | Internationalization |
ErrorBoundary | Sentry error boundary for crash reporting |
Router | React Router with shared history object |
AppAlert | Global toast notification system |
DetectNetwork | Offline/online detection banner |
All of these are initialized once when the SPA loads and remain alive across module switches.
Module Registration Pattern
Each module contributes to the SPA via two custom hooks:
| Hook | Returns | Purpose |
|---|---|---|
useXxxRoutes() | CrelioRouteType[] | Array of route definitions (path + component + props) |
useXxxSidebarMenu() | CrelioSidebarItem[] | Array of sidebar menu items for the module |
Example: Operations Module
Source: RoutesAndMenus/operationsRoutesAndMenu.tsx
// Routes hook
export const useOperationsRoutes = (): CrelioRouteType[] => {
return [
{ path: "/operation", Component: OperationsDashboard },
{ path: "/operation/operation-dashboard/:route", Component: OperationsDashboard },
{ path: "/operation/testwise-waiting-list/:route", Component: WaitingList },
// ... more routes
];
};
// Sidebar hook
export const useOperationsSidebarMenu = (): CrelioSidebarItem[] => {
return [
{ name: t("Operations Dashboard"), icon: "fa-chart-line", route: "/operation" },
{ name: t("Waiting List"), icon: "fa-list", route: "/operation/testwise-waiting-list/all-tests" },
// ... more menu items
];
};All Registered Modules
These hooks are consumed in routes.tsx and assembled into a modules array:
const modules: Module[] = useMemo(() => [
{ menu: financeSidebarMenu, routes: financeRoutes, selectedModule: t("Finance") },
{ menu: labAdminSidebarMenu, routes: labAdminRoutes, selectedModule: t("Admin") },
{ menu: accessionSidebarMenu, routes: accessionRoutes, selectedModule: t("Accession") },
{ menu: inventorySidebarMenu, routes: inventoryRoutes, selectedModule: t("Inventory") },
{ menu: operationSidebarMenu, routes: operationsRoutes, selectedModule: t("Operation") },
{ menu: registrationSidebarMenu, routes: registrationRoutes, selectedModule: t("Registration") },
{ menu: crmDashboardSidebarMenu, routes: crmDashboardRoutes, selectedModule: t("CRM") },
], [/* dependencies */]);Module Resolution — matchingModule
Source: RoutesAndMenus/routes.tsx
The SPA determines which module is active purely from the URL:
const matchingModule: Module = useMemo(() => {
const pathParts: string[] = location.pathname.split("/").filter(Boolean);
const moduleName: string = pathParts[0]?.toLowerCase();
// Strategy 1: Match by sidebar route
const moduleByRoute: Module | undefined = modules.find((module: Module) =>
module.menu.some((item: CrelioSidebarItem) => {
if (item.route) {
const itemRouteBase = item.route.split("/").filter(Boolean)[0]?.toLowerCase();
const locationPathBase = pathParts[0]?.toLowerCase();
const isExactMatch = location.pathname === item.route;
const isBaseMatch = locationPathBase === itemRouteBase;
return isExactMatch || isBaseMatch;
}
return false;
})
);
// Strategy 2: Match by module name
const moduleByName: Module | undefined = modules?.find(
(module: Module) => module.selectedModule?.toLowerCase() === moduleName
);
// Fallback: Registration
return moduleByRoute || moduleByName || {
menu: registrationSidebarMenu,
routes: registrationRoutes,
selectedModule: t("Registration"),
};
}, [location.pathname, modules, t]);Resolution Priority
| Priority | Strategy | Example |
|---|---|---|
| 1 | Route match — first path segment matches a sidebar item's route base | /finance/dashboard → Finance module |
| 2 | Name match — first path segment matches a module's selectedModule | /operation → Operation module |
| 3 | Fallback — if nothing matches, default to Registration | /unknown-path → Registration module |
Route Type System
Source: CrelioDashboard/routeTypes.ts
Core Types
// A regular route with a component
type CrelioRoute = {
path: string;
Component: React.ElementType;
routeProps?: RouteProps;
};
// A redirect route
type CrelioRedirectRoute = {
to: string;
from: string;
isRedirect: true | false;
};
// Union type used in route arrays
type CrelioRouteType = CrelioRoute | CrelioRedirectRoute;
// Sidebar item
type CrelioSidebarItem = {
name: string | JSX.Element;
icon?: string;
route?: string;
onClick?: Function;
isCustom?: true | false;
hasPermission?: Function;
submenuArr?: CrelioSidebarItem[];
showPartition?: boolean;
requiredPermissions?: string[];
hidden?: boolean;
count?: number | string;
redirection?: boolean;
};
// Module aggregate
interface Module {
menu: CrelioSidebarItem[];
routes: CrelioRouteType[];
selectedModule: string;
}Routes support permission-based access control via routeProps, consumed by the AuthRoute component which checks the user's session permissions before rendering.
Route Rendering
The Switch block dynamically renders only the matching module's routes:
<Switch>
{matchingModule?.routes?.map((route: any) => {
const {
to = "", from = "", path = "",
Component = null, isRedirect = false, routeProps = {},
} = route;
const key = `crelio-dashboard-route-${path || to}`;
if (isRedirect) {
return <Redirect key={key} exact={true} from={from} to={to} />;
}
return (
<AuthRoute
key={key}
exact={true}
path={path}
Component={Component}
routeProps={routeProps}
/>
);
})}
<AuthRoute Component={PageNotFound} />
</Switch>Only the matchingModule.routes are rendered at any time. Route paths across modules don't need to be globally unique — /dashboard in Finance and /dashboard in Operations won't conflict because only one module's routes are active.
The navigate Utility — v5 ↔ SPA Redirection
Source: src/utils/navigate.ts
The navigate utility is the primary programmatic navigation function used across all modules. It wraps React Router's history.push / history.replace and adds module-aware URL prefixing that makes the same navigation calls work correctly in both v5 single-build mode and SPA mode.
How It Works
import history from "src/utils/history";
import { getReduxState, isLabLogin } from "./helpers";
export type NavigateInput = string | { pathname: string; state?: JsonObject };
export const moduleNames: string[] = [
"registration", "accession", "operation",
"finance", "crm", "admin", "inventory",
];
const getUpdatedUrl = (pathname: string, locationPathname: string): string => {
const sessionData = getReduxState("SESSION");
const [firstPart] = locationPathname.split("/").filter(Boolean);
const [path] = pathname.split("/").filter(Boolean);
const urlBeforeHash = window.location.href.split("#")[0];
const updatedUrl =
isLabLogin() &&
!urlBeforeHash.includes("superAdminUserManagement") &&
moduleNames.includes(firstPart) &&
!moduleNames.includes(path) &&
!urlBeforeHash.includes("inventory")
? `/${firstPart}/${pathname}`
: pathname;
return removeDoubleSlashes(updatedUrl);
};
export const navigate: Navigate = (urlOrObject, options?) => {
const method = options?.replace ? history.replace : history.push;
const updatedUrl = getUpdatedUrl(
typeof urlOrObject === "string" ? urlOrObject : urlOrObject.pathname,
history?.location?.pathname
);
method(typeof urlOrObject === "string" ? updatedUrl : { pathname: updatedUrl, state: urlOrObject.state });
};URL Prefixing Logic — getUpdatedUrl
The key function is getUpdatedUrl, which decides whether to prefix the target URL with the current module name:
| Condition | Result | Example |
|---|---|---|
| User is a lab login AND current URL is in a known module AND target path is NOT a known module name | Prefix the target with the current module | In /operation, navigating to /dashboard → /operation/dashboard |
| Target path IS a known module name | No prefix — it's a cross-module navigation | Navigating to /finance/dashboard → /finance/dashboard |
URL contains superAdminUserManagement | No prefix — admin context bypass | Admin pages skip module prefixing |
URL contains inventory | No prefix — inventory has its own routing structure | Inventory module navigation is passed through directly |
Why This Matters
In v5 single-build mode, each module runs in its own React app instance with its own <Router>. Routes like /dashboard or /settings are relative to the module and don't need prefixing because the module context is implicit in the HTML entry point.
In SPA mode, all modules share a single <Router>. A component calling navigate("/dashboard") from within the Operations module needs the URL to resolve as /operation/dashboard, not just /dashboard (which would lose the module context). The navigate utility handles this transparently — components don't need to know if they're running in SPA or v5 mode.
Usage
import { navigate } from "src/utils/navigate";
// Simple string navigation
navigate("/testwise-waiting-list/all-tests");
// With state
navigate({ pathname: "/patient-details", state: { patientId: 123 } });
// Replace instead of push (no back button entry)
navigate("/settings", { replace: true });
// Cross-module navigation (explicitly includes module name)
navigate("/finance/dashboard");SPA Bootstrap — Initialization Sequence
When the SPA loads, the Routes component runs a comprehensive initialization sequence:
useEffect(() => {
const initialize = async () => {
// 1. Session + bill settings
await getAppLicationLevelSessionAndBillSetting();
// 2. Registration-specific
await getCustomRegistrationPage();
await getBillSettingData(dispatch);
// 3. Operations-specific
dispatch(getSettingsData("/getOperationSetting/", false));
dispatch(getReportSettings("/getAllReportSettings/", true));
// 4. Shared data
dispatch(getDepartmentList(true));
await getDepartmentWiseDoctor(dispatch);
await getDepartmentAndSignDocRel(dispatch);
await getDoctorList(dispatch);
// 5. Inventory (conditional)
inventoryManagement && userInventoryManagement && await fetchInventorySettings();
// 6. CRM (parallel batch)
axiosInstance.all([
"/crm/onboarding/user/permissions/",
"/crm/store/store-enable-status/",
"/crm/onboarding/walkthrough/configurations/",
"/crm/onboarding/walkthrough/",
"/crm/home-collections/get-crm-settings/",
].map(endpoint => axiosInstance.get(endpoint)))
.then(/* dispatch all CRM state */)
.finally(() => setLoading(false));
};
initialize();
}, []);This runs once on SPA mount. All subsequent module switches use the data already in Redux.
Spotlight Search
The SPA includes a cross-module search feature (Cmd+K / Ctrl+K):
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent): void => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
onSpotlightSearch();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);The SpotlightSearch component receives the selectedModule to scope search results to the current module context while still enabling cross-module navigation.
Build Pipeline
app-config.js — Entry Point Registry
Source: app-config.js
const allApps = [
"all",
// Clubbable modules (SPA-eligible)
"Admin", "Finance", "inventory", "LabAdmin",
"Accession", "Operations", "Registration", "crmDashboard", "CrelioDashboard",
// Standalone modules (separate builds)
"crm", "DoctorLogin", "ReferralLogin", "MarketingLogin",
"centerdashboard", "OrganisationLogin",
];
const allBuilds = {
Admin: "src/modules/Admin/index.tsx",
Finance: "src/modules/Finance/index.tsx",
CrelioDashboard: "src/modules/CrelioDashboard/index.tsx",
// ... each module maps to its entry point
};Build Path Resolution
| Build | Output Directory | Public URL Path |
|---|---|---|
CrelioDashboard | ./build/CrelioDashboard | /static/build_assets/CrelioDashboard/build |
| All other modules | ./build/dashboard | /static/build_assets/dashboard/build |
config-overrides.js — Webpack Customization
Source: config-overrides.js
Key customizations using react-app-rewired + customize-cra:
Multi-entry support:
const build = process?.env?.REACT_APP_BUILD_NAME || "DoctorLogin";
const entries = appConfig.getBuild(build);
const multipleEntry = require("react-app-rewire-multiple-entry")(entries);esbuild-loader (replaces babel-loader):
rule.oneOf[babelLoaderIndex] = {
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: [{
loader: "esbuild-loader",
options: { loader: "tsx", target: "es2015" },
}],
};Chunk splitting strategy:
splitChunks: {
chunks: "all",
maxInitialRequests: 20,
minSize: 20000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)?.[1];
return packageName ? `vendor.${packageName.replace("@", "")}` : "vendor";
},
priority: 20,
},
commons: {
test: /[\\/]src[\\/]/,
name: "commons",
minChunks: 2,
priority: 10,
},
},
},This creates per-package vendor chunks (vendor.react.js, vendor.ag-grid-community.js), a commons chunk for shared source code, and a single runtime chunk.
Production Optimizations
| Plugin | Purpose |
|---|---|
TerserPlugin | JavaScript minification |
EsbuildPlugin | Additional minification + CSS minification |
CompressionPlugin | Gzip pre-compression for .js, .css, .html, .svg |
MiniCssExtractPlugin | Extract CSS into separate files with content hashes |
ModuleConcatenationPlugin | Scope hoisting for smaller bundles |
BundleAnalyzerPlugin | Static HTML report of bundle composition |
ContextReplacementPlugin | Strip unused Moment.js locales (keep only English) |
NormalModuleReplacementPlugin | Replace lodash with lodash-es for tree-shaking |
Build Output Comparison
| Aspect | SPA Build | All Build (Legacy) | Single Module Build |
|---|---|---|---|
| Command | nps build.CrelioDashboardProd | nps build.allProd | nps build.RegistrationProd |
| Entry points | 1 (CrelioDashboard/index.tsx) | 15 (all modules) | 1 (specific module) |
| HTML files | 1 (index.html) | 15 (one per module) | 1 (index.html) |
| JS bundles | Unified + code-split per module | Shared chunks + per-module bundles | Module-specific bundle |
| Output dir | build/CrelioDashboard/ | build/dashboard/ | build/dashboard/ |
| Build time | ~3-5 min | ~15-25 min | ~2-3 min |
Asset Deployment — move-assets.sh
After building, assets are copied into the Django project's static directory:
prepare_and_move_assets() {
cd ../livehealthapp/static
rm -vrf build_assets/$1
mkdir -p build_assets/$1/build
cd ../../livehealth-frontend
cp -vrf build/$1/* ../livehealthapp/static/build_assets/$1/build
}CI/CD Pipeline
Bitbucket Pipelines
Source: bitbucket-pipelines.yml
Each module has staging and production build steps:
| Build Step | Build Command |
|---|---|
registration-prod-build | build.RegistrationProd |
finance-prod-build | build.FinanceProd |
crm-prod-build | build.crmProd |
Jenkins Integration
For the all build (including SPA), Jenkins is triggered via the Bitbucket pipeline:
- Checks out the branch.
- Runs
yarn installandyarn start build.allProd. - Uploads bundled assets to S3 or deploys to EC2.
Post-build steps (Jenkins only):
sentry:production— Uploads source maps to Sentry for error deobfuscation.delete-sourcemaps— Removes.mapfiles from the build output to prevent client-side source exposure.
Comparison: SPA Module vs. Standalone Module
| Aspect | SPA Module (CrelioDashboard) | Standalone Module (e.g., DoctorLogin) |
|---|---|---|
| Entry point | CrelioDashboard/index.tsx | DoctorLogin/index.tsx |
| Routes | All modules combined in one Switch | Module-specific routes only |
| Sidebar | Shared SideBar with module switcher | Module-specific SideBar |
| Initialization | All settings fetched once | Module-specific settings only |
| Module switching | In-memory route change | Full page reload |
| Session | Shared staff session | Role-specific session |
| Build output | crelio-dashboard.html | doctor-login.html |
Key Frontend Locations
| File | Purpose |
|---|---|
CrelioDashboard/index.tsx | SPA entry point — renders Main with unified Routes |
RoutesAndMenus/routes.tsx | SPA-level routing: module resolution, initialization, sidebar binding |
routeTypes.ts | TypeScript type definitions for routes, sidebar items, and modules |
Sidebar/sidebar.tsx | Unified sidebar with module-aware menu rendering |
SwitchModuleDropdown/ | Module switcher dropdown component |
src/utils/navigate.ts | Module-aware navigation utility — handles URL prefixing for v5 ↔ SPA compatibility |
app-config.js | Module registry: maps build names to entry points |
config-overrides.js | Webpack customization: esbuild-loader, chunk splitting |
package-scripts.js | nps build commands for local dev and production builds |
bitbucket-pipelines.yml | CI/CD pipeline: per-module build steps |
move-assets.sh | Asset deployment: copies build output into Django static directory |
src/setupProxy.js | Dev server proxy: routes /api-v3/ to Py3 backend |