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.
This commit is contained in:
2025-10-31 16:19:02 +01:00
parent 0828493be5
commit 5addba879a
6 changed files with 113 additions and 29 deletions

View File

@@ -11,6 +11,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar'; 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'; import { HeaderComponent } from './header/header.component';
@@ -60,7 +61,8 @@ import { Header2Component } from './header-2/header-2.component';
MatIconModule, MatIconModule,
MatTabsModule, MatTabsModule,
MatToolbarModule, MatToolbarModule,
MatProgressBarModule MatProgressBarModule,
MatSnackBarModule
], ],
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),

View File

@@ -24,7 +24,9 @@
</div> </div>
</div> </div>
@if (loading$ | async) { @if (loadingService.error$ | async) {
<div class="h-1 w-full" style="background-color: red;"></div>
} @else if (loadingService.loading$ | async){
<mat-progress-bar <mat-progress-bar
mode="indeterminate" mode="indeterminate"
class="h-1 w-full" class="h-1 w-full"

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, inject } from '@angular/core';
import { LoadingService } from '../loading.service'; import { LoadingService } from '../loading.service';
@Component({ @Component({
@@ -8,8 +8,5 @@ import { LoadingService } from '../loading.service';
styleUrl: './header-2.component.css' styleUrl: './header-2.component.css'
}) })
export class Header2Component { export class Header2Component {
loading$; protected loadingService = inject(LoadingService)
constructor(private loadingService: LoadingService) {
this.loading$ = this.loadingService.loading$;
}
} }

View File

@@ -1,18 +1,80 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class LoadingService { export class LoadingService {
private _loading = new BehaviorSubject<boolean>(false); private loadingSubject = new BehaviorSubject<boolean>(false);
readonly loading$ = this._loading.asObservable(); private errorSubject = new BehaviorSubject<boolean>(false);
show() { public loading$ = this.loadingSubject.asObservable();
this._loading.next(true); public error$ = this.errorSubject.asObservable();
constructor(private snackBar: MatSnackBar) {}
show(): void {
this.loadingSubject.next(true);
this.errorSubject.next(false);
} }
hide() { hide(): void {
this._loading.next(false); 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!';
} }
} }

View File

@@ -4,6 +4,7 @@ import { Vorstellung } from '@infinimotion/model-frontend';
import { Performance } from '../model/performance.model'; import { Performance } from '../model/performance.model';
import { MovieGroup } from '../model/movie-group.model'; import { MovieGroup } from '../model/movie-group.model';
import { LoadingService } from '../loading.service'; import { LoadingService } from '../loading.service';
import { catchError, map, of, tap } from 'rxjs';
@Component({ @Component({
selector: 'app-schedule', selector: 'app-schedule',
@@ -47,14 +48,19 @@ export class ScheduleComponent implements OnInit {
loadPerformances() { loadPerformances() {
this.loading.show(); this.loading.show();
this.http.getPerformaces().subscribe({ this.http.getPerformaces().pipe(
next: (data) => { map(data => Array.isArray(data) ? data : [data]),
this.performaces = Array.isArray(data) ? data : [data]; tap(performaces => {
this.performaces = performaces;
this.assignPerformancesToDates(); this.assignPerformancesToDates();
}, this.loading.hide();
error: (err) => console.error('Fehler beim Laden der Performances', err), }),
complete: () => this.loading.hide() catchError(err => {
}); this.loading.showError(err);
console.error('Fehler beim Laden der Vorstellung', err);
return of([]);
})
).subscribe();
} }
assignPerformancesToDates() { assignPerformancesToDates() {

View File

@@ -6,15 +6,30 @@
// custom components at https://material.angular.dev/guide/theming // custom components at https://material.angular.dev/guide/theming
@use '@angular/material' as mat; @use '@angular/material' as mat;
@include mat.progress-bar-overrides(( @include mat.progress-bar-overrides((
active-indicator-color: white, active-indicator-color: white,
track-color: transparent, // Transparent machen track-color: transparent,
)); ));
// Gradient als Hintergrund .mdc-linear-progress__buffer-bar {
.mdc-linear-progress__buffer-bar {
background: linear-gradient(to right, #6366f1, #db2777) !important; 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;
}
html { html {
@include mat.theme(( @include mat.theme((