Skip to content

Conversation

@shubhamkumar9199
Copy link
Contributor

@shubhamkumar9199 shubhamkumar9199 commented Oct 27, 2025

Description

Implemented a skeleton loader to ensure that the page content displays only after the required data has been fully loaded.
Previously, the content rendered before the data was fetched, causing layout shifts and a poor user experience.
The loader provides a smooth transition while data is being retrieved, making the interface feel more consistent and responsive.

#{Issue Number}

Screenshots, if any

WEB-377

Checklist

before:
https://github.com/user-attachments/assets/d0b39e93-1ad0-4dba-8d89-d0d33311b119
after:
https://github.com/user-attachments/assets/920527a0-bc48-47ee-84b0-58c64d0fec94

Please make sure these boxes are checked before submitting your pull request - thanks!

  • [ X ] If you have multiple commits please combine them into one commit by squashing them.

  • [ X ] Read and understood the contribution guidelines at web-app/.github/CONTRIBUTING.md.

Summary by CodeRabbit

  • New Features

    • Added a versatile skeleton loader with multiple display modes (profile, table, card, list, custom).
    • Profile page now shows an animated loading skeleton while translations load.
  • Style

    • Dark theme support for skeletons with improved colors and contrast.
  • Localization

    • Added new state labels ("Authenticated", "Not Authenticated") to translations.

@coderabbitai
Copy link

coderabbitai bot commented Oct 27, 2025

Walkthrough

Adds a reusable SkeletonLoaderComponent (profile/table/list/card/custom) and integrates it into the profile view. Profile now shows the skeleton while translations load, using a 600ms minimum display timer before revealing the final content. SCSS updated with skeleton styles and dark-theme overrides.

Changes

Cohort / File(s) Summary
Skeleton Loader Component
src/app/shared/skeleton-loader/index.ts, src/app/shared/skeleton-loader/skeleton-loader.component.ts, src/app/shared/skeleton-loader/skeleton-loader.component.html, src/app/shared/skeleton-loader/skeleton-loader.component.scss
New standalone SkeletonLoaderComponent exported via index. Provides configurable inputs (type, items, cssClass, showButtons, buttonCount, tableRows, tableColumns), template variants for profile/table/card/list/custom, shimmer animation styles, and dark-theme overrides.
Profile Component Updates
src/app/profile/profile.component.ts, src/app/profile/profile.component.html, src/app/profile/profile.component.scss
Injects TranslateService and uses isLoadingTranslations flag. Template conditionally renders mifosx-skeleton-loader while loading and full profile content when complete. Implements a 600ms minimum skeleton display in ngOnInit. Adds .skeleton-wrapper styles and dark-mode button/card adjustments.
Translations
src/assets/translations/en-US.json
Added top-level states object containing Authenticated and Not Authenticated.

Sequence Diagram(s)

sequenceDiagram
    participant Profile as ProfileComponent
    participant Transl as TranslateService
    participant Skeleton as SkeletonLoaderComponent
    participant UI as Profile UI

    Profile->>Profile: set isLoadingTranslations = true\nstart 600ms minimum timer
    Profile->>Skeleton: render skeleton (type=profile)
    Skeleton-->>UI: show loading placeholders

    par Translation fetch
        Profile->>Transl: request translation key(s)
        Transl-->>Profile: returns translation(s)
    and Minimum timer
        Profile->>Profile: wait until 600ms elapsed
    end

    Profile->>Profile: set isLoadingTranslations = false
    Skeleton-->>UI: unmount skeleton
    Profile-->>UI: render actual profile content
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Pay attention to the 600ms timer and race between translation response and timer in src/app/profile/profile.component.ts.
  • Review dynamic template loops and trackBy usage in src/app/shared/skeleton-loader/skeleton-loader.component.html.
  • Verify CSS variable/dark-theme overrides in skeleton-loader.component.scss and profile.component.scss to avoid visual regressions.

Suggested reviewers

  • alberto-art3ch

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "WEB-377-fix/content-render-timing" is partially related to the changeset and refers to a real aspect of the primary change. The PR's main objective is to implement a skeleton loader that ensures page content renders only after data has fully loaded, fixing the content-render-timing issue that previously caused layout shifts and poor UX. While the title doesn't explicitly mention the skeleton loader solution, it accurately captures the problem being addressed—ensuring content renders at the appropriate time. The title is specific enough to convey meaningful information and would help teammates understand that this PR addresses rendering timing issues, though it could be more explicit about the skeleton loader implementation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/profile/profile.component.ts (1)

31-49: Component uses imports but lacks standalone: true.

This is a compile-time error in Angular 15+. Add standalone: true.

 @Component({
   selector: 'mifosx-profile',
   templateUrl: './profile.component.html',
   styleUrls: ['./profile.component.scss'],
+  standalone: true,
   imports: [

Optionally add changeDetection: ChangeDetectionStrategy.OnPush for perf.

🧹 Nitpick comments (9)
src/app/profile/profile.component.scss (2)

6-10: Scope generic table borders to the component container.

To avoid unintended styling on nested tables, anchor to .container table th, .container table td.

-table th,
-table td {
+.container table th,
+.container table td {
   border-top: 1px solid var(--border-color, rgb(0 0 0 / 12%));
 }

112-137: Dark theme button overrides may fight Material theming.

Using explicit colors on .mat-raised-button can override palette/theming. Prefer Material tokens or CSS vars bound to theme.

-  button {
-    &.mat-raised-button {
-      background-color: var(--primary-color, #1976d2);
-      color: #fff;
-      &:hover { background-color: var(--primary-hover-color, #1565c0); }
-    }
-  }
+  .mat-raised-button {
+    background-color: var(--mifos-primary, var(--primary-color, #1976d2));
+    color: var(--mifos-on-primary, #fff);
+  }
src/app/profile/profile.component.html (1)

2-4: Add basic a11y: announce loading state.

Expose status to AT and mark busy.

-<div *ngIf="isLoadingTranslations" class="skeleton-wrapper">
-  <mifosx-skeleton-loader type="profile" [showButtons]="true" [buttonCount]="2" [tableRows]="3" [tableColumns]="2">
+<div *ngIf="isLoadingTranslations" class="skeleton-wrapper" role="status" aria-live="polite" aria-busy="true">
+  <mifosx-skeleton-loader type="profile" [showButtons]="true" [buttonCount]="2" [tableRows]="3" [tableColumns]="2" aria-hidden="true">
   </mifosx-skeleton-loader>
src/app/shared/skeleton-loader/skeleton-loader.component.scss (1)

291-361: Prefer CSS variables over class overrides for dark mode + reduced motion.

You already set CSS vars; with the placeholder change above, you can drop most duplication. Also respect prefers-reduced-motion.

 :host-context(.dark-theme) {
   --skeleton-base-bg: #3a3a3a;
   --skeleton-card-bg: #2d2d2d;
   --skeleton-info-box-bg: #383838;
   --skeleton-border-color: #444;
   --skeleton-table-header-bg: #333;
   --skeleton-shimmer-color: rgb(255 255 255 / 10%);
-  .skeleton-base { ... }
-  .skeleton-buttons .skeleton-button { ... }
+  /* With %skeleton-base using vars, most overrides above become unnecessary */
 }
+@media (prefers-reduced-motion: reduce) {
+  .skeleton-base::after { animation: none; }
+}
src/app/shared/skeleton-loader/skeleton-loader.component.ts (1)

10-18: Consider stricter typing and perf defaults.

  • Make type a string-literal union via a reusable type alias or enum.
  • Add ChangeDetectionStrategy.OnPush.
-import { Component, Input } from '@angular/core';
+import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
+export type SkeletonType = 'profile' | 'table' | 'list' | 'card' | 'custom';
 ...
 export class SkeletonLoaderComponent {
-  @Input() type: 'profile' | 'table' | 'list' | 'card' | 'custom' = 'profile';
+  @Input() type: SkeletonType = 'profile';
   @Input() items: number = 1;
   @Input() cssClass: string = '';
   @Input() showButtons: boolean = true;
   @Input() buttonCount: number = 2;
   @Input() tableRows: number = 3;
   @Input() tableColumns: number = 2;
 }

Also update @Component with changeDetection: ChangeDetectionStrategy.OnPush.

src/app/profile/profile.component.ts (3)

65-67: Prefer explicit typing for state.

Add explicit type to aid readability and strictness.

- isLoadingTranslations = true;
+ isLoadingTranslations: boolean = true;

85-103: Simplify min-delay + translation readiness with RxJS and ensure completion.

get() completes, but the timer pattern can be expressed declaratively and guards against errors.

-// Show skeleton for at least 600ms and wait for translations
-const startTime = Date.now();
-const minDisplayTime = 600;
-this.translateService.get('labels.inputs.Tenant Id').subscribe(() => {
-  const elapsedTime = Date.now() - startTime;
-  const remainingTime = Math.max(0, minDisplayTime - elapsedTime);
-  if (remainingTime > 0) {
-    setTimeout(() => { this.isLoadingTranslations = false; }, remainingTime);
-  } else {
-    this.isLoadingTranslations = false;
-  }
-});
+// Wait for translation readiness and a minimum 600ms display
+const minDisplayTime = 600;
+combineLatest([
+  this.translateService.get('labels.inputs.Tenant Id').pipe(take(1), catchError(() => of('labels.inputs.Tenant Id'))),
+  timer(minDisplayTime)
+]).subscribe(() => { this.isLoadingTranslations = false; });

Add imports: combineLatest, timer, of from rxjs, and take, catchError from rxjs/operators.


57-63: Type the table data source.

If you have a Role model, use it to improve type safety.

- dataSource = new MatTableDataSource();
+ dataSource = new MatTableDataSource<Role>();
src/app/shared/skeleton-loader/skeleton-loader.component.html (1)

11-11: Make the info boxes count configurable via component input.

Currently, the info boxes count is hardcoded to 6 (line 11). This should be an input property to match the flexibility of other display modes.

Add an input property to the component:

// In skeleton-loader.component.ts
@Input() infoBoxes: number = 6;

Then update the template:

-      <div class="skeleton-info-box" *ngFor="let i of [].constructor(6); let idx = index">
+      <div class="skeleton-info-box" *ngFor="let i of [].constructor(infoBoxes); let idx = index">
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28c8f7b and 1ab9e2f.

📒 Files selected for processing (7)
  • src/app/profile/profile.component.html (1 hunks)
  • src/app/profile/profile.component.scss (2 hunks)
  • src/app/profile/profile.component.ts (3 hunks)
  • src/app/shared/skeleton-loader/index.ts (1 hunks)
  • src/app/shared/skeleton-loader/skeleton-loader.component.html (1 hunks)
  • src/app/shared/skeleton-loader/skeleton-loader.component.scss (1 hunks)
  • src/app/shared/skeleton-loader/skeleton-loader.component.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/app/**

⚙️ CodeRabbit configuration file

src/app/**: For Angular code: verify component separation, trackBy on *ngFor,
strict type safety, and clean observable patterns.

Files:

  • src/app/shared/skeleton-loader/skeleton-loader.component.scss
  • src/app/shared/skeleton-loader/index.ts
  • src/app/shared/skeleton-loader/skeleton-loader.component.ts
  • src/app/profile/profile.component.html
  • src/app/shared/skeleton-loader/skeleton-loader.component.html
  • src/app/profile/profile.component.ts
  • src/app/profile/profile.component.scss
🧬 Code graph analysis (2)
src/app/shared/skeleton-loader/skeleton-loader.component.ts (2)
src/app/profile/profile.component.ts (1)
  • Component (31-129)
src/app/shared/skeleton-loader/index.ts (1)
  • SkeletonLoaderComponent (1-1)
src/app/profile/profile.component.ts (1)
src/app/shared/skeleton-loader/index.ts (1)
  • SkeletonLoaderComponent (1-1)
🔇 Additional comments (2)
src/app/shared/skeleton-loader/index.ts (1)

1-1: LGTM: clean barrel export.

Re-export looks correct and matches usage app/shared/skeleton-loader.

src/app/shared/skeleton-loader/skeleton-loader.component.html (1)

70-73: Custom type implementation is clean.

The passthrough content area via ng-content with CSS class bindings is well-structured and provides good extensibility.

fix/i18n-and-darkmode-skeleton-profile

fix: content rendering timing and i18n dark mode skeleton issue in profile
@shubhamkumar9199 shubhamkumar9199 force-pushed the fix/content-render-timing branch from 1ab9e2f to 0b8fa32 Compare October 27, 2025 17:58
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/app/shared/skeleton-loader/skeleton-loader.component.html (1)

1-88: Excellent! Previous feedback addressed. Optional: clean up unused index variables.

The template now correctly uses createArray() helper and trackBy: trackByIndex on all *ngFor loops, addressing the previous review feedback.

As an optional improvement, you can remove the unused idx and idx2 variables throughout the template since they're declared but never referenced:

-  *ngFor="let i of createArray(buttonCount); let idx = index; trackBy: trackByIndex"
+  *ngFor="let i of createArray(buttonCount); trackBy: trackByIndex"

-  <div class="skeleton-info-box" *ngFor="let i of createArray(6); let idx = index; trackBy: trackByIndex">
+  <div class="skeleton-info-box" *ngFor="let i of createArray(6); trackBy: trackByIndex">

-    *ngFor="let i of createArray(tableColumns); let idx = index; trackBy: trackByIndex"
+    *ngFor="let i of createArray(tableColumns); trackBy: trackByIndex"

-  <div class="skeleton-table-row" *ngFor="let i of createArray(tableRows); let idx = index; trackBy: trackByIndex">
+  <div class="skeleton-table-row" *ngFor="let i of createArray(tableRows); trackBy: trackByIndex">

-    *ngFor="let j of createArray(tableColumns); let idx2 = index; trackBy: trackByIndex"
+    *ngFor="let j of createArray(tableColumns); trackBy: trackByIndex"

-    *ngFor="let i of createArray(tableColumns); let idx = index; trackBy: trackByIndex"
+    *ngFor="let i of createArray(tableColumns); trackBy: trackByIndex"

-  <div class="skeleton-table-row" *ngFor="let i of createArray(tableRows); let idx = index; trackBy: trackByIndex">
+  <div class="skeleton-table-row" *ngFor="let i of createArray(tableRows); trackBy: trackByIndex">

-    *ngFor="let j of createArray(tableColumns); let idx2 = index; trackBy: trackByIndex"
+    *ngFor="let j of createArray(tableColumns); trackBy: trackByIndex"

-  <div class="skeleton-card-item" *ngFor="let i of createArray(items); let idx = index; trackBy: trackByIndex">
+  <div class="skeleton-card-item" *ngFor="let i of createArray(items); trackBy: trackByIndex">

-  <div class="skeleton-list-item" *ngFor="let i of createArray(items); let idx = index; trackBy: trackByIndex">
+  <div class="skeleton-list-item" *ngFor="let i of createArray(items); trackBy: trackByIndex">
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ab9e2f and 0b8fa32.

📒 Files selected for processing (8)
  • src/app/profile/profile.component.html (1 hunks)
  • src/app/profile/profile.component.scss (2 hunks)
  • src/app/profile/profile.component.ts (3 hunks)
  • src/app/shared/skeleton-loader/index.ts (1 hunks)
  • src/app/shared/skeleton-loader/skeleton-loader.component.html (1 hunks)
  • src/app/shared/skeleton-loader/skeleton-loader.component.scss (1 hunks)
  • src/app/shared/skeleton-loader/skeleton-loader.component.ts (1 hunks)
  • src/assets/translations/en-US.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/app/shared/skeleton-loader/index.ts
  • src/app/shared/skeleton-loader/skeleton-loader.component.scss
  • src/app/profile/profile.component.html
🧰 Additional context used
📓 Path-based instructions (1)
src/app/**

⚙️ CodeRabbit configuration file

src/app/**: For Angular code: verify component separation, trackBy on *ngFor,
strict type safety, and clean observable patterns.

Files:

  • src/app/profile/profile.component.scss
  • src/app/shared/skeleton-loader/skeleton-loader.component.ts
  • src/app/profile/profile.component.ts
  • src/app/shared/skeleton-loader/skeleton-loader.component.html
🧬 Code graph analysis (2)
src/app/shared/skeleton-loader/skeleton-loader.component.ts (2)
src/app/profile/profile.component.ts (1)
  • Component (31-129)
src/app/shared/skeleton-loader/index.ts (1)
  • SkeletonLoaderComponent (1-1)
src/app/profile/profile.component.ts (1)
src/app/shared/skeleton-loader/index.ts (1)
  • SkeletonLoaderComponent (1-1)
🔇 Additional comments (6)
src/assets/translations/en-US.json (1)

2547-2550: Translation keys for authentication states added appropriately.

The new "states" object with "Authenticated" and "Not Authenticated" entries is well-placed and provides necessary i18n support for the skeleton loader's profile view. The JSON structure is valid, key naming is clear and consistent with existing conventions, and the object is properly positioned in the file hierarchy.

src/app/profile/profile.component.scss (1)

1-138: LGTM! Clean styling with proper dark theme support.

The SCSS changes are well-structured with appropriate use of CSS variables, fallbacks, and dark theme overrides. The skeleton-wrapper styling integrates nicely with the existing profile styles.

src/app/profile/profile.component.ts (3)

25-26: LGTM! Appropriate imports for the new functionality.

The TranslateService and SkeletonLoaderComponent imports are correctly added to support the translation-aware loading state.


47-48: LGTM! SkeletonLoaderComponent correctly added to standalone imports.

The component is properly added to the imports array since ProfileComponent is standalone.


65-66: LGTM! Loading state property appropriately declared.

The public boolean property is correctly initialized and suitable for template binding.

src/app/shared/skeleton-loader/skeleton-loader.component.ts (1)

1-27: LGTM! Well-structured reusable skeleton loader component.

The component is correctly implemented as standalone, has properly typed inputs with sensible defaults, and includes the helper methods (trackByIndex, createArray) that support the template's *ngFor loops. The previous feedback regarding standalone: true has been addressed.

Comment on lines +87 to +103

// Show skeleton for at least 600ms and wait for translations
const startTime = Date.now();
const minDisplayTime = 600;

this.translateService.get('labels.inputs.Tenant Id').subscribe(() => {
const elapsedTime = Date.now() - startTime;
const remainingTime = Math.max(0, minDisplayTime - elapsedTime);

if (remainingTime > 0) {
setTimeout(() => {
this.isLoadingTranslations = false;
}, remainingTime);
} else {
this.isLoadingTranslations = false;
}
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clean up subscription and timeout on component destroy.

The translation subscription and setTimeout are not cleaned up, which can cause memory leaks and errors if the component is destroyed before they complete.

Apply this diff to add proper cleanup:

+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Subject, takeUntil } from 'rxjs';

-export class ProfileComponent implements OnInit {
+export class ProfileComponent implements OnInit, OnDestroy {
+  private destroy$ = new Subject<void>();
+  private loadingTimeout?: number;

   /** Loading state for translations */
   isLoadingTranslations = true;

   ngOnInit() {
     this.dataSource = new MatTableDataSource(this.profileData.roles);

     // Show skeleton for at least 600ms and wait for translations
     const startTime = Date.now();
     const minDisplayTime = 600;

-    this.translateService.get('labels.inputs.Tenant Id').subscribe(() => {
+    this.translateService.get('labels.inputs.Tenant Id')
+      .pipe(takeUntil(this.destroy$))
+      .subscribe(() => {
       const elapsedTime = Date.now() - startTime;
       const remainingTime = Math.max(0, minDisplayTime - elapsedTime);

       if (remainingTime > 0) {
-        setTimeout(() => {
+        this.loadingTimeout = window.setTimeout(() => {
           this.isLoadingTranslations = false;
         }, remainingTime);
       } else {
         this.isLoadingTranslations = false;
       }
     });
   }

+  ngOnDestroy() {
+    this.destroy$.next();
+    this.destroy$.complete();
+    if (this.loadingTimeout) {
+      clearTimeout(this.loadingTimeout);
+    }
+  }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/profile/profile.component.ts around lines 87 to 103, the
translateService subscription and the setTimeout timer are never cleaned up
which can leak memory or trigger after the component is destroyed; store the
Subscription returned from translateService.get(...) and store the timeout id
returned by setTimeout (if any), implement ngOnDestroy to call
subscription.unsubscribe() and clearTimeout(timeoutId) (guarding for undefined),
and ensure isLoadingTranslations is not toggled after destroy by checking a
destroyed flag or relying on the unsubscribe/clearTimeout to prevent callbacks.

Copy link
Collaborator

@steinwinde steinwinde left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not in favour of addressing this content loading problem in this way at all. Why is the static content of the SPA displayed later than the Fineract data arrives in the browser?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants