From bd421954c50fed8344a3038a2c839620a74df702 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Wed, 5 Aug 2026 11:25:36 +0200 Subject: [PATCH] skeleton loading --- .../features/public-team/public-player.html | 18 ++++++++--- .../features/public-team/public-player.scss | 3 ++ .../app/features/public-team/public-player.ts | 5 +-- .../app/features/public-team/public-team.html | 32 ++++++++++++++++++- .../app/features/public-team/public-team.scss | 11 +++++++ .../app/features/public-team/public-team.ts | 6 ++-- .../src/app/shared/skeleton/skeleton.html | 7 ++++ .../src/app/shared/skeleton/skeleton.scss | 31 ++++++++++++++++++ .../src/app/shared/skeleton/skeleton.spec.ts | 30 +++++++++++++++++ .../src/app/shared/skeleton/skeleton.ts | 12 +++++++ 10 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.html create mode 100644 myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.scss create mode 100644 myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.spec.ts create mode 100644 myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.ts diff --git a/myteamwallet_frontend_modern/src/app/features/public-team/public-player.html b/myteamwallet_frontend_modern/src/app/features/public-team/public-player.html index 7f6160c..34f640b 100644 --- a/myteamwallet_frontend_modern/src/app/features/public-team/public-player.html +++ b/myteamwallet_frontend_modern/src/app/features/public-team/public-player.html @@ -6,12 +6,22 @@
arrow_backZur Teamübersicht
-

Öffentliche Ansicht

-

{{ player()?.firstName }} {{ player()?.lastName }}

-

Die letzten Buchungen dieses Mitglieds.

+ @if (loading()) { + + + + } @else { +

Öffentliche Ansicht

+

{{ player()?.firstName }} {{ player()?.lastName }}

+

Die letzten Buchungen dieses Mitglieds.

+ }
@if (loading()) { -
+
+ @for (row of skeletonRows; track row) { + + } +
} @else if (notFound()) {
search_offVerlauf nicht gefunden.
} @else if (transactions().length === 0) { diff --git a/myteamwallet_frontend_modern/src/app/features/public-team/public-player.scss b/myteamwallet_frontend_modern/src/app/features/public-team/public-player.scss index c3859af..d35a6cc 100644 --- a/myteamwallet_frontend_modern/src/app/features/public-team/public-player.scss +++ b/myteamwallet_frontend_modern/src/app/features/public-team/public-player.scss @@ -37,6 +37,9 @@ main { .page-title p { margin-top: 0; } +.page-title app-skeleton + app-skeleton { + margin-top: 10px; +} .eyebrow { color: var(--mat-sys-primary); font-size: 0.75rem; diff --git a/myteamwallet_frontend_modern/src/app/features/public-team/public-player.ts b/myteamwallet_frontend_modern/src/app/features/public-team/public-player.ts index b6dc58d..cf05410 100644 --- a/myteamwallet_frontend_modern/src/app/features/public-team/public-player.ts +++ b/myteamwallet_frontend_modern/src/app/features/public-team/public-player.ts @@ -5,10 +5,10 @@ import { ActivatedRoute, RouterLink } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { PublicTeamApi } from '../../core/team/public-team-api'; import { PlayerTransaction } from '../../models/transaction.model'; import { PublicPlayer as PublicPlayerModel } from '../../models/public-access.model'; +import { Skeleton } from '../../shared/skeleton/skeleton'; import { TransactionAmount } from '../../shared/transaction-amount/transaction-amount'; registerLocaleData(localeDe); @@ -21,7 +21,7 @@ registerLocaleData(localeDe); MatButtonModule, MatCardModule, MatIconModule, - MatProgressSpinnerModule, + Skeleton, TransactionAmount, ], providers: [{ provide: LOCALE_ID, useValue: 'de-DE' }], @@ -36,6 +36,7 @@ export class PublicPlayer { protected readonly transactions = signal([]); protected readonly loading = signal(true); protected readonly notFound = signal(false); + protected readonly skeletonRows = [0, 1, 2, 3, 4]; constructor() { const playerId = Number(this.route.snapshot.paramMap.get('playerId')); diff --git a/myteamwallet_frontend_modern/src/app/features/public-team/public-team.html b/myteamwallet_frontend_modern/src/app/features/public-team/public-team.html index eb98620..29a7b85 100644 --- a/myteamwallet_frontend_modern/src/app/features/public-team/public-team.html +++ b/myteamwallet_frontend_modern/src/app/features/public-team/public-team.html @@ -6,7 +6,37 @@
@if (loading()) { -
Team wird geladen …
+
+
+ + +
+ + + +
+
+
+
+ + +
+ @for (row of skeletonMemberRows; track row) { + + } +
+
+ +
+
} @else if (notFound() || !team()) {
search_off diff --git a/myteamwallet_frontend_modern/src/app/features/public-team/public-team.scss b/myteamwallet_frontend_modern/src/app/features/public-team/public-team.scss index def7521..ee189f2 100644 --- a/myteamwallet_frontend_modern/src/app/features/public-team/public-team.scss +++ b/myteamwallet_frontend_modern/src/app/features/public-team/public-team.scss @@ -39,6 +39,13 @@ main { text-transform: uppercase; margin: 0 0 6px; } +.hero app-skeleton + app-skeleton { + margin: 8px 0 20px; +} +.content-grid > div > app-skeleton, +.content-grid > aside > app-skeleton { + margin-bottom: 8px; +} .balance-grid { display: grid; grid-template-columns: repeat(3, 1fr); @@ -81,6 +88,10 @@ aside h2 { .search { width: 100%; } +.list-skeleton { + display: grid; + gap: 10px; +} .player-list { overflow: hidden; border: 1px solid var(--mat-sys-outline-variant); diff --git a/myteamwallet_frontend_modern/src/app/features/public-team/public-team.ts b/myteamwallet_frontend_modern/src/app/features/public-team/public-team.ts index 1500c70..186948b 100644 --- a/myteamwallet_frontend_modern/src/app/features/public-team/public-team.ts +++ b/myteamwallet_frontend_modern/src/app/features/public-team/public-team.ts @@ -7,10 +7,10 @@ import { MatCardModule } from '@angular/material/card'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { PublicTeamApi } from '../../core/team/public-team-api'; import { Penalty } from '../../models/penalty.model'; import { PublicTeamOverview } from '../../models/public-access.model'; +import { Skeleton } from '../../shared/skeleton/skeleton'; registerLocaleData(localeDe); @@ -24,7 +24,7 @@ registerLocaleData(localeDe); MatFormFieldModule, MatIconModule, MatInputModule, - MatProgressSpinnerModule, + Skeleton, ], providers: [{ provide: LOCALE_ID, useValue: 'de-DE' }], templateUrl: './public-team.html', @@ -38,6 +38,8 @@ export class PublicTeam { protected readonly loading = signal(true); protected readonly notFound = signal(false); protected readonly search = signal(''); + protected readonly skeletonMemberRows = [0, 1, 2, 3, 4]; + protected readonly skeletonPenaltyRows = [0, 1, 2]; protected readonly players = computed(() => { const query = this.search().trim().toLocaleLowerCase('de'); return (this.team()?.players ?? []) diff --git a/myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.html b/myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.html new file mode 100644 index 0000000..7bfabaa --- /dev/null +++ b/myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.html @@ -0,0 +1,7 @@ + diff --git a/myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.scss b/myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.scss new file mode 100644 index 0000000..63fc2a9 --- /dev/null +++ b/myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.scss @@ -0,0 +1,31 @@ +:host { + display: block; +} + +.skeleton { + display: block; + background: linear-gradient( + 100deg, + var(--mat-sys-surface-container) 30%, + var(--mat-sys-surface-container-high) 50%, + var(--mat-sys-surface-container) 70% + ); + background-size: 200% 100%; + animation: skeleton-shimmer 1.4s ease-in-out infinite; +} + +@keyframes skeleton-shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +@media (prefers-reduced-motion: reduce) { + .skeleton { + animation: none; + background: var(--mat-sys-surface-container-high); + } +} diff --git a/myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.spec.ts b/myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.spec.ts new file mode 100644 index 0000000..df89998 --- /dev/null +++ b/myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.spec.ts @@ -0,0 +1,30 @@ +import { TestBed } from '@angular/core/testing'; +import { Skeleton } from './skeleton'; + +describe('Skeleton', () => { + it('renders a placeholder block sized from its inputs and hidden from screen readers', async () => { + await TestBed.configureTestingModule({ imports: [Skeleton] }).compileComponents(); + const fixture = TestBed.createComponent(Skeleton); + fixture.componentRef.setInput('width', '60%'); + fixture.componentRef.setInput('height', '24px'); + fixture.componentRef.setInput('radius', '12px'); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('.skeleton'); + expect(element.getAttribute('aria-hidden')).toBe('true'); + expect(element.style.width).toBe('60%'); + expect(element.style.height).toBe('24px'); + expect(element.style.borderRadius).toBe('12px'); + }); + + it('falls back to sensible default dimensions', async () => { + await TestBed.configureTestingModule({ imports: [Skeleton] }).compileComponents(); + const fixture = TestBed.createComponent(Skeleton); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('.skeleton'); + expect(element.style.width).toBe('100%'); + expect(element.style.height).toBe('16px'); + expect(element.style.borderRadius).toBe('8px'); + }); +}); diff --git a/myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.ts b/myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.ts new file mode 100644 index 0000000..8c9819e --- /dev/null +++ b/myteamwallet_frontend_modern/src/app/shared/skeleton/skeleton.ts @@ -0,0 +1,12 @@ +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'app-skeleton', + templateUrl: './skeleton.html', + styleUrl: './skeleton.scss', +}) +export class Skeleton { + readonly width = input('100%'); + readonly height = input('16px'); + readonly radius = input('8px'); +}