diff --git a/src/app/profile/profile.component.html b/src/app/profile/profile.component.html index f4ce664ccc..2576231f18 100644 --- a/src/app/profile/profile.component.html +++ b/src/app/profile/profile.component.html @@ -1,86 +1,99 @@ -
- - + +
+ +
-
- -
-
-
- {{ 'labels.inputs.Tenant Id' | translate }} + + +
+
+ + +
+
+ +
+
+
+ {{ 'labels.inputs.Tenant Id' | translate }} +
+
+ {{ tenantIdentifier }} +
-
- {{ tenantIdentifier }} +
+
+ {{ 'labels.inputs.User Id' | translate }} +
+
+ {{ profileData.userId }} +
-
-
-
- {{ 'labels.inputs.User Id' | translate }} -
-
- {{ profileData.userId }} -
-
-
-
- {{ 'labels.inputs.User Name' | translate }} -
-
- {{ profileData.username }} +
+
+ {{ 'labels.inputs.User Name' | translate }} +
+
+ {{ profileData.username }} +
-
-
-
- {{ 'labels.inputs.Office' | translate }} +
+
+ {{ 'labels.inputs.Office' | translate }} +
+
+ {{ profileData.officeName }} +
-
- {{ profileData.officeName }} -
-
-
-
- {{ 'labels.inputs.Status' | translate }} -
-
- {{ profileData.authenticated ? 'Authenticated' : 'Not Authenticated' }} -
-
-
-
- {{ 'labels.inputs.Language' | translate }} +
+
+ {{ 'labels.inputs.Status' | translate }} +
+
+ {{ + profileData.authenticated + ? ('labels.states.Authenticated' | translate) + : ('labels.states.Not Authenticated' | translate) + }} +
-
- {{ language }} +
+
+ {{ 'labels.inputs.Language' | translate }} +
+
+ {{ language }} +
-
- - - - - - - - - - - - - -
- {{ 'labels.inputs.Role' | translate }} - - {{ role.name }} - - {{ 'labels.inputs.Description' | translate }} - - {{ role.description }} -
-
+ + + + + + + + + + + + + +
+ {{ 'labels.inputs.Role' | translate }} + + {{ role.name }} + + {{ 'labels.inputs.Description' | translate }} + + {{ role.description }} +
+
+
diff --git a/src/app/profile/profile.component.scss b/src/app/profile/profile.component.scss index 0de57d301f..cbfed0e3cd 100644 --- a/src/app/profile/profile.component.scss +++ b/src/app/profile/profile.component.scss @@ -1,3 +1,14 @@ +.skeleton-wrapper { + display: block; + width: 100%; +} + +/* Generic table styles should come before specific ones */ +table th, +table td { + border-top: 1px solid var(--border-color, rgb(0 0 0 / 12%)); +} + .container { max-width: 37rem; padding: 1rem; @@ -76,11 +87,6 @@ } } -.container table th, -.container table td { - border-top: 1px solid var(--border-color, rgb(0 0 0 / 12%)); -} - th.mat-header-cell:not(:first-of-type), td.mat-cell:not(:first-of-type) { border-left: 1px solid var(--border-color, rgb(0 0 0 / 12%)); @@ -92,7 +98,6 @@ td.mat-cell:not(:first-of-type) { border-radius: 6px; } -// Dark mode styles :host-context(.dark-theme) { --border-color: #444; --card-background: #2d2d2d; @@ -107,10 +112,27 @@ td.mat-cell:not(:first-of-type) { .container { mat-card { box-shadow: 0 2px 4px rgb(0 0 0 / 30%); + background-color: var(--card-background); + color: var(--text-color); } table { box-shadow: 0 1px 3px rgb(0 0 0 / 30%); } } + + .skeleton-wrapper { + background-color: transparent; + } + + button { + &.mat-raised-button { + background-color: var(--primary-color, #1976d2); + color: #fff; + + &:hover { + background-color: var(--primary-hover-color, #1565c0); + } + } + } } diff --git a/src/app/profile/profile.component.ts b/src/app/profile/profile.component.ts index 3c11986b84..0dd5818fe7 100644 --- a/src/app/profile/profile.component.ts +++ b/src/app/profile/profile.component.ts @@ -22,6 +22,8 @@ import { ChangePasswordDialogComponent } from 'app/shared/change-password-dialog import { SettingsService } from 'app/settings/settings.service'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { TranslateService } from '@ngx-translate/core'; +import { SkeletonLoaderComponent } from 'app/shared/skeleton-loader'; /** * Profile Component. @@ -42,7 +44,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatHeaderRowDef, MatHeaderRow, MatRowDef, - MatRow + MatRow, + SkeletonLoaderComponent ] }) export class ProfileComponent implements OnInit { @@ -59,23 +62,45 @@ export class ProfileComponent implements OnInit { 'description' ]; + /** Loading state for translations */ + isLoadingTranslations = true; + /** * @param {AuthenticationService} authenticationService Authentication Service * @param {UserService} userService Users Service * @param {Router} router Router * @param {MatDialog} dialog Mat Dialog + * @param {TranslateService} translateService Translate Service */ constructor( private authenticationService: AuthenticationService, private settingsService: SettingsService, private router: Router, - public dialog: MatDialog + public dialog: MatDialog, + private translateService: TranslateService ) { this.profileData = authenticationService.getCredentials(); } 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(() => { + const elapsedTime = Date.now() - startTime; + const remainingTime = Math.max(0, minDisplayTime - elapsedTime); + + if (remainingTime > 0) { + setTimeout(() => { + this.isLoadingTranslations = false; + }, remainingTime); + } else { + this.isLoadingTranslations = false; + } + }); } /** diff --git a/src/app/shared/skeleton-loader/index.ts b/src/app/shared/skeleton-loader/index.ts new file mode 100644 index 0000000000..8ea16d5b95 --- /dev/null +++ b/src/app/shared/skeleton-loader/index.ts @@ -0,0 +1 @@ +export { SkeletonLoaderComponent } from './skeleton-loader.component'; diff --git a/src/app/shared/skeleton-loader/skeleton-loader.component.html b/src/app/shared/skeleton-loader/skeleton-loader.component.html new file mode 100644 index 0000000000..027edf8b84 --- /dev/null +++ b/src/app/shared/skeleton-loader/skeleton-loader.component.html @@ -0,0 +1,88 @@ + +
+ +
+
+
+ + +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+
+
+ + +
+ +
diff --git a/src/app/shared/skeleton-loader/skeleton-loader.component.scss b/src/app/shared/skeleton-loader/skeleton-loader.component.scss new file mode 100644 index 0000000000..a11f702660 --- /dev/null +++ b/src/app/shared/skeleton-loader/skeleton-loader.component.scss @@ -0,0 +1,368 @@ +/* stylelint-disable no-descending-specificity */ +/* stylelint-disable color-function-notation */ +/* stylelint-disable alpha-value-notation */ +/* stylelint-disable declaration-block-no-redundant-longhand-properties */ +/* stylelint-disable unit-allowed-list */ +/* stylelint-disable selector-max-universal */ + +/* Animations */ +@keyframes skeleton-shimmer { + 0% { + background-position: -200% 0; + } + + 100% { + background-position: 200% 0; + } +} + +/* Base SCSS placeholder */ +%skeleton-base { + background: var(--skeleton-base-bg, #f0f0f0); + border-radius: 4px; + position: relative; + overflow: hidden; + + &::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + 90deg, + transparent, + var(--skeleton-shimmer-color, rgb(255 255 255 / 60%)) 50%, + transparent + ); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s ease-in-out infinite; + } +} + +/* Base skeleton styles */ +.skeleton-base { + @extend %skeleton-base; +} + +/* Profile Skeleton */ +.skeleton-loader-profile { + max-width: 37rem; + padding: 1rem; + margin: 0 auto; + + .skeleton-buttons { + display: flex; + gap: 1rem; + margin-bottom: 1rem; + + .skeleton-button { + @extend %skeleton-base; + + width: 200px; + height: 40px; + } + } + + .skeleton-card { + margin-bottom: 1rem; + padding: 1.5rem; + border-radius: 6px; + background-color: #fff; + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); + border: 1px solid #ddd; + + .layout-row-wrap { + display: grid; + grid-template-columns: 50% 50%; + gap: 1rem; + + .skeleton-info-box { + display: flex; + flex-direction: column; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 6px; + background-color: #fff; + + .skeleton-header { + @extend %skeleton-base; + + width: 50%; + height: 16px; + margin-bottom: 0.5rem; + } + + .skeleton-text { + @extend %skeleton-base; + + width: 70%; + height: 14px; + } + } + } + + .skeleton-table { + width: 100%; + margin-top: 0.5rem; + + .skeleton-table-header { + display: grid; + grid-template-columns: repeat(var(--columns, 2), 1fr); + gap: 1rem; + margin-bottom: 0; + padding-bottom: 0.75rem; + border-bottom: 1px solid rgb(0 0 0 / 12%); + + .skeleton-cell { + @extend %skeleton-base; + + height: 22px; + } + } + + .skeleton-table-row { + display: grid; + grid-template-columns: repeat(var(--columns, 2), 1fr); + gap: 1rem; + margin-bottom: 0; + padding: 0.75rem 0; + border-bottom: 1px solid rgb(0 0 0 / 12%); + + .skeleton-cell { + @extend %skeleton-base; + + height: 20px; + } + } + } + } +} + +/* Table Skeleton */ +.skeleton-loader-table { + .skeleton-table { + border-radius: 8px; + overflow: hidden; + + .skeleton-table-header { + display: grid; + grid-template-columns: repeat(var(--columns, 2), 1fr); + gap: 1rem; + padding: 1rem; + background-color: #f5f5f5; + border-bottom: 2px solid #e0e0e0; + + .skeleton-cell { + @extend %skeleton-base; + + height: 20px; + } + } + + .skeleton-table-row { + display: grid; + grid-template-columns: repeat(var(--columns, 2), 1fr); + gap: 1rem; + padding: 1rem; + border-bottom: 1px solid #f0f0f0; + + .skeleton-cell { + @extend %skeleton-base; + + height: 18px; + } + } + } +} + +/* Card Skeleton */ +.skeleton-loader-card { + display: grid; + gap: 1.5rem; + + .skeleton-card-item { + padding: 1.5rem; + border-radius: 8px; + background-color: #fff; + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); + + .skeleton-card-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + + .skeleton-avatar { + @extend %skeleton-base; + + width: 50px; + height: 50px; + border-radius: 50%; + } + + .skeleton-card-title { + @extend %skeleton-base; + + width: 60%; + height: 20px; + } + } + + .skeleton-card-content { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1rem; + + .skeleton-line { + @extend %skeleton-base; + + height: 12px; + width: 100%; + } + + .skeleton-line-90 { + width: 90%; + } + + .skeleton-line-70 { + width: 70%; + } + + .skeleton-line-85 { + width: 85%; + } + } + + .skeleton-card-footer { + @extend %skeleton-base; + + width: 30%; + height: 30px; + } + } +} + +/* List Skeleton */ +.skeleton-loader-list { + display: flex; + flex-direction: column; + gap: 1rem; + + .skeleton-list-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + border-radius: 8px; + background-color: #fff; + box-shadow: 0 1px 3px rgb(0 0 0 / 10%); + + .skeleton-avatar { + @extend %skeleton-base; + + width: 40px; + height: 40px; + border-radius: 50%; + } + + .skeleton-list-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; + + .skeleton-line { + @extend %skeleton-base; + + height: 14px; + } + + .skeleton-line-60 { + width: 60%; + } + + .skeleton-line-40 { + width: 40%; + } + } + } +} + +/* Custom Skeleton */ +.skeleton-loader-custom { + @extend %skeleton-base; +} + +/* Dark mode styles */ +: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 { + background: var(--skeleton-base-bg); + + &::after { + background: linear-gradient(90deg, transparent, var(--skeleton-shimmer-color) 50%, transparent); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s ease-in-out infinite; + } + } + + .skeleton-loader-profile { + .skeleton-card { + background-color: var(--skeleton-card-bg); + box-shadow: 0 2px 4px rgb(0 0 0 / 30%); + border-color: var(--skeleton-border-color); + + .skeleton-info-box { + background-color: var(--skeleton-info-box-bg); + border-color: var(--skeleton-border-color); + } + + .skeleton-table { + .skeleton-table-header, + .skeleton-table-row { + border-bottom-color: var(--skeleton-border-color); + } + } + } + } + + .skeleton-loader-table { + .skeleton-table { + .skeleton-table-header { + background-color: var(--skeleton-table-header-bg); + border-bottom-color: #555; + } + + .skeleton-table-row { + border-bottom-color: var(--skeleton-border-color); + } + } + } + + .skeleton-card-item, + .skeleton-list-item { + background-color: var(--skeleton-card-bg); + box-shadow: 0 2px 4px rgb(0 0 0 / 30%); + border-color: var(--skeleton-border-color); + } + + .skeleton-buttons { + .skeleton-button { + background: var(--skeleton-base-bg); + + &::after { + background: linear-gradient(90deg, transparent, var(--skeleton-shimmer-color) 50%, transparent); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s ease-in-out infinite; + } + } + } +} + +/* stylelint-enable */ diff --git a/src/app/shared/skeleton-loader/skeleton-loader.component.ts b/src/app/shared/skeleton-loader/skeleton-loader.component.ts new file mode 100644 index 0000000000..3925440a51 --- /dev/null +++ b/src/app/shared/skeleton-loader/skeleton-loader.component.ts @@ -0,0 +1,27 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'mifosx-skeleton-loader', + templateUrl: './skeleton-loader.component.html', + styleUrls: ['./skeleton-loader.component.scss'], + standalone: true, + imports: [CommonModule] +}) +export class SkeletonLoaderComponent { + @Input() type: 'profile' | 'table' | 'list' | 'card' | 'custom' = 'profile'; + @Input() items: number = 1; + @Input() cssClass: string = ''; + @Input() showButtons: boolean = true; + @Input() buttonCount: number = 2; + @Input() tableRows: number = 3; + @Input() tableColumns: number = 2; + + trackByIndex(index: number): number { + return index; + } + + createArray(length: number): number[] { + return Array.from({ length }, (_, i) => i); + } +} diff --git a/src/assets/translations/en-US.json b/src/assets/translations/en-US.json index 5ee1253c18..73642fb737 100644 --- a/src/assets/translations/en-US.json +++ b/src/assets/translations/en-US.json @@ -2544,6 +2544,10 @@ "User Manual": "User Manual", "Working with Code": "Working with Code" }, + "states": { + "Authenticated": "Authenticated", + "Not Authenticated": "Not Authenticated" + }, "menus": { "Accounting": "Accounting", "Account Overview": "Account Overview",