feat(ui): animated boot splash and loading screen
Show a Lottie-animated splash while the app bundle boots and dismiss it once WebAppComponent initialises, plus a reusable mifosx-loading-screen component with loading and error animations. lottie.min.js is served from the lottie-web package instead of being vendored into src/assets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,8 @@
|
|||||||
"@angular/localize/init"
|
"@angular/localize/init"
|
||||||
],
|
],
|
||||||
"assets": [
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/apple-touch-icon.png",
|
||||||
"src/assets/images/cbsnexora-favicon.svg",
|
"src/assets/images/cbsnexora-favicon.svg",
|
||||||
"src/robots.txt",
|
"src/robots.txt",
|
||||||
"src/manifest.json",
|
"src/manifest.json",
|
||||||
@@ -39,6 +41,11 @@
|
|||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
"input": "node_modules/tinymce",
|
"input": "node_modules/tinymce",
|
||||||
"output": "assets/tinymce"
|
"output": "assets/tinymce"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "lottie.min.js",
|
||||||
|
"input": "node_modules/lottie-web/build/player",
|
||||||
|
"output": "assets/js"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
|||||||
Generated
+7
@@ -41,6 +41,7 @@
|
|||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
"lightgallery": "^2.9.0",
|
"lightgallery": "^2.9.0",
|
||||||
"lodash": "4.17.23",
|
"lodash": "4.17.23",
|
||||||
|
"lottie-web": "^5.13.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"ngx-image-cropper": "^9.1.6",
|
"ngx-image-cropper": "^9.1.6",
|
||||||
"ngx-mat-select-search": "^8.0.2",
|
"ngx-mat-select-search": "^8.0.2",
|
||||||
@@ -18380,6 +18381,12 @@
|
|||||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lottie-web": {
|
||||||
|
"version": "5.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.13.0.tgz",
|
||||||
|
"integrity": "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
|
|||||||
@@ -71,6 +71,7 @@
|
|||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
"lightgallery": "^2.9.0",
|
"lightgallery": "^2.9.0",
|
||||||
"lodash": "4.17.23",
|
"lodash": "4.17.23",
|
||||||
|
"lottie-web": "^5.13.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"ngx-image-cropper": "^9.1.6",
|
"ngx-image-cropper": "^9.1.6",
|
||||||
"ngx-mat-select-search": "^8.0.2",
|
"ngx-mat-select-search": "^8.0.2",
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ import { TokenInterceptor as ZitadelTokenInterceptor } from './zitadel/token.int
|
|||||||
import { AuthService } from './zitadel/auth.service';
|
import { AuthService } from './zitadel/auth.service';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { CallbackComponent } from './zitadel/callback/callback.component';
|
import { CallbackComponent } from './zitadel/callback/callback.component';
|
||||||
|
import { LoadingScreenComponent } from './shared/loading-screen/loading-screen.component';
|
||||||
import { OAuthModule } from 'angular-oauth2-oidc';
|
import { OAuthModule } from 'angular-oauth2-oidc';
|
||||||
|
|
||||||
export class CustomMissingTranslationHandler implements MissingTranslationHandler {
|
export class CustomMissingTranslationHandler implements MissingTranslationHandler {
|
||||||
@@ -195,6 +196,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
NotFoundComponent,
|
NotFoundComponent,
|
||||||
CallbackComponent,
|
CallbackComponent,
|
||||||
|
LoadingScreenComponent,
|
||||||
OAuthModule.forRoot()
|
OAuthModule.forRoot()
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<!--
|
||||||
|
Copyright since 2025 Mifos Initiative
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
-->
|
||||||
|
|
||||||
|
@if (active) {
|
||||||
|
@if (currentMode === 'overlay') {
|
||||||
|
<!-- Mifos Mobile Transaction / Action Progress Overlay -->
|
||||||
|
<div class="cbs-loading-backdrop" (click)="preventDismiss($event)">
|
||||||
|
<div class="cbs-loading-dialog" role="dialog" aria-modal="true" [attr.aria-label]="title || 'Loading'">
|
||||||
|
<div class="cbs-lottie-container" #lottieContainer>
|
||||||
|
<!-- Vector Fallback if Lottie is loading -->
|
||||||
|
@if (!lottieLoaded) {
|
||||||
|
<div class="cbs-vector-spinner">
|
||||||
|
<div class="spinner-ring ring-outer"></div>
|
||||||
|
<div class="spinner-ring ring-mid"></div>
|
||||||
|
<div class="spinner-ring ring-inner"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cbs-loading-dialog__content">
|
||||||
|
<h3 class="cbs-loading-dialog__title">{{ title || 'CBS Nexora' }}</h3>
|
||||||
|
<p class="cbs-loading-dialog__message">{{ message || 'Please wait...' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cbs-loading-pulse-bar">
|
||||||
|
<div class="cbs-pulse-bar__indicator"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (currentMode === 'fullscreen') {
|
||||||
|
<!-- Full-Screen App Boot / Splash Screen -->
|
||||||
|
<div class="cbs-fullscreen-loader">
|
||||||
|
<div class="cbs-fullscreen-loader__brand">
|
||||||
|
<div class="cbs-brand-icon">
|
||||||
|
<svg width="40" height="40" viewBox="0 0 32 32" aria-hidden="true">
|
||||||
|
<rect x="1" y="1" width="14" height="14" rx="2" fill="#2563eb" />
|
||||||
|
<rect x="17" y="1" width="14" height="14" rx="2" fill="#0ea5e9" />
|
||||||
|
<rect x="1" y="17" width="14" height="14" rx="2" fill="#10b981" />
|
||||||
|
<rect x="17" y="17" width="14" height="14" rx="2" fill="#94a3b8" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="cbs-brand-name">{{ title || 'CBS Nexora' }}</h2>
|
||||||
|
<span class="cbs-brand-sub">Core Banking System</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cbs-lottie-container cbs-lottie-container--large" #lottieContainer>
|
||||||
|
@if (!lottieLoaded) {
|
||||||
|
<div class="cbs-vector-spinner cbs-vector-spinner--large">
|
||||||
|
<div class="spinner-ring ring-outer"></div>
|
||||||
|
<div class="spinner-ring ring-mid"></div>
|
||||||
|
<div class="spinner-ring ring-inner"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cbs-progress-track">
|
||||||
|
<div class="cbs-progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="cbs-fullscreen-loader__status">{{ message || 'Connecting to Core Banking Engine...' }}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (currentMode === 'inline') {
|
||||||
|
<!-- Inline Component Loader -->
|
||||||
|
<div class="cbs-inline-loader">
|
||||||
|
<div class="cbs-lottie-container cbs-lottie-container--compact" #lottieContainer>
|
||||||
|
@if (!lottieLoaded) {
|
||||||
|
<div class="cbs-vector-spinner cbs-vector-spinner--compact">
|
||||||
|
<div class="spinner-ring ring-outer"></div>
|
||||||
|
<div class="spinner-ring ring-mid"></div>
|
||||||
|
<div class="spinner-ring ring-inner"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (message) {
|
||||||
|
<p class="cbs-inline-loader__message">{{ message }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Overlay Mode (MifosProgressIndicatorOverlay / MifosLoadingDialog)
|
||||||
|
========================================================================== */
|
||||||
|
.cbs-loading-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 99999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgb(9 13 22 / 72%);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
animation: backdrop-fade-in 0.25s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-loading-dialog {
|
||||||
|
position: relative;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 340px;
|
||||||
|
padding: 28px 24px;
|
||||||
|
background: #111827;
|
||||||
|
color: #f8fafc;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgb(255 255 255 / 12%);
|
||||||
|
box-shadow:
|
||||||
|
0 25px 60px -15px rgb(0 0 0 / 65%),
|
||||||
|
0 0 35px rgb(37 99 235 / 18%);
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: dialog-scale-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context([data-theme='light']) .cbs-loading-dialog,
|
||||||
|
:host-context(.light-theme) .cbs-loading-dialog {
|
||||||
|
background: #fff;
|
||||||
|
color: #0f172a;
|
||||||
|
border-color: rgb(0 0 0 / 8%);
|
||||||
|
box-shadow:
|
||||||
|
0 25px 50px -12px rgb(0 0 0 / 25%),
|
||||||
|
0 0 25px rgb(37 99 235 / 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-loading-dialog__content {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-loading-dialog__title {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-loading-dialog__message {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
line-height: 1.45;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context([data-theme='light']) .cbs-loading-dialog__message,
|
||||||
|
:host-context(.light-theme) .cbs-loading-dialog__message {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulse bar at dialog bottom */
|
||||||
|
.cbs-loading-pulse-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
background: rgb(255 255 255 / 8%);
|
||||||
|
border-radius: 99px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-pulse-bar__indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 40%;
|
||||||
|
background: linear-gradient(90deg, #2563eb, #38bdf8);
|
||||||
|
border-radius: 99px;
|
||||||
|
animation: pulse-bar-slide 1.4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Full-Screen Mode (App Boot / Splash)
|
||||||
|
========================================================================== */
|
||||||
|
.cbs-fullscreen-loader {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 99999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: radial-gradient(circle at center, #0f172a 0%, #090d16 100%);
|
||||||
|
color: #f8fafc;
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context([data-theme='light']) .cbs-fullscreen-loader,
|
||||||
|
:host-context(.light-theme) .cbs-fullscreen-loader {
|
||||||
|
background: radial-gradient(circle at center, #fff 0%, #f1f5f9 100%);
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-fullscreen-loader__brand {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-brand-icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: linear-gradient(135deg, #1e3a8a 0%, #0284c7 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 10px 24px rgb(14 165 233 / 35%);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-brand-name {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-brand-sub {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-progress-track {
|
||||||
|
width: 240px;
|
||||||
|
height: 5px;
|
||||||
|
background: rgb(255 255 255 / 10%);
|
||||||
|
border-radius: 99px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 18px 0 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-progress-fill {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 45%;
|
||||||
|
background: linear-gradient(90deg, #2563eb, #38bdf8);
|
||||||
|
border-radius: 99px;
|
||||||
|
box-shadow: 0 0 12px rgb(56 189 248 / 50%);
|
||||||
|
animation: pulse-bar-slide 1.5s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-fullscreen-loader__status {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-family: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Inline Mode
|
||||||
|
========================================================================== */
|
||||||
|
.cbs-inline-loader {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 32px 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-inline-loader__message {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Lottie & Vector Spinner Dimensions
|
||||||
|
========================================================================== */
|
||||||
|
.cbs-lottie-container {
|
||||||
|
width: 110px;
|
||||||
|
height: 110px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-lottie-container--large {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-lottie-container--compact {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vector Spinner Fallback (CSS Multi-Ring Loader) */
|
||||||
|
.cbs-vector-spinner {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-vector-spinner--large {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbs-vector-spinner--compact {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-ring {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ring-outer {
|
||||||
|
border-top-color: #2563eb;
|
||||||
|
border-right-color: #2563eb;
|
||||||
|
animation: spin-outer 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ring-mid {
|
||||||
|
inset: 6px;
|
||||||
|
border-bottom-color: #0ea5e9;
|
||||||
|
border-left-color: #0ea5e9;
|
||||||
|
animation: spin-mid 1.6s cubic-bezier(0.5, 0, 0.5, 1) infinite reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ring-inner {
|
||||||
|
inset: 13px;
|
||||||
|
border-top-color: #10b981;
|
||||||
|
animation: spin-inner 0.9s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Keyframe Animations
|
||||||
|
========================================================================== */
|
||||||
|
@keyframes backdrop-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialog-scale-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-bar-slide {
|
||||||
|
0% {
|
||||||
|
left: -40%;
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
left: 35%;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-outer {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-mid {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-inner {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
OnDestroy,
|
||||||
|
Input,
|
||||||
|
ElementRef,
|
||||||
|
ViewChild,
|
||||||
|
AfterViewInit,
|
||||||
|
inject,
|
||||||
|
ChangeDetectorRef
|
||||||
|
} from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { LoadingScreenService, LoadingConfig } from './loading-screen.service';
|
||||||
|
|
||||||
|
declare const lottie: any;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-loading-screen',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
templateUrl: './loading-screen.component.html',
|
||||||
|
styleUrls: ['./loading-screen.component.scss']
|
||||||
|
})
|
||||||
|
export class LoadingScreenComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
private readonly loadingService = inject(LoadingScreenService);
|
||||||
|
private readonly cdr = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
|
@Input() mode: 'overlay' | 'inline' | 'fullscreen' = 'overlay';
|
||||||
|
@Input() title: string = 'CBS Nexora';
|
||||||
|
@Input() message: string = 'Processing transaction...';
|
||||||
|
@Input() active: boolean = false;
|
||||||
|
@Input() isGlobalOverlay: boolean = true;
|
||||||
|
|
||||||
|
@ViewChild('lottieContainer') lottieContainerRef?: ElementRef<HTMLDivElement>;
|
||||||
|
|
||||||
|
currentMode: 'overlay' | 'inline' | 'fullscreen' = 'overlay';
|
||||||
|
lottieLoaded: boolean = false;
|
||||||
|
private animInstance: any = null;
|
||||||
|
private subscription?: Subscription;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.currentMode = this.mode;
|
||||||
|
|
||||||
|
if (this.isGlobalOverlay) {
|
||||||
|
this.subscription = new Subscription();
|
||||||
|
this.subscription.add(
|
||||||
|
this.loadingService.isVisible$.subscribe((visible) => {
|
||||||
|
this.active = visible;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
if (visible) {
|
||||||
|
setTimeout(() => this.initLottie(), 50);
|
||||||
|
} else {
|
||||||
|
this.destroyLottie();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subscription.add(
|
||||||
|
this.loadingService.config$.subscribe((cfg: LoadingConfig) => {
|
||||||
|
if (cfg.title !== undefined) this.title = cfg.title;
|
||||||
|
if (cfg.message !== undefined) this.message = cfg.message;
|
||||||
|
if (cfg.mode !== undefined) this.currentMode = cfg.mode;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
if (this.active) {
|
||||||
|
this.initLottie();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscription?.unsubscribe();
|
||||||
|
this.destroyLottie();
|
||||||
|
}
|
||||||
|
|
||||||
|
preventDismiss(event: MouseEvent): void {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initLottie(): void {
|
||||||
|
if (!this.lottieContainerRef?.nativeElement) return;
|
||||||
|
this.destroyLottie();
|
||||||
|
|
||||||
|
const container = this.lottieContainerRef.nativeElement;
|
||||||
|
|
||||||
|
// Check if lottie-web global is available
|
||||||
|
if (typeof lottie !== 'undefined') {
|
||||||
|
try {
|
||||||
|
this.animInstance = lottie.loadAnimation({
|
||||||
|
container,
|
||||||
|
renderer: 'svg',
|
||||||
|
loop: true,
|
||||||
|
autoplay: true,
|
||||||
|
path: 'assets/animations/loading_animation.json'
|
||||||
|
});
|
||||||
|
this.lottieLoaded = true;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Could not load Lottie animation, using vector fallback', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try dynamic script load of local asset if not loaded yet
|
||||||
|
this.loadLottieScript()
|
||||||
|
.then(() => {
|
||||||
|
if (typeof lottie !== 'undefined' && this.lottieContainerRef?.nativeElement) {
|
||||||
|
this.animInstance = lottie.loadAnimation({
|
||||||
|
container: this.lottieContainerRef.nativeElement,
|
||||||
|
renderer: 'svg',
|
||||||
|
loop: true,
|
||||||
|
autoplay: true,
|
||||||
|
path: 'assets/animations/loading_animation.json'
|
||||||
|
});
|
||||||
|
this.lottieLoaded = true;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.lottieLoaded = false;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadLottieScript(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof lottie !== 'undefined') {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingScript = document.getElementById('cbs-lottie-script');
|
||||||
|
if (existingScript) {
|
||||||
|
existingScript.addEventListener('load', () => resolve());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.id = 'cbs-lottie-script';
|
||||||
|
script.src = 'assets/js/lottie.min.js';
|
||||||
|
script.onload = () => resolve();
|
||||||
|
script.onerror = () => reject();
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private destroyLottie(): void {
|
||||||
|
if (this.animInstance) {
|
||||||
|
try {
|
||||||
|
this.animInstance.destroy();
|
||||||
|
} catch (_) {}
|
||||||
|
this.animInstance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
export interface LoadingConfig {
|
||||||
|
title?: string;
|
||||||
|
message?: string;
|
||||||
|
mode?: 'overlay' | 'inline' | 'fullscreen';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class LoadingScreenService {
|
||||||
|
private readonly visibleSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
private readonly configSubject = new BehaviorSubject<LoadingConfig>({
|
||||||
|
title: 'CBS Nexora',
|
||||||
|
message: 'Processing transaction...',
|
||||||
|
mode: 'overlay'
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly isVisible$: Observable<boolean> = this.visibleSubject.asObservable();
|
||||||
|
readonly config$: Observable<LoadingConfig> = this.configSubject.asObservable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the loading screen/overlay
|
||||||
|
* @param message Optional message to display (e.g. "Processing Transfer...")
|
||||||
|
* @param title Optional title to display (defaults to "CBS Nexora")
|
||||||
|
* @param mode Optional display mode ('overlay' | 'fullscreen')
|
||||||
|
*/
|
||||||
|
show(message?: string, title?: string, mode: 'overlay' | 'fullscreen' = 'overlay'): void {
|
||||||
|
this.configSubject.next({
|
||||||
|
title: title || 'CBS Nexora',
|
||||||
|
message: message || 'Please wait...',
|
||||||
|
mode
|
||||||
|
});
|
||||||
|
this.visibleSubject.next(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the loading screen/overlay
|
||||||
|
*/
|
||||||
|
hide(): void {
|
||||||
|
this.visibleSubject.next(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update message while loading screen is active
|
||||||
|
*/
|
||||||
|
setMessage(message: string): void {
|
||||||
|
const current = this.configSubject.getValue();
|
||||||
|
this.configSubject.next({ ...current, message });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ import { SearchToolComponent } from './search-tool/search-tool.component';
|
|||||||
import { KeyboardShortcutsDialogComponent } from './keyboard-shortcuts-dialog/keyboard-shortcuts-dialog.component';
|
import { KeyboardShortcutsDialogComponent } from './keyboard-shortcuts-dialog/keyboard-shortcuts-dialog.component';
|
||||||
import { ServerSelectorComponent } from './server-selector/server-selector.component';
|
import { ServerSelectorComponent } from './server-selector/server-selector.component';
|
||||||
import { TenantSelectorComponent } from './tenant-selector/tenant-selector.component';
|
import { TenantSelectorComponent } from './tenant-selector/tenant-selector.component';
|
||||||
|
import { LoadingScreenComponent } from './loading-screen/loading-screen.component';
|
||||||
|
|
||||||
/** Custom Modules */
|
/** Custom Modules */
|
||||||
import { IconsModule } from './icons.module';
|
import { IconsModule } from './icons.module';
|
||||||
@@ -116,9 +117,11 @@ import { DatetimePickerStubComponent } from './datetime-picker-stub/datetime-pic
|
|||||||
DropdownComponent,
|
DropdownComponent,
|
||||||
InputAmountComponent,
|
InputAmountComponent,
|
||||||
InputPasswordComponent,
|
InputPasswordComponent,
|
||||||
|
LoadingScreenComponent,
|
||||||
DatetimePickerStubComponent // temporarily added stub for datetime picker
|
DatetimePickerStubComponent // temporarily added stub for datetime picker
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
LoadingScreenComponent,
|
||||||
FileUploadComponent,
|
FileUploadComponent,
|
||||||
FooterComponent,
|
FooterComponent,
|
||||||
LanguageSelectorComponent,
|
LanguageSelectorComponent,
|
||||||
|
|||||||
@@ -7,3 +7,4 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
<mifosx-loading-screen></mifosx-loading-screen>
|
||||||
|
|||||||
@@ -296,6 +296,11 @@ export class WebAppComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dismiss initial application boot splash screen smoothly
|
||||||
|
if (typeof (window as any).hideAppSplash === 'function') {
|
||||||
|
(window as any).hideAppSplash();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -6,53 +6,95 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Modernized Loader Wrapper (Inspired by openMF/mifos-mobile)
|
||||||
|
Supports full-screen and embedded containers with smooth vector physics.
|
||||||
|
========================================================================== */
|
||||||
.loader-wrapper {
|
.loader-wrapper {
|
||||||
height: 100vh;
|
position: relative;
|
||||||
width: 100vw;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 280px;
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
/* Modern orbital rings that replace legacy rigid triangles smoothly */
|
||||||
.triangle {
|
.triangle {
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-style: solid;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
animation: rotate 0.8s infinite 0.2s;
|
border-radius: 50%;
|
||||||
|
border-style: solid;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
border-width: 0 90px 90px;
|
width: 72px;
|
||||||
border-color: transparent transparent #1fb050;
|
height: 72px;
|
||||||
bottom: calc(50% - 100px);
|
border-width: 3px;
|
||||||
left: calc(50% - 90px);
|
border-color: #2563eb transparent transparent;
|
||||||
|
animation: cbs-spin-ring 1.1s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||||
|
box-shadow: 0 0 16px rgb(37 99 235 / 35%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.top {
|
.top {
|
||||||
border-width: 90px 90px 0;
|
width: 54px;
|
||||||
border-color: #b4d575 transparent transparent;
|
height: 54px;
|
||||||
top: calc(50% - 100px);
|
border-width: 3px;
|
||||||
left: calc(50% - 90px);
|
border-color: transparent transparent #0ea5e9;
|
||||||
|
animation: cbs-spin-ring 1.4s cubic-bezier(0.5, 0, 0.5, 1) infinite reverse;
|
||||||
|
box-shadow: 0 0 14px rgb(14 165 233 / 35%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
border-width: 90px 0 90px 90px;
|
width: 36px;
|
||||||
border-color: transparent transparent transparent #1074b9;
|
height: 36px;
|
||||||
top: calc(50% - 90px);
|
border-width: 2.5px;
|
||||||
left: calc(50% - 100px);
|
border-color: #10b981 transparent transparent;
|
||||||
|
animation: cbs-spin-ring 0.9s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||||
|
box-shadow: 0 0 10px rgb(16 185 129 / 35%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
border-width: 90px 90px 90px 0;
|
width: 18px;
|
||||||
border-color: transparent #1daeec transparent transparent;
|
height: 18px;
|
||||||
top: calc(50% - 90px);
|
border-width: 2px;
|
||||||
right: calc(50% - 100px);
|
border-color: transparent #38bdf8 transparent transparent;
|
||||||
|
animation: cbs-spin-ring 0.7s cubic-bezier(0.5, 0, 0.5, 1) infinite reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rotate {
|
/* Optional central glowing core */
|
||||||
from {
|
&::after {
|
||||||
transform: rotate(0deg);
|
content: '';
|
||||||
}
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
to {
|
border-radius: 50%;
|
||||||
transform: rotate(360deg);
|
background: #38bdf8;
|
||||||
}
|
box-shadow: 0 0 12px #38bdf8;
|
||||||
|
animation: cbs-pulse-core 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cbs-spin-ring {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cbs-pulse-core {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(0.85);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.3);
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+259
-8
@@ -11,7 +11,7 @@
|
|||||||
<html lang="en" class="h-full bg-gray-100">
|
<html lang="en" class="h-full bg-gray-100">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>{{'APP_NAME' | translate}}</title>
|
<title>CBS Nexora</title>
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
@@ -111,13 +111,264 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<mifosx-web-app>
|
<div id="cbs-app-splash-screen" class="cbs-init-screen">
|
||||||
<div class="loader-wrapper">
|
<style>
|
||||||
<div class="bottom triangle"></div>
|
.cbs-init-screen {
|
||||||
<div class="top triangle"></div>
|
position: fixed;
|
||||||
<div class="left triangle"></div>
|
inset: 0;
|
||||||
<div class="right triangle"></div>
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: radial-gradient(circle at center, #0f172a 0%, #090d16 100%);
|
||||||
|
color: #f8fafc;
|
||||||
|
font-family:
|
||||||
|
'Plus Jakarta Sans',
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
sans-serif;
|
||||||
|
z-index: 999999;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.cbs-init-screen.cbs-splash-fade-out {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.02);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.cbs-init-brand {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.cbs-init-icon {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: linear-gradient(135deg, #1e3a8a 0%, #0284c7 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 12px 28px rgba(14, 165, 233, 0.35);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
.cbs-init-title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.cbs-init-sub {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.cbs-init-lottie-box {
|
||||||
|
width: 130px;
|
||||||
|
height: 130px;
|
||||||
|
margin: 8px auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
filter: drop-shadow(0 8px 24px rgba(37, 99, 235, 0.25));
|
||||||
|
}
|
||||||
|
.cbs-init-spinner {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.cbs-init-ring {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
}
|
||||||
|
.cbs-init-ring-1 {
|
||||||
|
border-top-color: #2563eb;
|
||||||
|
border-right-color: #2563eb;
|
||||||
|
animation: cbsInitSpin 1.1s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||||
|
}
|
||||||
|
.cbs-init-ring-2 {
|
||||||
|
inset: 6px;
|
||||||
|
border-bottom-color: #0ea5e9;
|
||||||
|
border-left-color: #0ea5e9;
|
||||||
|
animation: cbsInitSpin 1.5s cubic-bezier(0.5, 0, 0.5, 1) infinite reverse;
|
||||||
|
}
|
||||||
|
.cbs-init-ring-3 {
|
||||||
|
inset: 12px;
|
||||||
|
border-top-color: #10b981;
|
||||||
|
animation: cbsInitSpin 0.9s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||||
|
}
|
||||||
|
.cbs-init-track {
|
||||||
|
width: 260px;
|
||||||
|
height: 5px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 99px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 18px 0 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.cbs-init-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
background: linear-gradient(90deg, #2563eb, #38bdf8);
|
||||||
|
border-radius: 99px;
|
||||||
|
box-shadow: 0 0 12px rgba(56, 189, 248, 0.6);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
.cbs-init-status {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-family: inherit;
|
||||||
|
margin: 0;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
@keyframes cbsInitSpin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="cbs-init-brand">
|
||||||
|
<div class="cbs-init-icon">
|
||||||
|
<svg width="40" height="40" viewBox="0 0 32 32">
|
||||||
|
<rect x="2" y="2" width="13" height="13" rx="2.5" fill="#ffffff" />
|
||||||
|
<rect x="17" y="2" width="13" height="13" rx="2.5" fill="#29abe2" />
|
||||||
|
<rect x="2" y="17" width="13" height="13" rx="2.5" fill="#00b388" />
|
||||||
|
<rect x="17" y="17" width="13" height="13" rx="2.5" fill="#6b7fa3" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="cbs-init-title">CBS Nexora</h2>
|
||||||
|
<span class="cbs-init-sub">Next-Generation Core Banking</span>
|
||||||
</div>
|
</div>
|
||||||
</mifosx-web-app>
|
|
||||||
|
<div id="cbs-init-lottie-target" class="cbs-init-lottie-box">
|
||||||
|
<div class="cbs-init-spinner">
|
||||||
|
<div class="cbs-init-ring cbs-init-ring-1"></div>
|
||||||
|
<div class="cbs-init-ring cbs-init-ring-2"></div>
|
||||||
|
<div class="cbs-init-ring cbs-init-ring-3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cbs-init-track">
|
||||||
|
<div class="cbs-init-fill" id="cbs-init-progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="cbs-init-status" id="cbs-init-status-text">Connecting to Secure Banking Gateway...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var splash = document.getElementById('cbs-app-splash-screen');
|
||||||
|
var bar = document.getElementById('cbs-init-progress-bar');
|
||||||
|
var status = document.getElementById('cbs-init-status-text');
|
||||||
|
var progress = 0;
|
||||||
|
var minTimePassed = false;
|
||||||
|
var angularReady = false;
|
||||||
|
var dismissed = false;
|
||||||
|
|
||||||
|
// Load Lottie Vector Animation
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = 'assets/js/lottie.min.js';
|
||||||
|
script.onload = function () {
|
||||||
|
if (typeof lottie !== 'undefined') {
|
||||||
|
var target = document.getElementById('cbs-init-lottie-target');
|
||||||
|
if (target) {
|
||||||
|
target.innerHTML = '';
|
||||||
|
lottie.loadAnimation({
|
||||||
|
container: target,
|
||||||
|
renderer: 'svg',
|
||||||
|
loop: true,
|
||||||
|
autoplay: true,
|
||||||
|
path: 'assets/animations/loading_animation.json'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
// Progress simulation stages (Guaranteed ~2 seconds duration on refresh)
|
||||||
|
var interval = setInterval(function () {
|
||||||
|
if (progress < 90) {
|
||||||
|
progress += 5;
|
||||||
|
} else if (angularReady && progress < 100) {
|
||||||
|
progress += 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bar) bar.style.width = Math.min(progress, 100) + '%';
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
if (progress >= 85) {
|
||||||
|
status.innerText = 'Launching CBS Nexora Dashboard...';
|
||||||
|
} else if (progress >= 60) {
|
||||||
|
status.innerText = 'Synchronizing core banking ledger schema...';
|
||||||
|
} else if (progress >= 30) {
|
||||||
|
status.innerText = 'Authenticating token & cryptographic keys...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress >= 100 && angularReady && minTimePassed) {
|
||||||
|
clearInterval(interval);
|
||||||
|
dismissSplash();
|
||||||
|
}
|
||||||
|
}, 90);
|
||||||
|
|
||||||
|
// Enforce minimum hold time (2000ms = 2 seconds) so refresh always shows the animation
|
||||||
|
setTimeout(function () {
|
||||||
|
minTimePassed = true;
|
||||||
|
if (angularReady) {
|
||||||
|
progress = 100;
|
||||||
|
if (bar) bar.style.width = '100%';
|
||||||
|
dismissSplash();
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
function dismissSplash() {
|
||||||
|
if (dismissed) return;
|
||||||
|
dismissed = true;
|
||||||
|
clearInterval(interval);
|
||||||
|
if (bar) bar.style.width = '100%';
|
||||||
|
if (status) status.innerText = 'Ready! System loaded successfully.';
|
||||||
|
setTimeout(function () {
|
||||||
|
if (splash) {
|
||||||
|
splash.classList.add('cbs-splash-fade-out');
|
||||||
|
setTimeout(function () {
|
||||||
|
if (splash && splash.parentNode) {
|
||||||
|
splash.parentNode.removeChild(splash);
|
||||||
|
}
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global hook for Angular to notify when ready
|
||||||
|
window.hideAppSplash = function () {
|
||||||
|
angularReady = true;
|
||||||
|
if (minTimePassed) {
|
||||||
|
dismissSplash();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Safety fallback (Max 7s in case of backend network failure)
|
||||||
|
setTimeout(function () {
|
||||||
|
angularReady = true;
|
||||||
|
minTimePassed = true;
|
||||||
|
dismissSplash();
|
||||||
|
}, 7000);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<mifosx-web-app></mifosx-web-app>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user