|
@@ -9,6 +9,10 @@
|
|
| 9 |
<a routerLink="/books" routerLinkActive="active"
|
| 10 |
ariaCurrentWhenActive="page">Books</a>
|
| 11 |
</li>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
</ul>
|
| 13 |
</nav>
|
| 14 |
<router-outlet />
|
|
|
|
| 9 |
<a routerLink="/books" routerLinkActive="active"
|
| 10 |
ariaCurrentWhenActive="page">Books</a>
|
| 11 |
</li>
|
| 12 |
+
<li>
|
| 13 |
+
<a routerLink="/admin" routerLinkActive="active"
|
| 14 |
+
ariaCurrentWhenActive="page">Admin</a>
|
| 15 |
+
</li>
|
| 16 |
</ul>
|
| 17 |
</nav>
|
| 18 |
<router-outlet />
|
|
@@ -2,9 +2,11 @@ import { Routes } from '@angular/router';
|
|
| 2 |
|
| 3 |
import { HomePage } from './home-page/home-page';
|
| 4 |
import { booksPortalRoutes } from './books-portal/books-portal.routes';
|
|
|
|
| 5 |
|
| 6 |
export const routes: Routes = [
|
| 7 |
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
| 8 |
{ path: 'home', component: HomePage, title: 'BookMonkey' },
|
| 9 |
-
...booksPortalRoutes
|
|
|
|
| 10 |
];
|
|
|
|
| 2 |
|
| 3 |
import { HomePage } from './home-page/home-page';
|
| 4 |
import { booksPortalRoutes } from './books-portal/books-portal.routes';
|
| 5 |
+
import { booksAdminRoutes } from './books-admin/books-admin.routes';
|
| 6 |
|
| 7 |
export const routes: Routes = [
|
| 8 |
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
| 9 |
{ path: 'home', component: HomePage, title: 'BookMonkey' },
|
| 10 |
+
...booksPortalRoutes,
|
| 11 |
+
...booksAdminRoutes
|
| 12 |
];
|
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<h1>Create book</h1>
|
| 2 |
+
|
| 3 |
+
<form (submit)="submitForm($event)">
|
| 4 |
+
<label for="title">Title</label>
|
| 5 |
+
<input type="text" id="title" [field]="bookForm.title" />
|
| 6 |
+
|
| 7 |
+
<label for="subtitle">Subtitle</label>
|
| 8 |
+
<input type="text" id="subtitle" [field]="bookForm.subtitle" />
|
| 9 |
+
|
| 10 |
+
<label for="isbn">ISBN</label>
|
| 11 |
+
<input type="text" id="isbn" [field]="bookForm.isbn" />
|
| 12 |
+
|
| 13 |
+
<fieldset>
|
| 14 |
+
<legend>Authors</legend>
|
| 15 |
+
<button type="button" (click)="addAuthorControl()">Add Author</button>
|
| 16 |
+
<div role="group">
|
| 17 |
+
@for (authorControl of bookForm.authors; track $index) {
|
| 18 |
+
<input
|
| 19 |
+
type="text"
|
| 20 |
+
[attr.aria-label]="`Author ${$index + 1}`"
|
| 21 |
+
[field]="authorControl"
|
| 22 |
+
/>
|
| 23 |
+
}
|
| 24 |
+
</div>
|
| 25 |
+
</fieldset>
|
| 26 |
+
|
| 27 |
+
<label for="description">Description</label>
|
| 28 |
+
<textarea id="description" [field]="bookForm.description"></textarea>
|
| 29 |
+
|
| 30 |
+
<label for="imageUrl">Thumbnail URL</label>
|
| 31 |
+
<input type="url" id="imageUrl" [field]="bookForm.imageUrl" />
|
| 32 |
+
|
| 33 |
+
<button
|
| 34 |
+
type="submit"
|
| 35 |
+
[attr.aria-busy]="bookForm().submitting()">
|
| 36 |
+
Save
|
| 37 |
+
</button>
|
| 38 |
+
</form>
|
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component, inject, signal } from '@angular/core';
|
| 2 |
+
import { Field, form, submit } from '@angular/forms/signals';
|
| 3 |
+
import { Router } from '@angular/router';
|
| 4 |
+
|
| 5 |
+
import { Book } from '../../shared/book';
|
| 6 |
+
import { BookStore } from '../../shared/book-store';
|
| 7 |
+
|
| 8 |
+
@Component({
|
| 9 |
+
selector: 'app-book-create-page',
|
| 10 |
+
imports: [Field],
|
| 11 |
+
templateUrl: './book-create-page.html',
|
| 12 |
+
styleUrl: './book-create-page.scss'
|
| 13 |
+
})
|
| 14 |
+
export class BookCreatePage {
|
| 15 |
+
#bookStore = inject(BookStore);
|
| 16 |
+
#router = inject(Router);
|
| 17 |
+
|
| 18 |
+
readonly #book = signal({
|
| 19 |
+
isbn: '',
|
| 20 |
+
title: '',
|
| 21 |
+
subtitle: '',
|
| 22 |
+
authors: [''],
|
| 23 |
+
description: '',
|
| 24 |
+
imageUrl: '',
|
| 25 |
+
createdAt: new Date().toISOString(),
|
| 26 |
+
} satisfies Book);
|
| 27 |
+
protected readonly bookForm = form(this.#book);
|
| 28 |
+
|
| 29 |
+
addAuthorControl() {
|
| 30 |
+
this.bookForm.authors().value.update((authors) => [...authors, '']);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
async submitForm(e: Event) {
|
| 34 |
+
e.preventDefault();
|
| 35 |
+
|
| 36 |
+
await submit(this.bookForm, async (form) => {
|
| 37 |
+
const formValue = form().value();
|
| 38 |
+
const authors = formValue.authors.filter(author => !!author);
|
| 39 |
+
|
| 40 |
+
const newBook: Book = {
|
| 41 |
+
...formValue,
|
| 42 |
+
authors,
|
| 43 |
+
createdAt: new Date().toISOString()
|
| 44 |
+
};
|
| 45 |
+
this.#bookStore.create(newBook).subscribe(createdBook => {
|
| 46 |
+
this.#router.navigate(['/books', 'details', createdBook.isbn]);
|
| 47 |
+
});
|
| 48 |
+
});
|
| 49 |
+
}
|
| 50 |
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Routes } from '@angular/router';
|
| 2 |
+
|
| 3 |
+
import { BookCreatePage } from './book-create-page/book-create-page';
|
| 4 |
+
|
| 5 |
+
export const booksAdminRoutes: Routes = [
|
| 6 |
+
{ path: 'admin', redirectTo: 'admin/create' },
|
| 7 |
+
{ path: 'admin/create', component: BookCreatePage, title: 'Create Book' }
|
| 8 |
+
];
|
|
@@ -27,4 +27,8 @@ export class BookStore {
|
|
| 27 |
remove(isbn: string): Observable<void> {
|
| 28 |
return this.#http.delete<void>(`${this.#apiUrl}/books/${isbn}`);
|
| 29 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
}
|
|
|
|
| 27 |
remove(isbn: string): Observable<void> {
|
| 28 |
return this.#http.delete<void>(`${this.#apiUrl}/books/${isbn}`);
|
| 29 |
}
|
| 30 |
+
|
| 31 |
+
create(book: Book): Observable<Book> {
|
| 32 |
+
return this.#http.post<Book>(`${this.#apiUrl}/books`, book);
|
| 33 |
+
}
|
| 34 |
}
|