feat: add comprehensive loading feedback (#18)

This commit is contained in:
nessar committed 2026-08-17 14:18:15 +02:00
1 parent 45de0cebcc
commit ec3319494b
47 files changed
+1753 -406

No files matched your search

@@ -84,6 +84,17 @@
padding-inline: 0;
font-weight: 750;
}
.count-skeleton {
width: min(260px, 80%);
margin: 0 0 12px;
}
.count-error p {
margin-bottom: 4px;
color: var(--danger);
}
.feature-card-primary mat-progress-bar {
margin-bottom: 12px;
}
.feature-icon {
display: grid;
width: 48px;
@@ -8,7 +8,10 @@
<span class="status-pill"><span></span>Espace sécurisé</span>
</header>
<div class="dashboard-grid">
<article class="feature-card feature-card-primary">
<article
class="feature-card feature-card-primary"
[attr.aria-busy]="workspace.apartmentLoadState().loading"
>
<div class="feature-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M4 21V5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16M2 21h20" />
@@ -17,12 +20,39 @@
</div>
<div>
<p class="eyebrow">Votre portefeuille</p>
<h2>{{ workspace.apartments().length }} appartement{{ workspace.apartments().length > 1 ? 's' : '' }}</h2>
@if (!workspace.apartmentLoadState().loaded && workspace.apartmentLoadState().loading) {
<span
class="skeleton-line skeleton-line-title count-skeleton"
aria-label="Chargement du nombre d’appartements"
></span>
} @else if (
!workspace.apartmentLoadState().loaded && workspace.apartmentLoadState().error
) {
<div class="count-error" role="alert">
<p>{{ workspace.apartmentLoadState().error }}</p>
<button mat-button type="button" (click)="workspace.loadApartments()">Réessayer</button>
</div>
} @else {
<h2>
{{ workspace.apartments().length }} appartement{{
workspace.apartments().length > 1 ? 's' : ''
}}
</h2>
@if (workspace.apartmentLoadState().loading) {
<mat-progress-bar mode="indeterminate" aria-label="Actualisation des appartements" />
}
@if (workspace.apartmentLoadState().error) {
<div class="count-error" role="alert">
<p>{{ workspace.apartmentLoadState().error }}</p>
<button mat-button type="button" (click)="workspace.loadApartments()">
Réessayer
</button>
</div>
}
}
<p>Centralisez les informations essentielles de vos biens.</p>
</div>
<button mat-button type="button" (click)="showApartments()">
Gérer mes appartements
</button>
<button mat-button type="button" (click)="showApartments()">Gérer mes appartements</button>
</article>
<article class="feature-card user-profile">
<div class="large-avatar">{{ userInitial() }}</div>
@@ -1,4 +1,5 @@
import { HttpClient } from '@angular/common/http';
import { signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { of, throwError } from 'rxjs';
@@ -11,10 +12,15 @@ describe('DashboardPage', () => {
const http = { get: vi.fn() };
const router = { navigateByUrl: vi.fn() };
const session = { username: vi.fn(() => 'camille'), email: vi.fn(() => 'camille@example.com') };
const workspace = { apartments: vi.fn(() => []), loadApartments: vi.fn() };
const workspace = {
apartments: signal([]),
apartmentLoadState: signal({ loaded: false, loading: true, error: '' }),
loadApartments: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
workspace.apartmentLoadState.set({ loaded: false, loading: true, error: '' });
TestBed.configureTestingModule({
providers: [
{ provide: HttpClient, useValue: http },
@@ -25,6 +31,14 @@ describe('DashboardPage', () => {
});
});
it('skeletonizes only the apartment count while it loads', () => {
const fixture = TestBed.createComponent(DashboardPage);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.count-skeleton')).not.toBeNull();
expect(fixture.nativeElement.textContent).toContain('Tableau de bord');
});
it('loads the workspace and displays the user initial', () => {
const fixture = TestBed.createComponent(DashboardPage);
@@ -2,12 +2,13 @@ import { Component, computed, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { SessionService } from '../../services/session.service';
import { WorkspaceService } from '../../services/workspace.service';
@Component({
selector: 'app-dashboard-page',
imports: [MatButtonModule],
imports: [MatButtonModule, MatProgressBarModule],
templateUrl: './dashboard-page.html',
styleUrl: './dashboard-page.css',
})
@@ -19,7 +20,9 @@ export class DashboardPage {
readonly userInitial = computed(() => this.session.username().charAt(0).toUpperCase());
readonly helloMessage = signal('');
constructor() { this.workspace.loadApartments(); }
constructor() {
this.workspace.loadApartments();
}
checkConnection(): void {
this.http.get('/api/hello', { responseType: 'text' }).subscribe({
@@ -28,5 +31,7 @@ export class DashboardPage {
});
}
showApartments(): void { this.router.navigateByUrl('/apartments'); }
showApartments(): void {
this.router.navigateByUrl('/apartments');
}
}