@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NgModule } from '@angular/core';
|
2 |
+
import { Routes, RouterModule } from '@angular/router';
|
3 |
+
|
4 |
+
import { CreateBookComponent } from './create-book/create-book.component';
|
5 |
+
import { EditBookComponent } from './edit-book/edit-book.component';
|
6 |
+
|
7 |
+
const routes: Routes = [
|
8 |
+
{
|
9 |
+
path: '',
|
10 |
+
redirectTo: 'create',
|
11 |
+
pathMatch: 'full'
|
12 |
+
},
|
13 |
+
{
|
14 |
+
path: 'create',
|
15 |
+
component: CreateBookComponent
|
16 |
+
},
|
17 |
+
{
|
18 |
+
path: 'edit/:isbn',
|
19 |
+
component: EditBookComponent
|
20 |
+
}
|
21 |
+
];
|
22 |
+
|
23 |
+
@NgModule({
|
24 |
+
imports: [RouterModule.forChild(routes)],
|
25 |
+
exports: [RouterModule],
|
26 |
+
providers: []
|
27 |
+
})
|
28 |
+
export class AdminRoutingModule { }
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NgModule } from '@angular/core';
|
2 |
+
import { CommonModule } from '@angular/common';
|
3 |
+
import { ReactiveFormsModule } from '@angular/forms';
|
4 |
+
import { DateValueAccessorModule } from 'angular-date-value-accessor';
|
5 |
+
|
6 |
+
import { AdminRoutingModule } from './admin-routing.module';
|
7 |
+
import { BookFormComponent } from './book-form/book-form.component';
|
8 |
+
import { CreateBookComponent } from './create-book/create-book.component';
|
9 |
+
import { FormMessagesComponent } from './form-messages/form-messages.component';
|
10 |
+
import { EditBookComponent } from './edit-book/edit-book.component';
|
11 |
+
|
12 |
+
@NgModule({
|
13 |
+
imports: [
|
14 |
+
CommonModule,
|
15 |
+
AdminRoutingModule,
|
16 |
+
ReactiveFormsModule,
|
17 |
+
DateValueAccessorModule
|
18 |
+
],
|
19 |
+
declarations: [
|
20 |
+
BookFormComponent,
|
21 |
+
CreateBookComponent,
|
22 |
+
EditBookComponent,
|
23 |
+
FormMessagesComponent
|
24 |
+
]
|
25 |
+
})
|
26 |
+
export class AdminModule { }
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<form class="ui form"
|
2 |
+
[formGroup]="bookForm"
|
3 |
+
(ngSubmit)="submitForm()">
|
4 |
+
|
5 |
+
<label>Buchtitel</label>
|
6 |
+
<input formControlName="title">
|
7 |
+
<bm-form-messages
|
8 |
+
[control]="bookForm.get('title')"
|
9 |
+
controlName="title">
|
10 |
+
</bm-form-messages>
|
11 |
+
|
12 |
+
<label>Untertitel</label>
|
13 |
+
<input formControlName="subtitle">
|
14 |
+
|
15 |
+
<label>ISBN</label>
|
16 |
+
<input formControlName="isbn">
|
17 |
+
<bm-form-messages
|
18 |
+
[control]="bookForm.get('isbn')"
|
19 |
+
controlName="isbn">
|
20 |
+
</bm-form-messages>
|
21 |
+
|
22 |
+
<label>Erscheinungsdatum</label>
|
23 |
+
<input type="date"
|
24 |
+
useValueAsDate
|
25 |
+
formControlName="published">
|
26 |
+
<bm-form-messages
|
27 |
+
[control]="bookForm.get('published')"
|
28 |
+
controlName="published">
|
29 |
+
</bm-form-messages>
|
30 |
+
|
31 |
+
<label>Autoren</label>
|
32 |
+
<button type="button" class="ui mini button"
|
33 |
+
(click)="addAuthorControl()">
|
34 |
+
+ Autor
|
35 |
+
</button>
|
36 |
+
<div class="fields" formArrayName="authors">
|
37 |
+
<div class="sixteen wide field"
|
38 |
+
*ngFor="let c of authors.controls; index as i">
|
39 |
+
<input placeholder="Autor"
|
40 |
+
[formControlName]="i">
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
<bm-form-messages
|
44 |
+
[control]="bookForm.get('authors')"
|
45 |
+
controlName="authors">
|
46 |
+
</bm-form-messages>
|
47 |
+
|
48 |
+
<label>Beschreibung</label>
|
49 |
+
<textarea formControlName="description"></textarea>
|
50 |
+
|
51 |
+
<label>Bilder</label>
|
52 |
+
<button type="button" class="ui mini button"
|
53 |
+
(click)="addThumbnailControl()">
|
54 |
+
+ Bild
|
55 |
+
</button>
|
56 |
+
<div formArrayName="thumbnails">
|
57 |
+
<div class="fields"
|
58 |
+
*ngFor="let c of thumbnails.controls; index as i"
|
59 |
+
[formGroupName]="i">
|
60 |
+
<div class="nine wide field">
|
61 |
+
<input placeholder="URL"
|
62 |
+
formControlName="url">
|
63 |
+
</div>
|
64 |
+
<div class="seven wide field">
|
65 |
+
<input placeholder="Titel"
|
66 |
+
formControlName="title">
|
67 |
+
</div>
|
68 |
+
</div>
|
69 |
+
</div>
|
70 |
+
|
71 |
+
<button class="ui button" type="submit"
|
72 |
+
[disabled]="bookForm.invalid">
|
73 |
+
Speichern
|
74 |
+
</button>
|
75 |
+
</form>
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Component, OnInit, Input, Output, EventEmitter, OnChanges } from '@angular/core';
|
2 |
+
import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';
|
3 |
+
|
4 |
+
import { Book, Thumbnail } from '../../shared/book';
|
5 |
+
import { BookValidators } from '../shared/book.validators';
|
6 |
+
import { BookExistsValidatorService } from '../shared/book-exists-validator.service';
|
7 |
+
|
8 |
+
@Component({
|
9 |
+
selector: 'bm-book-form',
|
10 |
+
templateUrl: './book-form.component.html',
|
11 |
+
styleUrls: ['./book-form.component.css']
|
12 |
+
})
|
13 |
+
export class BookFormComponent implements OnInit, OnChanges {
|
14 |
+
|
15 |
+
bookForm: FormGroup;
|
16 |
+
|
17 |
+
@Input() book: Book;
|
18 |
+
@Input() editing = false;
|
19 |
+
@Output() submitBook = new EventEmitter<Book>();
|
20 |
+
|
21 |
+
constructor(
|
22 |
+
private fb: FormBuilder,
|
23 |
+
private bookExistsValidator: BookExistsValidatorService
|
24 |
+
) { }
|
25 |
+
|
26 |
+
ngOnInit() {
|
27 |
+
this.initForm();
|
28 |
+
}
|
29 |
+
|
30 |
+
ngOnChanges() {
|
31 |
+
this.initForm();
|
32 |
+
this.setFormValues(this.book);
|
33 |
+
}
|
34 |
+
|
35 |
+
private setFormValues(book: Book) {
|
36 |
+
this.bookForm.patchValue(book);
|
37 |
+
|
38 |
+
this.bookForm.setControl(
|
39 |
+
'authors',
|
40 |
+
this.buildAuthorsArray(book.authors)
|
41 |
+
);
|
42 |
+
|
43 |
+
this.bookForm.setControl(
|
44 |
+
'thumbnails',
|
45 |
+
this.buildThumbnailsArray(book.thumbnails)
|
46 |
+
);
|
47 |
+
}
|
48 |
+
|
49 |
+
private initForm() {
|
50 |
+
if (this.bookForm) { return; }
|
51 |
+
|
52 |
+
this.bookForm = this.fb.group({
|
53 |
+
title: ['', Validators.required],
|
54 |
+
subtitle: [''],
|
55 |
+
isbn: [
|
56 |
+
{ value: '', disabled: this.editing },
|
57 |
+
[
|
58 |
+
Validators.required,
|
59 |
+
BookValidators.isbnFormat
|
60 |
+
],
|
61 |
+
this.editing ? null : [this.bookExistsValidator]
|
62 |
+
],
|
63 |
+
description: [''],
|
64 |
+
authors: this.buildAuthorsArray(['']),
|
65 |
+
thumbnails: this.buildThumbnailsArray([
|
66 |
+
{ title: '', url: '' }
|
67 |
+
]),
|
68 |
+
published: []
|
69 |
+
});
|
70 |
+
}
|
71 |
+
|
72 |
+
private buildAuthorsArray(values: string[]): FormArray {
|
73 |
+
return this.fb.array(values, BookValidators.atLeastOneAuthor);
|
74 |
+
}
|
75 |
+
|
76 |
+
private buildThumbnailsArray(values: Thumbnail[]): FormArray {
|
77 |
+
return this.fb.array(
|
78 |
+
values.map(t => this.fb.group(t))
|
79 |
+
);
|
80 |
+
}
|
81 |
+
|
82 |
+
get authors(): FormArray {
|
83 |
+
return this.bookForm.get('authors') as FormArray;
|
84 |
+
}
|
85 |
+
|
86 |
+
get thumbnails(): FormArray {
|
87 |
+
return this.bookForm.get('thumbnails') as FormArray;
|
88 |
+
}
|
89 |
+
|
90 |
+
addAuthorControl() {
|
91 |
+
this.authors.push(this.fb.control(''));
|
92 |
+
}
|
93 |
+
|
94 |
+
addThumbnailControl() {
|
95 |
+
this.thumbnails.push(
|
96 |
+
this.fb.group({ url: '', title: '' })
|
97 |
+
);
|
98 |
+
}
|
99 |
+
|
100 |
+
submitForm() {
|
101 |
+
const formValue = this.bookForm.value;
|
102 |
+
const authors = formValue.authors
|
103 |
+
.filter(author => author);
|
104 |
+
const thumbnails = formValue.thumbnails
|
105 |
+
.filter(thumbnail => thumbnail.url);
|
106 |
+
|
107 |
+
const isbn = this.editing ? this.book.isbn : formValue.isbn;
|
108 |
+
|
109 |
+
const newBook: Book = {
|
110 |
+
...formValue,
|
111 |
+
isbn,
|
112 |
+
authors,
|
113 |
+
thumbnails
|
114 |
+
};
|
115 |
+
|
116 |
+
this.submitBook.emit(newBook);
|
117 |
+
this.bookForm.reset();
|
118 |
+
}
|
119 |
+
}
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
1 |
+
<h1>Buch hinzufügen</h1>
|
2 |
+
|
3 |
+
<bm-book-form (submitBook)="createBook($event)"></bm-book-form>
|
@@ -0,0 +1,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';
|
6 |
+
|
7 |
+
@Component({
|
8 |
+
selector: 'bm-create-book',
|
9 |
+
templateUrl: './create-book.component.html',
|
10 |
+
styleUrls: ['./create-book.component.css']
|
11 |
+
})
|
12 |
+
export class CreateBookComponent implements OnInit {
|
13 |
+
|
14 |
+
constructor(
|
15 |
+
private bs: BookStoreService,
|
16 |
+
private route: ActivatedRoute,
|
17 |
+
private router: Router
|
18 |
+
) { }
|
19 |
+
|
20 |
+
ngOnInit() {
|
21 |
+
}
|
22 |
+
|
23 |
+
createBook(book: Book) {
|
24 |
+
this.bs.create(book).subscribe(() => {
|
25 |
+
this.router.navigate(['../..', 'books'], { relativeTo: this.route });
|
26 |
+
});
|
27 |
+
}
|
28 |
+
|
29 |
+
}
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<h1>Buch bearbeiten</h1>
|
2 |
+
|
3 |
+
<bm-book-form
|
4 |
+
*ngIf="book"
|
5 |
+
(submitBook)="updateBook($event)"
|
6 |
+
[book]="book"
|
7 |
+
[editing]="true"
|
8 |
+
></bm-book-form>
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Component, OnInit } from '@angular/core';
|
2 |
+
import { ActivatedRoute, Router } from '@angular/router';
|
3 |
+
import { map, switchMap } from 'rxjs/operators';
|
4 |
+
|
5 |
+
import { Book } from '../../shared/book';
|
6 |
+
import { BookStoreService } from '../../shared/book-store.service';
|
7 |
+
|
8 |
+
@Component({
|
9 |
+
selector: 'bm-edit-book',
|
10 |
+
templateUrl: './edit-book.component.html',
|
11 |
+
styleUrls: ['./edit-book.component.css']
|
12 |
+
})
|
13 |
+
export class EditBookComponent implements OnInit {
|
14 |
+
|
15 |
+
book: Book;
|
16 |
+
|
17 |
+
constructor(
|
18 |
+
private bs: BookStoreService,
|
19 |
+
private route: ActivatedRoute,
|
20 |
+
private router: Router
|
21 |
+
) { }
|
22 |
+
|
23 |
+
ngOnInit() {
|
24 |
+
this.route.paramMap.pipe(
|
25 |
+
map(params => params.get('isbn')),
|
26 |
+
switchMap((isbn: string) => this.bs.getSingle(isbn))
|
27 |
+
)
|
28 |
+
.subscribe(book => this.book = book);
|
29 |
+
}
|
30 |
+
|
31 |
+
updateBook(book: Book) {
|
32 |
+
this.bs.update(book).subscribe(() => {
|
33 |
+
this.router.navigate(['../../..', 'books', book.isbn], { relativeTo: this.route });
|
34 |
+
});
|
35 |
+
}
|
36 |
+
|
37 |
+
}
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
1 |
+
<div class="ui negative message"
|
2 |
+
*ngFor="let msg of errorsForControl()">
|
3 |
+
{{ msg }}
|
4 |
+
</div>
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Component, OnInit, Input } from '@angular/core';
|
2 |
+
import { AbstractControl } from '@angular/forms';
|
3 |
+
|
4 |
+
@Component({
|
5 |
+
selector: 'bm-form-messages',
|
6 |
+
templateUrl: './form-messages.component.html',
|
7 |
+
styleUrls: ['./form-messages.component.css']
|
8 |
+
})
|
9 |
+
export class FormMessagesComponent implements OnInit {
|
10 |
+
|
11 |
+
@Input() control: AbstractControl;
|
12 |
+
@Input() controlName: string;
|
13 |
+
|
14 |
+
private allMessages = {
|
15 |
+
title: {
|
16 |
+
required: 'Ein Buchtitel muss angegeben werden.'
|
17 |
+
},
|
18 |
+
isbn: {
|
19 |
+
required: 'Es muss eine ISBN angegeben werden.',
|
20 |
+
isbnFormat: 'Die ISBN muss aus 10 oder 13 Zeichen bestehen.',
|
21 |
+
isbnExists: 'Die ISBN existiert bereits.'
|
22 |
+
},
|
23 |
+
published: {
|
24 |
+
required: 'Es muss ein Erscheinungsdatum angegeben werden.'
|
25 |
+
},
|
26 |
+
authors: {
|
27 |
+
atLeastOneAuthor: 'Es muss ein Autor angegeben werden.'
|
28 |
+
}
|
29 |
+
};
|
30 |
+
|
31 |
+
constructor() { }
|
32 |
+
|
33 |
+
ngOnInit() {
|
34 |
+
}
|
35 |
+
|
36 |
+
errorsForControl(): string[] {
|
37 |
+
const messages = this.allMessages[this.controlName];
|
38 |
+
|
39 |
+
if (
|
40 |
+
!this.control ||
|
41 |
+
!this.control.errors ||
|
42 |
+
!messages ||
|
43 |
+
!this.control.dirty
|
44 |
+
) { return null; }
|
45 |
+
|
46 |
+
return Object.keys(this.control.errors)
|
47 |
+
.map(err => messages[err]);
|
48 |
+
}
|
49 |
+
|
50 |
+
}
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Injectable } from '@angular/core';
|
2 |
+
import { FormControl, AsyncValidator, ValidationErrors } from '@angular/forms';
|
3 |
+
import { Observable, of } from 'rxjs';
|
4 |
+
import { map, catchError } from 'rxjs/operators';
|
5 |
+
|
6 |
+
import { BookStoreService } from '../../shared/book-store.service';
|
7 |
+
|
8 |
+
@Injectable({
|
9 |
+
providedIn: 'root'
|
10 |
+
})
|
11 |
+
export class BookExistsValidatorService implements AsyncValidator {
|
12 |
+
|
13 |
+
constructor(private bs: BookStoreService) { }
|
14 |
+
|
15 |
+
validate(
|
16 |
+
control: FormControl
|
17 |
+
): Observable<ValidationErrors | null> {
|
18 |
+
return this.bs.check(control.value).pipe(
|
19 |
+
map(exists => (exists === false) ? null : {
|
20 |
+
isbnExists: { valid: false }
|
21 |
+
}),
|
22 |
+
catchError(() => of(null))
|
23 |
+
);
|
24 |
+
}
|
25 |
+
}
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { FormControl, FormArray, ValidationErrors } from '@angular/forms';
|
2 |
+
|
3 |
+
export class BookValidators {
|
4 |
+
|
5 |
+
static isbnFormat(control: FormControl): ValidationErrors | null {
|
6 |
+
if (!control.value) { return null; }
|
7 |
+
|
8 |
+
const numbers = control.value.replace(/-/g, '');
|
9 |
+
const isbnPattern = /(^\d{10}$)|(^\d{13}$)/;
|
10 |
+
|
11 |
+
if (isbnPattern.test(numbers)) {
|
12 |
+
return null;
|
13 |
+
} else {
|
14 |
+
return {
|
15 |
+
isbnFormat: { valid: false }
|
16 |
+
};
|
17 |
+
}
|
18 |
+
}
|
19 |
+
|
20 |
+
static atLeastOneAuthor(controlArray: FormArray): ValidationErrors | null {
|
21 |
+
if (controlArray.controls.some(el => el.value)) {
|
22 |
+
return null;
|
23 |
+
} else {
|
24 |
+
return {
|
25 |
+
atLeastOneAuthor: { valid: false }
|
26 |
+
};
|
27 |
+
}
|
28 |
+
}
|
29 |
+
|
30 |
+
}
|
@@ -2,10 +2,7 @@
|
|
2 |
import { Routes, RouterModule } from '@angular/router';
|
3 |
|
4 |
import { HomeComponent } from './home/home.component';
|
5 |
-
import {
|
6 |
-
import { BookDetailsComponent } from './book-details/book-details.component';
|
7 |
-
import { CreateBookComponent } from './create-book/create-book.component';
|
8 |
-
import { EditBookComponent } from './edit-book/edit-book.component';
|
9 |
|
10 |
export const routes: Routes = [
|
11 |
{
|
@@ -19,24 +16,12 @@
|
|
19 |
},
|
20 |
{
|
21 |
path: 'books',
|
22 |
-
|
23 |
-
},
|
24 |
-
{
|
25 |
-
path: 'books/:isbn',
|
26 |
-
component: BookDetailsComponent
|
27 |
},
|
28 |
{
|
29 |
path: 'admin',
|
30 |
-
|
31 |
-
|
32 |
-
},
|
33 |
-
{
|
34 |
-
path: 'admin/create',
|
35 |
-
component: CreateBookComponent
|
36 |
-
},
|
37 |
-
{
|
38 |
-
path: 'admin/edit/:isbn',
|
39 |
-
component: EditBookComponent
|
40 |
}
|
41 |
];
|
42 |
|
2 |
import { Routes, RouterModule } from '@angular/router';
|
3 |
|
4 |
import { HomeComponent } from './home/home.component';
|
5 |
+
import { CanNavigateToAdminGuard } from './can-navigate-to-admin.guard';
|
|
|
|
|
|
|
6 |
|
7 |
export const routes: Routes = [
|
8 |
{
|
16 |
},
|
17 |
{
|
18 |
path: 'books',
|
19 |
+
loadChildren: () => import('src/app/book-monkey/iteration-6/guards/books/books.module').then(m => m.BooksModule)
|
|
|
|
|
|
|
|
|
20 |
},
|
21 |
{
|
22 |
path: 'admin',
|
23 |
+
loadChildren: () => import('src/app/book-monkey/iteration-6/guards/admin/admin.module').then(m => m.AdminModule),
|
24 |
+
canActivate: [CanNavigateToAdminGuard]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
}
|
26 |
];
|
27 |
|
@@ -1,49 +1,25 @@
|
|
1 |
import { CommonModule } from '@angular/common';
|
2 |
import { NgModule, LOCALE_ID } from '@angular/core';
|
3 |
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
4 |
-
import { ReactiveFormsModule } from '@angular/forms';
|
5 |
-
import { DateValueAccessorModule } from 'angular-date-value-accessor';
|
6 |
import localeDe from '@angular/common/locales/de';
|
7 |
import { registerLocaleData } from '@angular/common';
|
8 |
|
9 |
import { AppRoutingModule } from './app-routing.module.one-app';
|
10 |
import { AppComponent } from './app.component';
|
11 |
import { HomeComponent } from './home/home.component';
|
12 |
-
import { BookListComponent } from './book-list/book-list.component';
|
13 |
-
import { BookListItemComponent } from './book-list-item/book-list-item.component';
|
14 |
-
import { BookDetailsComponent } from './book-details/book-details.component';
|
15 |
import { SearchComponent } from './search/search.component';
|
16 |
import { TokenInterceptor } from './shared/token-interceptor';
|
17 |
-
import { BookFormComponent } from './book-form/book-form.component';
|
18 |
-
import { CreateBookComponent } from './create-book/create-book.component';
|
19 |
-
import { FormMessagesComponent } from './form-messages/form-messages.component';
|
20 |
-
import { EditBookComponent } from './edit-book/edit-book.component';
|
21 |
-
import { IsbnPipe } from './shared/isbn.pipe';
|
22 |
-
import { ZoomDirective } from './shared/zoom.directive';
|
23 |
-
import { DelayDirective } from './shared/delay.directive';
|
24 |
|
25 |
@NgModule({
|
26 |
declarations: [
|
27 |
AppComponent,
|
28 |
HomeComponent,
|
29 |
-
|
30 |
-
BookListItemComponent,
|
31 |
-
BookDetailsComponent,
|
32 |
-
SearchComponent,
|
33 |
-
BookFormComponent,
|
34 |
-
CreateBookComponent,
|
35 |
-
FormMessagesComponent,
|
36 |
-
EditBookComponent,
|
37 |
-
IsbnPipe,
|
38 |
-
ZoomDirective,
|
39 |
-
DelayDirective
|
40 |
],
|
41 |
imports: [
|
42 |
CommonModule,
|
43 |
HttpClientModule,
|
44 |
-
AppRoutingModule
|
45 |
-
ReactiveFormsModule,
|
46 |
-
DateValueAccessorModule
|
47 |
],
|
48 |
providers: [
|
49 |
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true },
|
1 |
import { CommonModule } from '@angular/common';
|
2 |
import { NgModule, LOCALE_ID } from '@angular/core';
|
3 |
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
|
|
|
|
4 |
import localeDe from '@angular/common/locales/de';
|
5 |
import { registerLocaleData } from '@angular/common';
|
6 |
|
7 |
import { AppRoutingModule } from './app-routing.module.one-app';
|
8 |
import { AppComponent } from './app.component';
|
9 |
import { HomeComponent } from './home/home.component';
|
|
|
|
|
|
|
10 |
import { SearchComponent } from './search/search.component';
|
11 |
import { TokenInterceptor } from './shared/token-interceptor';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
@NgModule({
|
14 |
declarations: [
|
15 |
AppComponent,
|
16 |
HomeComponent,
|
17 |
+
SearchComponent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
],
|
19 |
imports: [
|
20 |
CommonModule,
|
21 |
HttpClientModule,
|
22 |
+
AppRoutingModule
|
|
|
|
|
23 |
],
|
24 |
providers: [
|
25 |
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true },
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
5 |
+
<div class="ui grid">
|
6 |
+
<div class="four wide column">
|
7 |
+
<h4>Autoren</h4>
|
8 |
+
<ng-container *ngFor="let author of book.authors">
|
9 |
+
{{ author }}<br>
|
10 |
+
</ng-container>
|
11 |
+
</div>
|
12 |
+
<div class="four wide column">
|
13 |
+
<h4>ISBN</h4>
|
14 |
+
{{ book.isbn | isbn }}
|
15 |
+
</div>
|
16 |
+
<div class="four wide column">
|
17 |
+
<h4>Erschienen</h4>
|
18 |
+
{{ book.published | date:'longDate' }}
|
19 |
+
</div>
|
20 |
+
<div class="four wide column">
|
21 |
+
<h4>Rating</h4>
|
22 |
+
<ng-container
|
23 |
+
*ngFor="let r of getRating(book.rating);
|
24 |
+
index as i">
|
25 |
+
<i class="yellow star icon"
|
26 |
+
*bmDelay="500 + i * 200"></i>
|
27 |
+
</ng-container>
|
28 |
+
</div>
|
29 |
+
</div>
|
30 |
+
<h4>Beschreibung</h4>
|
31 |
+
<p>{{ book.description }}</p>
|
32 |
+
<div class="ui small images">
|
33 |
+
<img *ngFor="let thumbnail of book.thumbnails"
|
34 |
+
[src]="thumbnail.url">
|
35 |
+
</div>
|
36 |
+
<button class="ui tiny red labeled icon button"
|
37 |
+
(click)="removeBook()">
|
38 |
+
<i class="remove icon"></i> Buch löschen
|
39 |
+
</button>
|
40 |
+
<a class="ui tiny yellow labeled icon button"
|
41 |
+
[routerLink]="['../../admin/edit', book.isbn]">
|
42 |
+
<i class="write icon"></i> Buch bearbeiten
|
43 |
+
</a>
|
44 |
+
</div>
|
45 |
+
|
46 |
+
<ng-template #loading>
|
47 |
+
<div class="ui active centered inline loader"></div>
|
48 |
+
</ng-template>
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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';
|
6 |
+
|
7 |
+
@Component({
|
8 |
+
selector: 'bm-book-details',
|
9 |
+
templateUrl: './book-details.component.html',
|
10 |
+
styleUrls: ['./book-details.component.css']
|
11 |
+
})
|
12 |
+
export class BookDetailsComponent implements OnInit {
|
13 |
+
book: Book;
|
14 |
+
|
15 |
+
constructor(
|
16 |
+
private bs: BookStoreService,
|
17 |
+
private router: Router,
|
18 |
+
private route: ActivatedRoute
|
19 |
+
) { }
|
20 |
+
|
21 |
+
ngOnInit() {
|
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 (confirm('Buch wirklich löschen?')) {
|
33 |
+
this.bs.remove(this.book.isbn)
|
34 |
+
.subscribe(res => this.router.navigate(['../'], { relativeTo: this.route }));
|
35 |
+
}
|
36 |
+
}
|
37 |
+
}
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<div class="ui middle aligned selection divided list">
|
2 |
+
|
3 |
+
<ng-container *ngIf="books$ | async as books; else loading">
|
4 |
+
<bm-book-list-item class="item"
|
5 |
+
*ngFor="let b of books"
|
6 |
+
[book]="b"
|
7 |
+
[routerLink]="b.isbn"></bm-book-list-item>
|
8 |
+
|
9 |
+
<p *ngIf="!books.length">Es wurden noch keine Bücher eingetragen.</p>
|
10 |
+
</ng-container>
|
11 |
+
|
12 |
+
<ng-template #loading>
|
13 |
+
<div class="ui active dimmer">
|
14 |
+
<div class="ui large text loader">Daten werden geladen...</div>
|
15 |
+
</div>
|
16 |
+
</ng-template>
|
17 |
+
|
18 |
+
</div>
|
19 |
+
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Component, OnInit } from '@angular/core';
|
2 |
+
import { Observable } from 'rxjs';
|
3 |
+
|
4 |
+
import { Book } from '../../shared/book';
|
5 |
+
import { BookStoreService } from '../../shared/book-store.service';
|
6 |
+
|
7 |
+
@Component({
|
8 |
+
selector: 'bm-book-list',
|
9 |
+
templateUrl: './book-list.component.html',
|
10 |
+
styleUrls: ['./book-list.component.css']
|
11 |
+
})
|
12 |
+
export class BookListComponent implements OnInit {
|
13 |
+
books$: Observable<Book[]>;
|
14 |
+
|
15 |
+
constructor(private bs: BookStoreService) { }
|
16 |
+
|
17 |
+
ngOnInit() {
|
18 |
+
this.books$ = this.bs.getAll();
|
19 |
+
}
|
20 |
+
}
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<img class="ui tiny image"
|
2 |
+
*ngIf="book.thumbnails && book.thumbnails[0] && book.thumbnails[0].url"
|
3 |
+
[src]="book.thumbnails[0].url"
|
4 |
+
bmZoom>
|
5 |
+
<div class="content">
|
6 |
+
<div class="header">{{ book.title }}</div>
|
7 |
+
<div *ngIf="book.subtitle" class="description">{{ book.subtitle }}</div>
|
8 |
+
<div class="metadata">
|
9 |
+
<span *ngFor="let author of book.authors; last as l">
|
10 |
+
{{ author }}<span *ngIf="!l">, </span>
|
11 |
+
</span>
|
12 |
+
<br>
|
13 |
+
ISBN {{ book.isbn | isbn }}
|
14 |
+
</div>
|
15 |
+
</div>
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Component, OnInit, Input } from '@angular/core';
|
2 |
+
|
3 |
+
import { Book } from '../../shared/book';
|
4 |
+
|
5 |
+
@Component({
|
6 |
+
selector: 'bm-book-list-item',
|
7 |
+
templateUrl: './book-list-item.component.html',
|
8 |
+
styleUrls: ['./book-list-item.component.css']
|
9 |
+
})
|
10 |
+
export class BookListItemComponent implements OnInit {
|
11 |
+
@Input() book: Book;
|
12 |
+
|
13 |
+
ngOnInit() {
|
14 |
+
}
|
15 |
+
}
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NgModule } from '@angular/core';
|
2 |
+
import { Routes, RouterModule } from '@angular/router';
|
3 |
+
|
4 |
+
import { BookListComponent } from './book-list/book-list.component';
|
5 |
+
import { BookDetailsComponent } from './book-details/book-details.component';
|
6 |
+
|
7 |
+
const routes: Routes = [
|
8 |
+
{
|
9 |
+
path: '',
|
10 |
+
component: BookListComponent
|
11 |
+
},
|
12 |
+
{
|
13 |
+
path: ':isbn',
|
14 |
+
component: BookDetailsComponent
|
15 |
+
}
|
16 |
+
];
|
17 |
+
|
18 |
+
@NgModule({
|
19 |
+
imports: [RouterModule.forChild(routes)],
|
20 |
+
exports: [RouterModule],
|
21 |
+
providers: []
|
22 |
+
})
|
23 |
+
export class BooksRoutingModule { }
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { DelayDirective } from './shared/delay.directive';
|
2 |
+
import { NgModule } from '@angular/core';
|
3 |
+
import { CommonModule } from '@angular/common';
|
4 |
+
import { BooksRoutingModule } from './books-routing.module';
|
5 |
+
|
6 |
+
import { BookListComponent } from './book-list/book-list.component';
|
7 |
+
import { BookListItemComponent } from './book-list-item/book-list-item.component';
|
8 |
+
import { BookDetailsComponent } from './book-details/book-details.component';
|
9 |
+
import { IsbnPipe } from './shared/isbn.pipe';
|
10 |
+
import { ZoomDirective } from './shared/zoom.directive';
|
11 |
+
|
12 |
+
@NgModule({
|
13 |
+
imports: [
|
14 |
+
CommonModule,
|
15 |
+
BooksRoutingModule
|
16 |
+
],
|
17 |
+
declarations: [
|
18 |
+
BookListComponent,
|
19 |
+
BookListItemComponent,
|
20 |
+
BookDetailsComponent,
|
21 |
+
IsbnPipe,
|
22 |
+
ZoomDirective,
|
23 |
+
DelayDirective
|
24 |
+
]
|
25 |
+
})
|
26 |
+
export class BooksModule { }
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Directive, OnInit, Input, TemplateRef, ViewContainerRef } from '@angular/core';
|
2 |
+
|
3 |
+
@Directive({
|
4 |
+
selector: '[bmDelay]'
|
5 |
+
})
|
6 |
+
export class DelayDirective implements OnInit {
|
7 |
+
@Input() bmDelay;
|
8 |
+
|
9 |
+
constructor(
|
10 |
+
private templateRef: TemplateRef<any>,
|
11 |
+
private viewContainerRef: ViewContainerRef
|
12 |
+
) { }
|
13 |
+
|
14 |
+
ngOnInit() {
|
15 |
+
setTimeout(() => {
|
16 |
+
this.viewContainerRef.createEmbeddedView(this.templateRef);
|
17 |
+
}, this.bmDelay);
|
18 |
+
}
|
19 |
+
|
20 |
+
}
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Pipe, PipeTransform } from '@angular/core';
|
2 |
+
|
3 |
+
@Pipe({
|
4 |
+
name: 'isbn'
|
5 |
+
})
|
6 |
+
export class IsbnPipe implements PipeTransform {
|
7 |
+
|
8 |
+
transform(value: string): string {
|
9 |
+
if (!value) { return null; }
|
10 |
+
return `${value.substr(0, 3)}-${value.substr(3)}`;
|
11 |
+
}
|
12 |
+
}
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Directive, HostBinding, HostListener } from '@angular/core';
|
2 |
+
|
3 |
+
@Directive({
|
4 |
+
selector: '[bmZoom]'
|
5 |
+
})
|
6 |
+
export class ZoomDirective {
|
7 |
+
@HostBinding('class.small') isZoomed: boolean;
|
8 |
+
|
9 |
+
@HostListener('mouseenter') onMouseEnter() {
|
10 |
+
this.isZoomed = true;
|
11 |
+
}
|
12 |
+
@HostListener('mouseleave') onMouseLeave() {
|
13 |
+
this.isZoomed = false;
|
14 |
+
}
|
15 |
+
}
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Injectable } from '@angular/core';
|
2 |
+
import { CanActivate } from '@angular/router';
|
3 |
+
|
4 |
+
@Injectable({
|
5 |
+
providedIn: 'root'
|
6 |
+
})
|
7 |
+
export class CanNavigateToAdminGuard implements CanActivate {
|
8 |
+
|
9 |
+
accessGranted = false;
|
10 |
+
|
11 |
+
canActivate(): boolean {
|
12 |
+
if (!this.accessGranted) {
|
13 |
+
this.accessGranted = window.confirm('Mit großer Macht kommt große Verantwortung. Möchten Sie den Admin-Bereich betreten?');
|
14 |
+
}
|
15 |
+
return this.accessGranted;
|
16 |
+
}
|
17 |
+
}
|