feat: add comprehensive loading feedback (#18)
This commit is contained in:
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');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user