Skip to content

Commit 945b4c6

Browse files
committed
End of section 11
1 parent 002f74a commit 945b4c6

19 files changed

+273
-10
lines changed

client/src/app/app.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<app-header></app-header>
22

3-
<div class="container mt-6">
3+
<div class="container mt-24">
44
<router-outlet></router-outlet>
55
</div>

client/src/app/app.config.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { provideRouter } from '@angular/router';
33

44
import { routes } from './app.routes';
55
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
6-
import { provideHttpClient } from '@angular/common/http';
6+
import { provideHttpClient, withInterceptors } from '@angular/common/http';
7+
import { errorInterceptor } from './core/interceptors/error.interceptor';
8+
import { loadingInterceptor } from './core/interceptors/loading.interceptor';
79

810
export const appConfig: ApplicationConfig = {
911
providers: [
1012
provideZoneChangeDetection({ eventCoalescing: true }),
1113
provideRouter(routes), provideAnimationsAsync(),
12-
provideHttpClient()
14+
provideHttpClient(withInterceptors([errorInterceptor, loadingInterceptor]))
1315
]
1416
};

client/src/app/app.routes.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@ import { Routes } from '@angular/router';
22
import { HomeComponent } from './features/home/home.component';
33
import { ShopComponent } from './features/shop/shop.component';
44
import { ProductDetailsComponent } from './features/shop/product-details/product-details.component';
5+
import { TestErrorComponent } from './features/test-error/test-error.component';
6+
import { NotFoundComponent } from './shared/components/not-found/not-found.component';
7+
import { ServerErrorComponent } from './shared/components/server-error/server-error.component';
58

69
export const routes: Routes = [
710
{ path: '', component: HomeComponent },
811
{ path: 'shop', component: ShopComponent },
912
{ path: 'shop/:id', component: ProductDetailsComponent },
10-
{ path: '**', redirectTo: '', pathMatch: 'full' }
13+
{ path: 'test-error', component: TestErrorComponent },
14+
{ path: 'not-found', component: NotFoundComponent },
15+
{ path: 'server-error', component: ServerErrorComponent },
16+
{ path: '**', redirectTo: 'not-found', pathMatch: 'full' }
1117
];
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
2+
import { inject } from '@angular/core';
3+
import { NavigationExtras, Router } from '@angular/router';
4+
import { catchError, throwError } from 'rxjs';
5+
import { SnackbarService } from '../services/snackbar.service';
6+
7+
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
8+
const router = inject(Router);
9+
const snackBar = inject(SnackbarService);
10+
11+
return next(req).pipe(
12+
catchError((err: HttpErrorResponse) => {
13+
if (err.status === 400) {
14+
if (err.error.errors) {
15+
const modelStateErrors = [];
16+
for (const key in err.error.errors) {
17+
if (err.error.errors[key]) {
18+
modelStateErrors.push(err.error.errors[key])
19+
}
20+
}
21+
22+
throw modelStateErrors.flat();
23+
} else {
24+
snackBar.error(err.error.title || err.error);
25+
}
26+
}
27+
if (err.status === 401) {
28+
snackBar.error(err.error.title || err.error);
29+
}
30+
if (err.status === 404) {
31+
router.navigateByUrl('/not-found');
32+
}
33+
if (err.status === 500) {
34+
const navigationExtras: NavigationExtras = { state: { error: err.error } }
35+
router.navigateByUrl('/server-error', navigationExtras);
36+
}
37+
38+
return throwError(() => err)
39+
})
40+
);
41+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { HttpInterceptorFn } from '@angular/common/http';
2+
import { inject } from '@angular/core';
3+
import { delay, finalize } from 'rxjs';
4+
import { BusyService } from '../services/busy.service';
5+
6+
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
7+
const busyService = inject(BusyService);
8+
9+
busyService.busy();
10+
11+
return next(req).pipe(
12+
delay(500),
13+
finalize(() => busyService.idle())
14+
);
15+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Injectable } from '@angular/core';
2+
3+
@Injectable({
4+
providedIn: 'root'
5+
})
6+
export class BusyService {
7+
loading = false;
8+
busyRequestCount = 0;
9+
10+
busy() {
11+
this.busyRequestCount++;
12+
this.loading = true;
13+
}
14+
idle() {
15+
this.busyRequestCount--;
16+
if (this.busyRequestCount <= 0) {
17+
this.busyRequestCount = 0;
18+
this.loading = false;
19+
}
20+
}
21+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { inject, Injectable } from '@angular/core';
2+
import { MatSnackBar } from '@angular/material/snack-bar';
3+
4+
@Injectable({
5+
providedIn: 'root'
6+
})
7+
export class SnackbarService {
8+
private snackBar = inject(MatSnackBar)
9+
10+
error(message: string) {
11+
this.snackBar.open(message, 'Close', {
12+
duration: 5000,
13+
panelClass: ['snack-error']
14+
})
15+
}
16+
17+
success(message: string) {
18+
this.snackBar.open(message, 'Close', {
19+
duration: 5000,
20+
panelClass: ['snack-success']
21+
})
22+
}
23+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<div class="mt-5 flex justify-center gap-4">
2+
<button mat-stroked-button (click)="get500Error()">Test 500 error</button>
3+
<button mat-stroked-button (click)="get404Error()">Test 404 error</button>
4+
<button mat-stroked-button (click)="get400Error()">Test 400 error</button>
5+
<button mat-stroked-button (click)="get401Error()">Test 401 error</button>
6+
<button mat-stroked-button (click)="get400ValidationErrorError()">Test validation error</button>
7+
</div>
8+
9+
@if (validationErrors) {
10+
<div class="mx-auto max-w-lg mt-5 bg-red-100">
11+
<ul class="space-y-2 p-2">
12+
@for (error of validationErrors; track $index) {
13+
<li class="text-red-800">{{error}}</li>
14+
}
15+
</ul>
16+
</div>
17+
}

client/src/app/features/test-error/test-error.component.scss

Whitespace-only changes.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { HttpClient } from '@angular/common/http';
2+
import { Component, inject } from '@angular/core';
3+
import { MatButton } from '@angular/material/button';
4+
5+
@Component({
6+
selector: 'app-test-error',
7+
imports: [
8+
MatButton
9+
],
10+
templateUrl: './test-error.component.html',
11+
styleUrl: './test-error.component.scss'
12+
})
13+
export class TestErrorComponent {
14+
baseUrl = 'https://localhost:5001/api/';
15+
private http = inject(HttpClient);
16+
validationErrors?: string[];
17+
18+
get404Error() {
19+
this.http.get(this.baseUrl + 'buggy/notfound').subscribe({
20+
next: response => console.log(response),
21+
error: error => console.log(error)
22+
})
23+
}
24+
25+
get400Error() {
26+
this.http.get(this.baseUrl + 'buggy/badrequest').subscribe({
27+
next: response => console.log(response),
28+
error: error => console.log(error)
29+
})
30+
}
31+
32+
get401Error() {
33+
this.http.get(this.baseUrl + 'buggy/unauthorized').subscribe({
34+
next: response => console.log(response),
35+
error: error => console.log(error)
36+
})
37+
}
38+
39+
get500Error() {
40+
this.http.get(this.baseUrl + 'buggy/internalerror').subscribe({
41+
next: response => console.log(response),
42+
error: error => console.log(error)
43+
})
44+
}
45+
46+
get400ValidationErrorError() {
47+
this.http.post(this.baseUrl + 'buggy/validationerror', {}).subscribe({
48+
next: response => console.log(response),
49+
error: error => this.validationErrors = error
50+
})
51+
}
52+
}

0 commit comments

Comments
 (0)