From 5addba879a462007d3bfc0ae47f8c7e06447da9a Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Fri, 31 Oct 2025 16:19:02 +0100 Subject: [PATCH] Add error handling and snackbar notifications Introduces error state management in LoadingService, displays error bar and snackbar notifications in the UI, and updates ScheduleComponent to use the new error handling. Also adds custom theming for error snackbar and progress bar. --- src/app/app-module.ts | 6 +- src/app/header-2/header-2.component.html | 4 +- src/app/header-2/header-2.component.ts | 7 +-- src/app/loading.service.ts | 74 ++++++++++++++++++++++-- src/app/schedule/schedule.component.ts | 20 ++++--- src/custom-theme.scss | 31 +++++++--- 6 files changed, 113 insertions(+), 29 deletions(-) diff --git a/src/app/app-module.ts b/src/app/app-module.ts index 06cbe6a..803e39c 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -10,7 +10,8 @@ import { App } from './app'; import { MatIconModule } from '@angular/material/icon'; import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; import { HeaderComponent } from './header/header.component'; @@ -60,7 +61,8 @@ import { Header2Component } from './header-2/header-2.component'; MatIconModule, MatTabsModule, MatToolbarModule, - MatProgressBarModule + MatProgressBarModule, + MatSnackBarModule ], providers: [ provideBrowserGlobalErrorListeners(), diff --git a/src/app/header-2/header-2.component.html b/src/app/header-2/header-2.component.html index 8f1c08a..9545660 100644 --- a/src/app/header-2/header-2.component.html +++ b/src/app/header-2/header-2.component.html @@ -24,7 +24,9 @@ - @if (loading$ | async) { + @if (loadingService.error$ | async) { +
+ } @else if (loadingService.loading$ | async){ (false); - readonly loading$ = this._loading.asObservable(); + private loadingSubject = new BehaviorSubject(false); + private errorSubject = new BehaviorSubject(false); - show() { - this._loading.next(true); + public loading$ = this.loadingSubject.asObservable(); + public error$ = this.errorSubject.asObservable(); + + constructor(private snackBar: MatSnackBar) {} + + show(): void { + this.loadingSubject.next(true); + this.errorSubject.next(false); } - hide() { - this._loading.next(false); + hide(): void { + this.loadingSubject.next(false); + this.errorSubject.next(false); + } + + showError(messageOrError?: string | HttpErrorResponse | any): void { + this.loadingSubject.next(false); + this.errorSubject.next(true); + + if (!messageOrError) { + return; + } + + const message = this.getErrorMessage(messageOrError); + + const snackBarRef = this.snackBar.open(message, 'Schließen', { + duration: 0, + panelClass: ['error-snackbar'], + horizontalPosition: 'center', + verticalPosition: 'bottom' + }); + + snackBarRef.afterDismissed().subscribe(() => { + this.hide(); + }); + } + + private getErrorMessage(error?: string | HttpErrorResponse | any): string { + + if (typeof error === 'string') { + return error; + } + + if (error instanceof HttpErrorResponse) { + + if (error.status === 0) { + return 'Netzwerkfehler: Keine Verbindung zum Server möglich!'; + } + + if (error.status >= 500) { + return `Serverfehler (${error.status}): ${error.statusText || 'Interner Serverfehler'}`; + } + + if (error.status >= 400) { + const errorMessage = error.error?.message || error.error?.error || error.statusText; + return `Fehler (${error.status}): ${errorMessage}`; + } + + return `HTTP Fehler (${error.status}): ${error.statusText}`; + } + + if (error.message) { + return error.message; + } + + return 'Ein unbekannter Fehler ist aufgetreten!'; } } diff --git a/src/app/schedule/schedule.component.ts b/src/app/schedule/schedule.component.ts index 7e89034..c7bbc7a 100644 --- a/src/app/schedule/schedule.component.ts +++ b/src/app/schedule/schedule.component.ts @@ -4,6 +4,7 @@ import { Vorstellung } from '@infinimotion/model-frontend'; import { Performance } from '../model/performance.model'; import { MovieGroup } from '../model/movie-group.model'; import { LoadingService } from '../loading.service'; +import { catchError, map, of, tap } from 'rxjs'; @Component({ selector: 'app-schedule', @@ -47,14 +48,19 @@ export class ScheduleComponent implements OnInit { loadPerformances() { this.loading.show(); - this.http.getPerformaces().subscribe({ - next: (data) => { - this.performaces = Array.isArray(data) ? data : [data]; + this.http.getPerformaces().pipe( + map(data => Array.isArray(data) ? data : [data]), + tap(performaces => { + this.performaces = performaces; this.assignPerformancesToDates(); - }, - error: (err) => console.error('Fehler beim Laden der Performances', err), - complete: () => this.loading.hide() - }); + this.loading.hide(); + }), + catchError(err => { + this.loading.showError(err); + console.error('Fehler beim Laden der Vorstellung', err); + return of([]); + }) + ).subscribe(); } assignPerformancesToDates() { diff --git a/src/custom-theme.scss b/src/custom-theme.scss index 6bdbd14..e55c90b 100644 --- a/src/custom-theme.scss +++ b/src/custom-theme.scss @@ -6,15 +6,30 @@ // custom components at https://material.angular.dev/guide/theming @use '@angular/material' as mat; - @include mat.progress-bar-overrides(( - active-indicator-color: white, - track-color: transparent, // Transparent machen - )); +@include mat.progress-bar-overrides(( + active-indicator-color: white, + track-color: transparent, +)); + +.mdc-linear-progress__buffer-bar { + background: linear-gradient(to right, #6366f1, #db2777) !important; +} + +@include mat.snack-bar-overrides(( + container-color: red, +)); + + + +.error-snackbar .mat-mdc-snack-bar-label { + color: white !important; +} + +.error-snackbar .mat-mdc-button { + color: white !important; +} + - // Gradient als Hintergrund - .mdc-linear-progress__buffer-bar { - background: linear-gradient(to right, #6366f1, #db2777) !important; - } html { @include mat.theme((