BookMonkey 4 Diff

Files changed (12) hide show
  1. tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/app.module.ts +9 -2
  2. tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/book-details/book-details.component.html +9 -1
  3. tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/book-details/book-details.component.ts +11 -3
  4. tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/book-list/book-list.component.html +6 -0
  5. tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/book-list/book-list.component.ts +1 -1
  6. tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/home/home.component.html +3 -0
  7. tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/search/search.component.html +17 -0
  8. tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/search/search.component.ts +32 -0
  9. tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/shared/book-factory.ts +11 -0
  10. tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/shared/book-raw.ts +15 -0
  11. tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/shared/book-store.service.ts +49 -34
  12. tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/shared/token.interceptor.ts +25 -0
tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/app.module.ts RENAMED
@@ -1,5 +1,6 @@
1
  import { CommonModule } from '@angular/common';
2
  import { NgModule } from '@angular/core';
   
3
   
4
  import { AppRoutingModule } from './app-routing.module.one-app';
5
  import { AppComponent } from './app.component';
@@ -7,6 +8,8 @@
7
  import { BookListComponent } from './book-list/book-list.component';
8
  import { BookListItemComponent } from './book-list-item/book-list-item.component';
9
  import { BookDetailsComponent } from './book-details/book-details.component';
   
   
10
   
11
  @NgModule({
12
  declarations: [
@@ -14,13 +17,17 @@
14
  HomeComponent,
15
  BookListComponent,
16
  BookListItemComponent,
17
- BookDetailsComponent
   
18
  ],
19
  imports: [
20
  CommonModule,
   
21
  AppRoutingModule
22
  ],
23
- providers: [],
   
   
24
  bootstrap: [AppComponent]
25
  })
26
  export class AppModule { }
1
  import { CommonModule } from '@angular/common';
2
  import { NgModule } from '@angular/core';
3
+ import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
4
   
5
  import { AppRoutingModule } from './app-routing.module.one-app';
6
  import { AppComponent } from './app.component';
8
  import { BookListComponent } from './book-list/book-list.component';
9
  import { BookListItemComponent } from './book-list-item/book-list-item.component';
10
  import { BookDetailsComponent } from './book-details/book-details.component';
11
+ import { SearchComponent } from './search/search.component';
12
+ import { TokenInterceptor } from './shared/token.interceptor';
13
   
14
  @NgModule({
15
  declarations: [
17
  HomeComponent,
18
  BookListComponent,
19
  BookListItemComponent,
20
+ BookDetailsComponent,
21
+ SearchComponent
22
  ],
23
  imports: [
24
  CommonModule,
25
+ HttpClientModule,
26
  AppRoutingModule
27
  ],
28
+ providers: [
29
+ { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true }
30
+ ],
31
  bootstrap: [AppComponent]
32
  })
33
  export class AppModule { }
tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/book-details/book-details.component.html RENAMED
@@ -1,4 +1,4 @@
1
- <div *ngIf="book">
2
  <h1>{{ book.title }}</h1>
3
  <h3 *ngIf="book.subtitle">{{ book.subtitle }}</h3>
4
  <div class="ui divider"></div>
@@ -29,4 +29,12 @@
29
  <img *ngFor="let thumbnail of book.thumbnails"
30
  [src]="thumbnail.url">
31
  </div>
   
   
   
   
32
  </div>
   
   
   
   
1
+ <div *ngIf="book; else loading">
2
  <h1>{{ book.title }}</h1>
3
  <h3 *ngIf="book.subtitle">{{ book.subtitle }}</h3>
4
  <div class="ui divider"></div>
29
  <img *ngFor="let thumbnail of book.thumbnails"
30
  [src]="thumbnail.url">
31
  </div>
32
+ <button class="ui tiny red labeled icon button"
33
+ (click)="removeBook()">
34
+ <i class="remove icon"></i> Buch löschen
35
+ </button>
36
  </div>
37
+  
38
+ <ng-template #loading>
39
+ <div class="ui active centered inline loader"></div>
40
+ </ng-template>
tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/book-details/book-details.component.ts RENAMED
@@ -1,5 +1,5 @@
1
  import { Component, OnInit } from '@angular/core';
2
- import { ActivatedRoute } from '@angular/router';
3
   
4
  import { Book } from '../shared/book';
5
  import { BookStoreService } from '../shared/book-store.service';
@@ -14,16 +14,24 @@
14
   
15
  constructor(
16
  private bs: BookStoreService,
   
17
  private route: ActivatedRoute
18
  ) { }
19
   
20
  ngOnInit(): void {
21
  const params = this.route.snapshot.paramMap;
22
-
23
- this.book = this.bs.getSingle(params.get('isbn') || '');
24
  }
25
   
26
  getRating(num: number) {
27
  return new Array(num);
28
  }
   
   
   
   
   
   
   
29
  }
1
  import { Component, OnInit } from '@angular/core';
2
+ import { ActivatedRoute, Router } from '@angular/router';
3
   
4
  import { Book } from '../shared/book';
5
  import { BookStoreService } from '../shared/book-store.service';
14
   
15
  constructor(
16
  private bs: BookStoreService,
17
+ private router: Router,
18
  private route: ActivatedRoute
19
  ) { }
20
   
21
  ngOnInit(): void {
22
  const params = this.route.snapshot.paramMap;
23
+ this.bs.getSingle(params.get('isbn') || '')
24
+ .subscribe(b => this.book = b);
25
  }
26
   
27
  getRating(num: number) {
28
  return new Array(num);
29
  }
30
+  
31
+ removeBook() {
32
+ if (this.book && confirm('Buch wirklich löschen?')) {
33
+ this.bs.remove(this.book.isbn)
34
+ .subscribe(res => this.router.navigate(['../'], { relativeTo: this.route }));
35
+ }
36
+ }
37
  }
tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/book-list/book-list.component.html RENAMED
@@ -3,4 +3,10 @@
3
  *ngFor="let b of books"
4
  [book]="b"
5
  [routerLink]="b.isbn"></bm-book-list-item>
   
   
   
   
   
   
6
  </div>
3
  *ngFor="let b of books"
4
  [book]="b"
5
  [routerLink]="b.isbn"></bm-book-list-item>
6
+  
7
+ <div *ngIf="!books" class="ui active dimmer">
8
+ <div class="ui large text loader">Daten werden geladen...</div>
9
+ </div>
10
+  
11
+ <p *ngIf="books && !books.length">Es wurden noch keine Bücher eingetragen.</p>
12
  </div>
tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/book-list/book-list.component.ts RENAMED
@@ -14,6 +14,6 @@
14
  constructor(private bs: BookStoreService) { }
15
   
16
  ngOnInit(): void {
17
- this.books = this.bs.getAll();
18
  }
19
  }
14
  constructor(private bs: BookStoreService) { }
15
   
16
  ngOnInit(): void {
17
+ this.bs.getAll().subscribe(res => this.books = res);
18
  }
19
  }
tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/home/home.component.html RENAMED
@@ -4,3 +4,6 @@
4
  Buchliste ansehen
5
  <i class="right arrow icon"></i>
6
  </a>
   
   
   
4
  Buchliste ansehen
5
  <i class="right arrow icon"></i>
6
  </a>
7
+  
8
+ <h2>Suche</h2>
9
+ <bm-search></bm-search>
tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/search/search.component.html RENAMED
@@ -0,0 +1,17 @@
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
1
+ <div class="ui search" [class.loading]="isLoading">
2
+ <div class="ui icon input">
3
+ <input type="text" class="prompt" #input
4
+ (keyup)="keyUp$.next(input.value)">
5
+ <i class="search icon"></i>
6
+ </div>
7
+  
8
+ <div class="results transition visible"
9
+ *ngIf="foundBooks.length">
10
+ <a class="result"
11
+ *ngFor="let book of foundBooks"
12
+ [routerLink]="['..', 'books', book.isbn]">
13
+ {{ book.title }}
14
+ <p class="description">{{ book.subtitle }}</p>
15
+ </a>
16
+ </div>
17
+ </div>
tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/search/search.component.ts RENAMED
@@ -0,0 +1,32 @@
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
1
+ import { Component, OnInit } from '@angular/core';
2
+ import { Subject } from 'rxjs';
3
+ import { debounceTime, distinctUntilChanged, tap, switchMap, filter } from 'rxjs/operators';
4
+  
5
+ import { Book } from '../shared/book';
6
+ import { BookStoreService } from '../shared/book-store.service';
7
+  
8
+ @Component({
9
+ selector: 'bm-search',
10
+ templateUrl: './search.component.html',
11
+ styleUrls: ['./search.component.css']
12
+ })
13
+ export class SearchComponent implements OnInit {
14
+  
15
+ keyUp$ = new Subject<string>();
16
+ isLoading = false;
17
+ foundBooks: Book[] = [];
18
+  
19
+ constructor(private bs: BookStoreService) { }
20
+  
21
+ ngOnInit(): void {
22
+ this.keyUp$.pipe(
23
+ filter(term => term.length >= 3),
24
+ debounceTime(500),
25
+ distinctUntilChanged(),
26
+ tap(() => this.isLoading = true),
27
+ switchMap(searchTerm => this.bs.getAllSearch(searchTerm)),
28
+ tap(() => this.isLoading = false)
29
+ )
30
+ .subscribe(books => this.foundBooks = books);
31
+ }
32
+ }
tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/shared/book-factory.ts RENAMED
@@ -0,0 +1,11 @@
   
   
   
   
   
   
   
   
   
   
   
1
+ import { Book } from './book';
2
+ import { BookRaw } from './book-raw';
3
+  
4
+ export class BookFactory {
5
+ static fromRaw(b: BookRaw): Book {
6
+ return {
7
+ ...b,
8
+ published: new Date(b.published)
9
+ };
10
+ }
11
+ }
tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/shared/book-raw.ts RENAMED
@@ -0,0 +1,15 @@
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
1
+ export interface BookRaw {
2
+ isbn: string;
3
+ title: string;
4
+ authors: string[];
5
+ published: string;
6
+ subtitle?: string;
7
+ rating?: number;
8
+ thumbnails?: ThumbnailRaw[];
9
+ description?: string;
10
+ }
11
+  
12
+ export interface ThumbnailRaw {
13
+ url: string;
14
+ title?: string;
15
+ }
tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/shared/book-store.service.ts RENAMED
@@ -1,49 +1,64 @@
1
  import { Injectable } from '@angular/core';
   
   
   
2
   
3
  import { Book } from './book';
   
   
4
   
5
  @Injectable({
6
  providedIn: 'root'
7
  })
8
  export class BookStoreService {
9
- books: Book[];
10
   
11
- constructor() {
12
- this.books = [
13
- {
14
- isbn: '9783864907791',
15
- title: 'Angular',
16
- authors: ['Ferdinand Malcher', 'Johannes Hoppe', 'Danny Koppenhagen'],
17
- published: new Date(2020, 8, 1),
18
- subtitle: 'Grundlagen, fortgeschrittene Themen und Best Practices',
19
- rating: 5,
20
- thumbnails: [{
21
- url: 'https://ng-buch.de/angular-cover.jpg',
22
- title: 'Buchcover'
23
- }],
24
- description: 'Lernen Sie Angular mit diesem Praxisbuch!'
25
- },
26
- {
27
- isbn: '9783864905520',
28
- title: 'React',
29
- authors: ['Oliver Zeigermann', 'Nils Hartmann'],
30
- published: new Date(2019, 11, 12),
31
- subtitle: 'Grundlagen, fortgeschrittene Techniken und Praxistipps',
32
- rating: 3,
33
- thumbnails: [{
34
- url: 'https://ng-buch.de/react-cover.jpg',
35
- title: 'Buchcover'
36
- }],
37
- description: 'Das bewährte und umfassende Praxisbuch zu React.'
38
- }
39
- ];
   
40
  }
41
   
42
- getAll(): Book[] {
43
- return this.books;
   
   
   
   
   
   
   
   
44
  }
45
   
46
- getSingle(isbn: string): Book | undefined {
47
- return this.books.find(book => book.isbn === isbn);
   
48
  }
49
  }
1
  import { Injectable } from '@angular/core';
2
+ import { HttpClient, HttpErrorResponse } from '@angular/common/http';
3
+ import { throwError, Observable } from 'rxjs';
4
+ import { retry, map, catchError } from 'rxjs/operators';
5
   
6
  import { Book } from './book';
7
+ import { BookRaw } from './book-raw';
8
+ import { BookFactory } from './book-factory';
9
   
10
  @Injectable({
11
  providedIn: 'root'
12
  })
13
  export class BookStoreService {
14
+ private api = 'https://api4.angular-buch.com/secure';
15
   
16
+ constructor(private http: HttpClient) {}
17
+  
18
+ getAll(): Observable<Book[]> {
19
+ return this.http.get<BookRaw[]>(`${this.api}/books`)
20
+ .pipe(
21
+ retry(3),
22
+ map(booksRaw =>
23
+ booksRaw.map(b => BookFactory.fromRaw(b)),
24
+ ),
25
+ catchError(this.errorHandler)
26
+ );
27
+ }
28
+  
29
+ getSingle(isbn: string): Observable<Book> {
30
+ return this.http.get<BookRaw>(
31
+ `${this.api}/book/${isbn}`
32
+ ).pipe(
33
+ retry(3),
34
+ map(b => BookFactory.fromRaw(b)),
35
+ catchError(this.errorHandler)
36
+ );
37
+ }
38
+  
39
+ remove(isbn: string): Observable<any> {
40
+ return this.http.delete(
41
+ `${this.api}/book/${isbn}`,
42
+ { responseType: 'text' }
43
+ ).pipe(
44
+ catchError(this.errorHandler)
45
+ );
46
  }
47
   
48
+ getAllSearch(searchTerm: string): Observable<Book[]> {
49
+ return this.http.get<BookRaw[]>(
50
+ `${this.api}/books/search/${searchTerm}`
51
+ ).pipe(
52
+ retry(3),
53
+ map(booksRaw =>
54
+ booksRaw.map(b => BookFactory.fromRaw(b)),
55
+ ),
56
+ catchError(this.errorHandler)
57
+ );
58
  }
59
   
60
+ private errorHandler(error: HttpErrorResponse): Observable<any> {
61
+ console.error('Fehler aufgetreten!');
62
+ return throwError(error);
63
  }
64
  }
tmp/src/app/book-monkey/{iteration-2/routing → iteration-3/interceptors}/shared/token.interceptor.ts RENAMED
@@ -0,0 +1,25 @@
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
1
+ import { Injectable } from '@angular/core';
2
+ import {
3
+ HttpRequest,
4
+ HttpHandler,
5
+ HttpEvent,
6
+ HttpInterceptor
7
+ } from '@angular/common/http';
8
+ import { Observable } from 'rxjs';
9
+  
10
+ @Injectable()
11
+ export class TokenInterceptor implements HttpInterceptor {
12
+ private authToken = '1234567890';
13
+  
14
+ intercept(
15
+ request: HttpRequest<unknown>,
16
+ next: HttpHandler
17
+ ): Observable<HttpEvent<unknown>> {
18
+ const newRequest = request.clone({
19
+ setHeaders: {
20
+ Authorization: `Bearer ${this.authToken}`
21
+ }
22
+ });
23
+ return next.handle(newRequest);
24
+ }
25
+ }