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"
|
||||
],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/apple-touch-icon.png",
|
||||
"src/assets/images/cbsnexora-favicon.svg",
|
||||
"src/robots.txt",
|
||||
"src/manifest.json",
|
||||
@@ -39,6 +41,11 @@
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/tinymce",
|
||||
"output": "assets/tinymce"
|
||||
},
|
||||
{
|
||||
"glob": "lottie.min.js",
|
||||
"input": "node_modules/lottie-web/build/player",
|
||||
"output": "assets/js"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
|
||||
Generated
+7
@@ -41,6 +41,7 @@
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"lightgallery": "^2.9.0",
|
||||
"lodash": "4.17.23",
|
||||
"lottie-web": "^5.13.0",
|
||||
"moment": "^2.29.4",
|
||||
"ngx-image-cropper": "^9.1.6",
|
||||
"ngx-mat-select-search": "^8.0.2",
|
||||
@@ -18380,6 +18381,12 @@
|
||||
"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": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
"lightgallery": "^2.9.0",
|
||||
"lodash": "4.17.23",
|
||||
"lottie-web": "^5.13.0",
|
||||
"moment": "^2.29.4",
|
||||
"ngx-image-cropper": "^9.1.6",
|
||||
"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 { environment } from '../environments/environment';
|
||||
import { CallbackComponent } from './zitadel/callback/callback.component';
|
||||
import { LoadingScreenComponent } from './shared/loading-screen/loading-screen.component';
|
||||
import { OAuthModule } from 'angular-oauth2-oidc';
|
||||
|
||||
export class CustomMissingTranslationHandler implements MissingTranslationHandler {
|
||||
@@ -195,6 +196,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||
AppRoutingModule,
|
||||
NotFoundComponent,
|
||||
CallbackComponent,
|
||||
LoadingScreenComponent,
|
||||
OAuthModule.forRoot()
|
||||
],
|
||||
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 { ServerSelectorComponent } from './server-selector/server-selector.component';
|
||||
import { TenantSelectorComponent } from './tenant-selector/tenant-selector.component';
|
||||
import { LoadingScreenComponent } from './loading-screen/loading-screen.component';
|
||||
|
||||
/** Custom Modules */
|
||||
import { IconsModule } from './icons.module';
|
||||
@@ -116,9 +117,11 @@ import { DatetimePickerStubComponent } from './datetime-picker-stub/datetime-pic
|
||||
DropdownComponent,
|
||||
InputAmountComponent,
|
||||
InputPasswordComponent,
|
||||
LoadingScreenComponent,
|
||||
DatetimePickerStubComponent // temporarily added stub for datetime picker
|
||||
],
|
||||
exports: [
|
||||
LoadingScreenComponent,
|
||||
FileUploadComponent,
|
||||
FooterComponent,
|
||||
LanguageSelectorComponent,
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
-->
|
||||
|
||||
<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() {
|
||||
|
||||
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/.
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
Modernized Loader Wrapper (Inspired by openMF/mifos-mobile)
|
||||
Supports full-screen and embedded containers with smooth vector physics.
|
||||
========================================================================== */
|
||||
.loader-wrapper {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
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 {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
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 {
|
||||
border-width: 0 90px 90px;
|
||||
border-color: transparent transparent #1fb050;
|
||||
bottom: calc(50% - 100px);
|
||||
left: calc(50% - 90px);
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-width: 3px;
|
||||
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 {
|
||||
border-width: 90px 90px 0;
|
||||
border-color: #b4d575 transparent transparent;
|
||||
top: calc(50% - 100px);
|
||||
left: calc(50% - 90px);
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-width: 3px;
|
||||
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 {
|
||||
border-width: 90px 0 90px 90px;
|
||||
border-color: transparent transparent transparent #1074b9;
|
||||
top: calc(50% - 90px);
|
||||
left: calc(50% - 100px);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-width: 2.5px;
|
||||
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 {
|
||||
border-width: 90px 90px 90px 0;
|
||||
border-color: transparent #1daeec transparent transparent;
|
||||
top: calc(50% - 90px);
|
||||
right: calc(50% - 100px);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-width: 2px;
|
||||
border-color: transparent #38bdf8 transparent transparent;
|
||||
animation: cbs-spin-ring 0.7s cubic-bezier(0.5, 0, 0.5, 1) infinite reverse;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
/* Optional central glowing core */
|
||||
&::after {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
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">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{{'APP_NAME' | translate}}</title>
|
||||
<title>CBS Nexora</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
@@ -111,13 +111,264 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<mifosx-web-app>
|
||||
<div class="loader-wrapper">
|
||||
<div class="bottom triangle"></div>
|
||||
<div class="top triangle"></div>
|
||||
<div class="left triangle"></div>
|
||||
<div class="right triangle"></div>
|
||||
<div id="cbs-app-splash-screen" class="cbs-init-screen">
|
||||
<style>
|
||||
.cbs-init-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
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>
|
||||
</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>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user