20230802FertigstellungVonBookMonkey
This commit is contained in:
13
package-lock.json
generated
13
package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"@angular/platform-browser": "^16.1.0",
|
||||
"@angular/platform-browser-dynamic": "^16.1.0",
|
||||
"@angular/router": "^16.1.0",
|
||||
"angular-date-value-accessor": "^3.0.0",
|
||||
"book-monkey5-styles": "^1.0.4",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
@@ -4562,6 +4563,18 @@
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/angular-date-value-accessor": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/angular-date-value-accessor/-/angular-date-value-accessor-3.0.0.tgz",
|
||||
"integrity": "sha512-1Oxm2fkKgleIQdZ50BiBSoMz7+qyQLI4rD+qllfJo7gCVIpkXLlWAObFUPVFte+SngdHSycH00bVMb8co8/lhw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">= 14.0.0",
|
||||
"@angular/core": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@angular/platform-browser": "^16.1.0",
|
||||
"@angular/platform-browser-dynamic": "^16.1.0",
|
||||
"@angular/router": "^16.1.0",
|
||||
"angular-date-value-accessor": "^3.0.0",
|
||||
"book-monkey5-styles": "^1.0.4",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { BookCreateComponent } from './book-create/book-create.component';
|
||||
import { BookEditComponent } from './book-edit/book-edit.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'admin', redirectTo: 'admin/create' },
|
||||
{ path: 'admin/create', component: BookCreateComponent },
|
||||
{ path: '', redirectTo: 'create', pathMatch: 'full' },
|
||||
{ path: 'create', component: BookCreateComponent },
|
||||
{ path: 'edit/:isbn', component: BookEditComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -2,15 +2,20 @@ import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { AdminRoutingModule } from './admin-routing.module';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { LocalIsoDateValueAccessor } from 'angular-date-value-accessor';
|
||||
import { BookFormComponent } from './book-form/book-form.component';
|
||||
import { BookCreateComponent } from './book-create/book-create.component';
|
||||
import { BookEditComponent } from './book-edit/book-edit.component';
|
||||
import { FormErrorsComponent } from './form-errors/form-errors.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
BookFormComponent,
|
||||
BookCreateComponent
|
||||
declarations: [BookFormComponent, BookCreateComponent, BookEditComponent, FormErrorsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
AdminRoutingModule,
|
||||
ReactiveFormsModule,
|
||||
LocalIsoDateValueAccessor,
|
||||
],
|
||||
imports: [CommonModule, AdminRoutingModule, FormsModule],
|
||||
})
|
||||
export class AdminModule {}
|
||||
|
||||
0
src/app/admin/book-edit/book-edit.component.css
Normal file
0
src/app/admin/book-edit/book-edit.component.css
Normal file
7
src/app/admin/book-edit/book-edit.component.html
Normal file
7
src/app/admin/book-edit/book-edit.component.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<h1>Edit Book</h1>
|
||||
|
||||
<bm-book-form
|
||||
*ngIf="book$ | async as book"
|
||||
[book]="book"
|
||||
(submitBook)="update($event)"
|
||||
></bm-book-form>
|
||||
21
src/app/admin/book-edit/book-edit.component.spec.ts
Normal file
21
src/app/admin/book-edit/book-edit.component.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BookEditComponent } from './book-edit.component';
|
||||
|
||||
describe('BookEditComponent', () => {
|
||||
let component: BookEditComponent;
|
||||
let fixture: ComponentFixture<BookEditComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [BookEditComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(BookEditComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
31
src/app/admin/book-edit/book-edit.component.ts
Normal file
31
src/app/admin/book-edit/book-edit.component.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { map, Observable, switchMap } from 'rxjs';
|
||||
import { Book } from 'src/shared/book';
|
||||
import { BookStoreService } from 'src/app/shared/book-store.service';
|
||||
|
||||
@Component({
|
||||
selector: 'bm-book-edit',
|
||||
templateUrl: './book-edit.component.html',
|
||||
styleUrls: ['./book-edit.component.css'],
|
||||
})
|
||||
export class BookEditComponent {
|
||||
book$: Observable<Book>;
|
||||
|
||||
constructor(
|
||||
private service: BookStoreService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
) {
|
||||
this.book$ = this.route.paramMap.pipe(
|
||||
map((params) => params.get('isbn')!),
|
||||
switchMap((isbn) => this.service.getSingle(isbn)),
|
||||
);
|
||||
}
|
||||
|
||||
update(book: Book) {
|
||||
this.service.update(book).subscribe((updatedBook) => {
|
||||
this.router.navigate(['/books', updatedBook.isbn]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,56 @@
|
||||
<form (ngSubmit)="submitForm()" #form="ngForm">
|
||||
<form [formGroup]="form" (ngSubmit)="submitForm()">
|
||||
<label for="title">Title</label>
|
||||
<input name="title" id="title" [(ngModel)]="book.title" required />
|
||||
<input id="title" formControlName="title" />
|
||||
<bm-form-errors
|
||||
controlName="title"
|
||||
[messages]="{ required: 'Title is required' }"
|
||||
></bm-form-errors>
|
||||
|
||||
<label for="subtitle">Subtitle</label>
|
||||
<input id="subtitle" formControlName="subtitle" />
|
||||
|
||||
<label for="isbn">ISBN</label>
|
||||
<input id="isbn" formControlName="isbn" />
|
||||
<bm-form-errors
|
||||
controlName="isbn"
|
||||
[messages]="{
|
||||
required: 'ISBN is required',
|
||||
isbnformat: 'ISBN must have 10 or 13 chars',
|
||||
isbnexists: 'ISBN alreads exists'
|
||||
}"
|
||||
></bm-form-errors>
|
||||
|
||||
<label>Authors</label>
|
||||
<button type="button" class="add" (click)="addAuthorControl()">
|
||||
+ Author
|
||||
</button>
|
||||
<fieldset formArrayName="authors">
|
||||
<input
|
||||
*ngFor="let a of authors.controls; index as i"
|
||||
[attr.aria-label]="'Author' + i"
|
||||
[formControlName]="i"
|
||||
/>
|
||||
</fieldset>
|
||||
<bm-form-errors
|
||||
controlName="authors"
|
||||
[messages]="{ atleastonevalue: 'At least one author required' }"
|
||||
></bm-form-errors>
|
||||
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" formControlName="description"></textarea>
|
||||
|
||||
<label for="published">Published</label>
|
||||
<input
|
||||
name="isbn"
|
||||
id="isbn"
|
||||
[(ngModel)]="book.isbn"
|
||||
required
|
||||
minlength="10"
|
||||
maxlength="13"
|
||||
type="date"
|
||||
useValueAsLocalIso
|
||||
id="published"
|
||||
formControlName="published"
|
||||
/>
|
||||
|
||||
<label for="author">Author</label>
|
||||
<input name="author" id="author" [(ngModel)]="book.authors[0]" required />
|
||||
<label for="thumbnailUrl">Thumbnail Url</label>
|
||||
<input type="url" id="thumbnailUrl" formControlName="thumbnailUrl" />
|
||||
|
||||
<button type="submit" [disabled]="form.invalid">Save</button>
|
||||
</form>
|
||||
|
||||
<!-- <pre>{{ form.value | json }}</pre> -->
|
||||
|
||||
@@ -1,21 +1,94 @@
|
||||
import { Component, Output, EventEmitter } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
Output,
|
||||
EventEmitter,
|
||||
OnChanges,
|
||||
Input,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { Book } from 'src/shared/book';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { FormArray } from '@angular/forms';
|
||||
import { atLeastOneValue, isbnFormat } from '../shared/validators';
|
||||
import { AsyncValidatorsService } from '../shared/async-validators.service';
|
||||
|
||||
@Component({
|
||||
selector: 'bm-book-form',
|
||||
templateUrl: './book-form.component.html',
|
||||
styleUrls: ['./book-form.component.css'],
|
||||
})
|
||||
export class BookFormComponent {
|
||||
export class BookFormComponent implements OnChanges {
|
||||
@Output() submitBook = new EventEmitter<Book>();
|
||||
@Input() book?: Book;
|
||||
|
||||
form = new FormGroup({
|
||||
title: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: Validators.required,
|
||||
}),
|
||||
subtitle: new FormControl('', { nonNullable: true }),
|
||||
isbn: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, isbnFormat],
|
||||
asyncValidators: inject(AsyncValidatorsService).isbnExists(),
|
||||
}),
|
||||
description: new FormControl('', { nonNullable: true }),
|
||||
published: new FormControl('', { nonNullable: true }),
|
||||
thumbnailUrl: new FormControl('', { nonNullable: true }),
|
||||
authors: this.buildAuthorsArray(['']),
|
||||
});
|
||||
|
||||
submitForm() {
|
||||
this.submitBook.emit(this.book);
|
||||
const formValue = this.form.getRawValue();
|
||||
const authors = formValue.authors.filter((author) => !!author);
|
||||
const newBook: Book = {
|
||||
...formValue,
|
||||
authors, // TODO:echte Eingabe
|
||||
};
|
||||
this.submitBook.emit(newBook);
|
||||
}
|
||||
|
||||
book: Book = {
|
||||
isbn: '',
|
||||
title: '',
|
||||
authors: [''],
|
||||
};
|
||||
// book: Book = {
|
||||
// isbn: '',
|
||||
// title: '',
|
||||
// authors: [''],
|
||||
// };
|
||||
|
||||
get authors() {
|
||||
return this.form.controls.authors;
|
||||
}
|
||||
|
||||
addAuthorControl() {
|
||||
this.authors.push(new FormControl('', { nonNullable: true }));
|
||||
}
|
||||
|
||||
private buildAuthorsArray(authors: string[]) {
|
||||
return new FormArray(
|
||||
authors.map((v) => new FormControl(v, { nonNullable: true })),
|
||||
atLeastOneValue,
|
||||
);
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
if (this.book) {
|
||||
this.setFormValues(this.book);
|
||||
this.setEditMode(true);
|
||||
} else {
|
||||
this.setEditMode(false);
|
||||
}
|
||||
}
|
||||
|
||||
private setEditMode(isEditing: boolean) {
|
||||
const isbnControle = this.form.controls.isbn;
|
||||
if (isEditing) {
|
||||
isbnControle.disable();
|
||||
} else {
|
||||
isbnControle.enable();
|
||||
}
|
||||
}
|
||||
|
||||
private setFormValues(book: Book) {
|
||||
this.form.patchValue(book);
|
||||
this.form.setControl('authors', this.buildAuthorsArray(book.authors));
|
||||
}
|
||||
}
|
||||
|
||||
0
src/app/admin/form-errors/form-errors.component.css
Normal file
0
src/app/admin/form-errors/form-errors.component.css
Normal file
1
src/app/admin/form-errors/form-errors.component.html
Normal file
1
src/app/admin/form-errors/form-errors.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<p class="error" *gnFor="let error of errors">{{ error }}</p>
|
||||
21
src/app/admin/form-errors/form-errors.component.spec.ts
Normal file
21
src/app/admin/form-errors/form-errors.component.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FormErrorsComponent } from './form-errors.component';
|
||||
|
||||
describe('FormErrorsComponent', () => {
|
||||
let component: FormErrorsComponent;
|
||||
let fixture: ComponentFixture<FormErrorsComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [FormErrorsComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(FormErrorsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
30
src/app/admin/form-errors/form-errors.component.ts
Normal file
30
src/app/admin/form-errors/form-errors.component.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { FormGroupDirective } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'bm-form-errors',
|
||||
templateUrl: './form-errors.component.html',
|
||||
styleUrls: ['./form-errors.component.css'],
|
||||
})
|
||||
export class FormErrorsComponent {
|
||||
@Input() controlName?: string;
|
||||
@Input() messages: { [errorCode: string]: string } = {};
|
||||
|
||||
constructor(private form: FormGroupDirective) {}
|
||||
|
||||
get errors(): string[] {
|
||||
if (!this.controlName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const control = this.form.control.get(this.controlName);
|
||||
|
||||
if (!control || !control.errors || !control.touched) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(control.errors).map((errorCode) => {
|
||||
return this.messages[errorCode];
|
||||
});
|
||||
}
|
||||
}
|
||||
16
src/app/admin/shared/async-validators.service.spec.ts
Normal file
16
src/app/admin/shared/async-validators.service.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AsyncValidatorsService } from './async-validators.service';
|
||||
|
||||
describe('AsyncValidatorsService', () => {
|
||||
let service: AsyncValidatorsService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(AsyncValidatorsService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
19
src/app/admin/shared/async-validators.service.ts
Normal file
19
src/app/admin/shared/async-validators.service.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AsyncValidatorFn } from '@angular/forms';
|
||||
import { map } from 'rxjs';
|
||||
import { BookStoreService } from 'src/app/shared/book-store.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AsyncValidatorsService {
|
||||
constructor(private service: BookStoreService) {}
|
||||
|
||||
isbnExists(): AsyncValidatorFn {
|
||||
return (control) => {
|
||||
return this.service
|
||||
.check(control.value)
|
||||
.pipe(map((exists) => (exists ? { isbnexists: true } : null)));
|
||||
};
|
||||
}
|
||||
}
|
||||
26
src/app/admin/shared/validators.ts
Normal file
26
src/app/admin/shared/validators.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { isFormArray, ValidatorFn } from '@angular/forms';
|
||||
|
||||
export const atLeastOneValue: ValidatorFn = function (control) {
|
||||
if (!isFormArray(control)) {
|
||||
return null;
|
||||
}
|
||||
if (control.controls.some((el) => !!el.value)) {
|
||||
return null;
|
||||
} else {
|
||||
return { atleastonevalue: true };
|
||||
}
|
||||
};
|
||||
|
||||
export const isbnFormat: ValidatorFn = function (control) {
|
||||
if (!control.value || typeof control.value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const isbnWithoutDashes = control.value.replace(/-/g, '');
|
||||
const length = isbnWithoutDashes.length;
|
||||
|
||||
if (length === 10 || length === 13) {
|
||||
return null;
|
||||
} else {
|
||||
return { isbnformat: true };
|
||||
}
|
||||
};
|
||||
@@ -1,10 +1,22 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { authGuard } from './shared/auth.guard';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
||||
{ path: 'home', component: HomeComponent },
|
||||
{
|
||||
path: 'books',
|
||||
loadChildren: () =>
|
||||
import('./books/books.module').then((m) => m.BooksModule),
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
loadChildren: () =>
|
||||
import('./admin/admin.module').then((m) => m.AdminModule),
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -3,16 +3,14 @@ import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { BooksModule } from './books/books.module';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
|
||||
import { SearchComponent } from './search/search.component';
|
||||
import { AuthInterceptor } from './shared/auth.interceptor';
|
||||
import { AdminModule } from './admin/admin.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent, HomeComponent, SearchComponent],
|
||||
imports: [BrowserModule, AppRoutingModule, BooksModule, HttpClientModule, AdminModule],
|
||||
imports: [BrowserModule, AppRoutingModule, HttpClientModule],
|
||||
providers: [
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<div class="details" *ngIf="book$ | async as book">
|
||||
<h1>{{ book.title }}</h1>
|
||||
<p role="doc-subtitle" *ngIf="book.subtilte">{{ book.subtilte }}</p>
|
||||
|
||||
<div class="header">
|
||||
<div>
|
||||
<h2>Authors</h2>
|
||||
@@ -8,18 +9,30 @@
|
||||
<li *ngFor="let author of book.authors">{{ author }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>ISBN</h2>
|
||||
{{ book.isbn }}
|
||||
{{ book.isbn | isbn }}
|
||||
</div>
|
||||
<div *ngIf="book.published">
|
||||
<h2>Published</h2>
|
||||
{{ book.published }}
|
||||
{{ book.published | date: "longDate" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Description</h2>
|
||||
<p>{{ book.description }}</p>
|
||||
<img *ngIf="book.thumbnailUrl" [src]="book.thumbnailUrl" alt="Cover" />
|
||||
<a class="button" routerLink="..">Back to list</a>
|
||||
<button class="red" (click)="removeBook(book.isbn)">Remove book</button>
|
||||
<ng-container *bmLoggedinOnly>
|
||||
<button
|
||||
class="red"
|
||||
bmConfirm="Remove book?"
|
||||
(confirm)="removeBook(book.isbn)"
|
||||
>
|
||||
Remove book
|
||||
</button>
|
||||
|
||||
<a class="button" [routerLink]="['/admin/edit', book.isbn]">Edit book</a>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -21,10 +21,8 @@ export class BookDetailsComponent {
|
||||
this.book$ = this.service.getSingle(isbn);
|
||||
}
|
||||
removeBook(isbn: string) {
|
||||
if (window.confirm('Remove book?')) {
|
||||
this.service.remove(isbn).subscribe(() => {
|
||||
this.router.navigateByUrl('/books');
|
||||
});
|
||||
}
|
||||
this.service.remove(isbn).subscribe(() => {
|
||||
this.router.navigateByUrl('/books');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
{{ author }}
|
||||
</li>
|
||||
</ul>
|
||||
<div>ISBN {{ book.isbn }}</div>
|
||||
<div>ISBN {{ book.isbn | isbn }}</div>
|
||||
</a>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { BookDetailsComponent } from './book-details/book-details.component';
|
||||
import { BookListComponent } from './book-list/book-list.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'books', component: BookListComponent },
|
||||
{ path: 'books/:isbn', component: BookDetailsComponent },
|
||||
{ path: '', component: BookListComponent },
|
||||
{ path: ':isbn', component: BookDetailsComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -5,12 +5,18 @@ import { BooksRoutingModule } from './books-routing.module';
|
||||
import { BookListComponent } from './book-list/book-list.component';
|
||||
import { BookListItemComponent } from './book-list-item/book-list-item.component';
|
||||
import { BookDetailsComponent } from './book-details/book-details.component';
|
||||
import { IsbnPipe } from './shared/isbn.pipe';
|
||||
import { ConfirmDirective } from './shared/confirm.directive';
|
||||
import { LoggedinOnlyDirective } from './shared/loggedin-only.directive';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
BookDetailsComponent,
|
||||
BookListComponent,
|
||||
BookListItemComponent,
|
||||
IsbnPipe,
|
||||
ConfirmDirective,
|
||||
LoggedinOnlyDirective,
|
||||
],
|
||||
imports: [CommonModule, BooksRoutingModule],
|
||||
exports: [],
|
||||
|
||||
8
src/app/books/shared/confirm.directive.spec.ts
Normal file
8
src/app/books/shared/confirm.directive.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ConfirmDirective } from './confirm.directive';
|
||||
|
||||
describe('ConfirmDirective', () => {
|
||||
it('should create an instance', () => {
|
||||
const directive = new ConfirmDirective();
|
||||
expect(directive).toBeTruthy();
|
||||
});
|
||||
});
|
||||
21
src/app/books/shared/confirm.directive.ts
Normal file
21
src/app/books/shared/confirm.directive.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
Directive,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[bmConfirm]',
|
||||
})
|
||||
export class ConfirmDirective {
|
||||
@Input('bmConform') confirmText?: string;
|
||||
@Output() confirm = new EventEmitter<void>();
|
||||
@HostListener('click') onClick() {
|
||||
if (window.confirm(this.confirmText)) {
|
||||
this.confirm.emit();
|
||||
}
|
||||
}
|
||||
constructor() {}
|
||||
}
|
||||
8
src/app/books/shared/isbn.pipe.spec.ts
Normal file
8
src/app/books/shared/isbn.pipe.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { IsbnPipe } from './isbn.pipe';
|
||||
|
||||
describe('IsbnPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new IsbnPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
||||
13
src/app/books/shared/isbn.pipe.ts
Normal file
13
src/app/books/shared/isbn.pipe.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'isbn',
|
||||
})
|
||||
export class IsbnPipe implements PipeTransform {
|
||||
transform(value: string): string {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return `${value.substring(0, 3)}-${value.substring(3)}`;
|
||||
}
|
||||
}
|
||||
8
src/app/books/shared/loggedin-only.directive.spec.ts
Normal file
8
src/app/books/shared/loggedin-only.directive.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { LoggedinOnlyDirective } from './loggedin-only.directive';
|
||||
|
||||
describe('LoggedinOnlyDirective', () => {
|
||||
it('should create an instance', () => {
|
||||
const directive = new LoggedinOnlyDirective();
|
||||
expect(directive).toBeTruthy();
|
||||
});
|
||||
});
|
||||
34
src/app/books/shared/loggedin-only.directive.ts
Normal file
34
src/app/books/shared/loggedin-only.directive.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Directive,
|
||||
OnDestroy,
|
||||
ViewContainerRef,
|
||||
TemplateRef,
|
||||
} from '@angular/core';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { AuthService } from 'src/app/shared/auth.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[bmLoggedinOnly]',
|
||||
})
|
||||
export class LoggedinOnlyDirective implements OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private template: TemplateRef<unknown>,
|
||||
private viewContainer: ViewContainerRef,
|
||||
private authService: AuthService,
|
||||
) {
|
||||
this.authService.isAuthenticated$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((isAuthenticated) => {
|
||||
if (isAuthenticated) {
|
||||
this.viewContainer.createEmbeddedView(this.template);
|
||||
} else {
|
||||
this.viewContainer.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
}
|
||||
}
|
||||
17
src/app/shared/auth.guard.spec.ts
Normal file
17
src/app/shared/auth.guard.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { CanActivateFn } from '@angular/router';
|
||||
|
||||
import { authGuard } from './auth.guard';
|
||||
|
||||
describe('authGuard', () => {
|
||||
const executeGuard: CanActivateFn = (...guardParameters) =>
|
||||
TestBed.runInInjectionContext(() => authGuard(...guardParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeGuard).toBeTruthy();
|
||||
});
|
||||
});
|
||||
21
src/app/shared/auth.guard.ts
Normal file
21
src/app/shared/auth.guard.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Inject, inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { AuthService } from './auth.service';
|
||||
import { map, take } from 'rxjs';
|
||||
|
||||
export const authGuard: CanActivateFn = (route, state) => {
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
return authService.isAuthenticated$.pipe(
|
||||
take(1),
|
||||
map((isAuthenticated) => {
|
||||
if (authService.isAuthenticated) {
|
||||
return true;
|
||||
} else {
|
||||
window.alert('Not logged in!');
|
||||
return router.parseUrl('home');
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -40,4 +40,12 @@ export class BookStoreService {
|
||||
create(book: Book): Observable<Book> {
|
||||
return this.http.post<Book>(`${this.apiUrl}/books`, book);
|
||||
}
|
||||
|
||||
update(book: Book): Observable<Book> {
|
||||
return this.http.put<Book>(`${this.apiUrl}/book/${book.isbn}`, book);
|
||||
}
|
||||
|
||||
check(isbn: string): Observable<boolean> {
|
||||
return this.http.get<boolean>(`${this.apiUrl}/books/${isbn}/check`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user