Move book card to own component

WIP: Edit Book's shelf functionality
This commit is contained in:
aditya.chandel
2024-12-20 23:49:32 -07:00
parent c72301f6c6
commit 1abe138dd1
16 changed files with 259 additions and 101 deletions

View File

@@ -17,6 +17,7 @@ export class AppComponent implements OnInit {
ngOnInit(): void {
this.libraryBookService.initializeLibraries();
this.libraryBookService.initializeShelves();
this.rxStompService.watch('/topic/books').subscribe((message: Message) => {
const book: Book = JSON.parse(message.body);
this.libraryBookService.handleNewBook(book);

View File

@@ -26,12 +26,15 @@ import {RxStompService} from './rx-stomp.service';
import {VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller';
import {LazyLoadImageModule} from 'ng-lazyload-image';
import {ConfirmDialogModule} from 'primeng/confirmdialog';
import { ShelfAssignerComponent } from './shelf-assigner/shelf-assigner.component';
import {CheckboxModule} from 'primeng/checkbox';
@NgModule({
declarations: [
AppComponent,
DirectoryPickerComponent,
LibraryCreatorComponent
LibraryCreatorComponent,
ShelfAssignerComponent
],
imports: [
BrowserModule,
@@ -53,6 +56,7 @@ import {ConfirmDialogModule} from 'primeng/confirmdialog';
VirtualScrollerModule,
LazyLoadImageModule,
ConfirmDialogModule,
CheckboxModule,
],
providers: [
DialogService,

View File

@@ -0,0 +1,14 @@
<div class="book-card">
<div class="cover-container">
<img [src]="coverImageSrc(book)" class="book-cover placeholder" alt="Cover of {{ book.metadata.title }}" loading="lazy" />
<p-button [rounded]="true" icon="pi pi-info" class="read-btn" (click)="openBookInfo(book)"></p-button>
<p-button [rounded]="true" icon="pi pi-book" class="info-btn" (click)="readBook(book)"></p-button>
</div>
<div class="book-info">
<div class="book-title-container">
<h4 class="book-title">{{ book.metadata.title }}</h4>
<p-menu #menu [model]="items" [popup]="true" appendTo="body" />
<p-button [text]="true" (click)="menu.toggle($event)" icon="pi pi-ellipsis-v" />
</div>
</div>
</div>

View File

@@ -0,0 +1,67 @@
.book-card {
background-color: var(--surface-card);
border-radius: 8px 8px 0 0;
position: relative;
overflow: hidden;
}
.cover-container {
position: relative;
}
.book-title-container {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: row;
}
.book-title {
font-size: 0.85rem;
color: var(--text-color);
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
padding: 6px 6px 3px;
}
.book-cover {
width: 100%;
height: auto;
max-height: 100%;
object-fit: cover;
border-radius: 8px 8px 0 0;
transition: filter 0.2s ease-in-out;
}
.info-btn, .read-btn {
position: absolute;
left: 50%;
transform: translateX(-50%);
display: none;
transition: opacity 0.2s ease-in-out;
}
.info-btn {
top: 25%; /* 30% below the top of the image */
}
.read-btn {
bottom: 25%; /* 30% above the bottom of the image */
}
.cover-container {
position: relative; /* Ensures buttons are positioned relative to the image */
}
.cover-container:hover .book-cover {
filter: blur(2px);
}
.cover-container:hover .info-btn,
.cover-container:hover .read-btn {
display: block;
}

View File

@@ -0,0 +1,75 @@
import {Component, Input, OnInit} from '@angular/core';
import {Book} from '../book/model/book.model';
import {Button} from 'primeng/button';
import {LibraryAndBookService} from '../book/service/library-and-book.service';
import {Router} from '@angular/router';
import {MenuModule} from 'primeng/menu';
import {MenuItem} from 'primeng/api';
import {DirectoryPickerComponent} from "../book/component/directory-picker/directory-picker.component";
import {DialogService} from "primeng/dynamicdialog";
import {ShelfAssignerComponent} from "../shelf-assigner/shelf-assigner.component";
@Component({
selector: 'app-book-card',
templateUrl: './book-card.component.html',
imports: [
Button,
MenuModule
],
styleUrls: ['./book-card.component.scss']
})
export class BookCardComponent implements OnInit {
@Input() book!: Book;
items: MenuItem[] | undefined;
constructor(
private libraryBookService: LibraryAndBookService, private router: Router,
private dialogService: DialogService) {
}
coverImageSrc(book: Book): string {
return this.libraryBookService.getBookCoverUrl(book.id);
}
readBook(book: Book) {
this.libraryBookService.readBook(book);
}
openBookInfo(book: Book) {
this.router.navigate(['/library', book.libraryId, 'book', book.id, 'info']);
}
ngOnInit(): void {
this.items = [
{
label: 'Options',
items: [
{
label: 'Add to shelf',
icon: 'pi pi-folder',
command: () => {
this.openShelfDialog(this.book);
}
}
]
}
];
}
private openShelfDialog(book: Book) {
this.dialogService.open(ShelfAssignerComponent, {
header: 'Edit Shelves for Book: ' + book.metadata.title,
modal: true,
width: '50%',
height: '75%',
contentStyle: {overflow: 'auto'},
baseZIndex: 10,
data: {
book: this.book
},
});
}
}

View File

@@ -3,7 +3,7 @@
}
.last-read-title-container {
}
.last-read-title {
@@ -73,7 +73,7 @@ p {
text-overflow: ellipsis;
}
.view-btn, .read-btn {
.info-btn, .read-btn {
position: absolute;
top: 50%;
left: 50%;
@@ -82,7 +82,7 @@ p {
margin: -60px 0;
}
.book-item:hover .view-btn {
.book-item:hover .info-btn {
display: block;
}

View File

@@ -11,3 +11,10 @@
<li *ngIf="item.separator" class="menu-separator"></li>
</ng-container>
</ul>
<ul class="layout-menu">
<ng-container *ngFor="let item of shelfMenu(); let i = index;">
<li app-menuitem *ngIf="!item.separator" [item]="item" [index]="i" [root]="true"></li>
<li *ngIf="item.separator" class="menu-separator"></li>
</ng-container>
</ul>

View File

@@ -27,6 +27,21 @@ export class AppMenuComponent implements OnInit {
];
});
shelfMenu = computed(() => {
const shelves = this.libraryBookService.shelves();
return [
{
label: 'Shelves',
separator: false,
items: shelves.map((shelf) => ({
label: shelf.name,
icon: 'pi pi-fw pi-heart',
routerLink: [`/shelf/${shelf.id}/books`],
})),
},
];
});
constructor(private libraryBookService: LibraryAndBookService) {
}

View File

@@ -26,15 +26,7 @@
<virtual-scroller class="virtual-scroller" #scroll [items]="currentLibraryBooks()">
<div class="grid" #container>
<div class="virtual-scroller-item" *ngFor="let book of scroll.viewPortItems">
<img [src]="coverImageSrc(book.id)" class="book-cover placeholder" alt="Cover of {{ book.metadata.title }}" loading="lazy"/>
<div class="book-info">
<div class="book-title-container">
<h4 class="book-title">{{ book.metadata.title }}</h4>
<p-button [text]="true" [rounded]="true" icon="pi pi-ellipsis-v" class="kebab-menu-btn"></p-button>
</div>
<p-button [rounded]="true" icon="pi pi-eye" class="view-btn" (click)="readBook(book)"></p-button>
<p-button [rounded]="true" icon="pi pi-info" class="read-btn" (click)="openBookInfo(book.id, book.libraryId)"></p-button>
</div>
<app-book-card [book]="book"></app-book-card>
</div>
</div>
</virtual-scroller>

View File

@@ -1,10 +1,3 @@
.book-title-container {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: row;
}
.library-header {
display: flex;
justify-content: space-between;
@@ -44,11 +37,8 @@
}
.virtual-scroller-item {
background-color: var(--surface-card);
height: 243px;
width: 150px;
border-radius: 8px 8px 0 0;
position: relative;
}
.grid {
@@ -60,60 +50,3 @@
overflow: hidden;
align-items: start;
}
.book-cover {
width: 100%;
height: auto;
max-height: 100%;
object-fit: cover;
border-radius: 8px 8px 0 0;
}
.book-title {
font-size: 0.85rem;
color: var(--text-color);
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
padding: 6px 6px 3px;
}
.book-authors {
color: var(--text-color-secondary);
font-size: 0.8rem;
line-height: 1.4;
margin: 0 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
.view-btn, .read-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: none;
margin-top: -40px;
transition: opacity 0.2s ease-in-out;
}
.virtual-scroller-item:hover .view-btn,
.virtual-scroller-item:hover .read-btn {
display: block;
}
.virtual-scroller-item:hover .read-btn {
top: 70%;
}
.virtual-scroller-item:hover .book-cover {
filter: blur(2px);
}

View File

@@ -1,7 +1,6 @@
import {Component, computed, OnInit, signal} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {LibraryAndBookService} from '../../service/library-and-book.service';
import {Book} from '../../model/book.model';
import {Button} from 'primeng/button';
import {NgForOf} from '@angular/common';
import {FormsModule} from '@angular/forms';
@@ -9,8 +8,9 @@ import {DropdownModule} from 'primeng/dropdown';
import {LazyLoadImageModule} from 'ng-lazyload-image';
import {VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller';
import {SpeedDialModule} from 'primeng/speeddial';
import {ConfirmationService, MenuItem, MenuItemCommandEvent, MessageService} from 'primeng/api';
import {ConfirmationService, MenuItem, MessageService} from 'primeng/api';
import {ConfirmDialogModule} from 'primeng/confirmdialog';
import {BookCardComponent} from '../../../book-card/book-card.component';
@Component({
selector: 'app-library-browser-v2',
@@ -24,7 +24,8 @@ import {ConfirmDialogModule} from 'primeng/confirmdialog';
LazyLoadImageModule,
VirtualScrollerModule,
SpeedDialModule,
ConfirmDialogModule
ConfirmDialogModule,
BookCardComponent
]
})
export class LibraryBrowserComponent implements OnInit {
@@ -113,20 +114,4 @@ export class LibraryBrowserComponent implements OnInit {
});
}
coverImageSrc(bookId: number): string {
return this.libraryBookService.getBookCoverUrl(bookId);
}
getAuthorNames(book: Book): string {
return book.metadata.authors?.map((author) => author.name).join(', ') || 'No authors available';
}
readBook(book: Book): void {
this.libraryBookService.readBook(book);
}
openBookInfo(bookId: number, libraryId: number) {
this.router.navigate(['/library', libraryId, 'book', bookId, 'info']);
}
}

View File

@@ -1,7 +1,8 @@
export interface Book {
id: number;
libraryId: number;
metadata: BookMetadata
metadata: BookMetadata;
shelves?: Shelf[];
}
export interface BookMetadata {
@@ -29,6 +30,11 @@ export interface Category {
name: string;
}
export interface Shelf {
id: number;
name: string;
}
export interface BookWithNeighborsDTO {
currentBook: Book;
previousBookId: number | null;

View File

@@ -1,5 +1,5 @@
import {Observable, of} from 'rxjs';
import {Book, BookMetadata, BookSetting, BookWithNeighborsDTO} from '../model/book.model';
import {Book, BookMetadata, BookSetting, BookWithNeighborsDTO, Shelf} from '../model/book.model';
import {computed, Injectable, signal} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {catchError, map, tap} from 'rxjs/operators';
@@ -11,10 +11,14 @@ import {Library, LibraryApiResponse} from '../model/library.model';
export class LibraryAndBookService {
private readonly libraryUrl = 'http://localhost:8080/v1/library';
private readonly bookUrl = 'http://localhost:8080/v1/book';
private readonly shelfUrl = 'http://localhost:8080/v1/shelf';
#libraries = signal<Library[]>([]);
libraries = computed(this.#libraries);
#shelves = signal<Shelf[]>([]);
shelves = computed(this.#shelves);
#lastReadBooks = signal<Book[]>([]);
lastReadBooks = computed(this.#lastReadBooks);
lastReadBooksLoaded: boolean = false;
@@ -31,6 +35,24 @@ export class LibraryAndBookService {
}
/*---------- Shelf Methods go below ----------*/
initializeShelves(): void {
this.http.get<Shelf[]>(this.shelfUrl).pipe(
map(response => response),
catchError(error => {
console.error('Error loading libraries:', error);
return of([]);
})
).subscribe(
(shelves) => {
this.#shelves.set(shelves);
console.log("Library Initialized")
}
);
}
/*---------- Library Methods go below ----------*/
initializeLibraries(): void {

View File

@@ -0,0 +1,4 @@
<div *ngFor="let shelf of shelves()" class="flex items-center">
<p-checkbox [inputId]="shelf.name" name="group" [value]="shelf" [(ngModel)]="selectedShelves" />
<label [for]="shelf.id" class="ml-2"> {{shelf.name }} </label>
</div>

View File

@@ -0,0 +1,33 @@
import {Component, computed, OnInit} from '@angular/core';
import {LibraryAndBookService} from '../book/service/library-and-book.service';
import {DynamicDialogConfig} from 'primeng/dynamicdialog';
import {Book} from '../book/model/book.model';
@Component({
selector: 'app-shelf-assigner',
standalone: false,
templateUrl: './shelf-assigner.component.html',
styleUrl: './shelf-assigner.component.scss'
})
export class ShelfAssignerComponent implements OnInit {
shelves = computed(() => {
return this.libraryBookService.shelves();
});
book: Book;
selectedShelves: any[] = [];
constructor(private libraryBookService: LibraryAndBookService, private dynamicDialogConfig: DynamicDialogConfig) {
this.book = this.dynamicDialogConfig.data.book;
}
ngOnInit(): void {
if (this.book.shelves) {
const bookShelfIds = this.book.shelves.map(shelf => shelf.id);
this.selectedShelves = this.shelves().filter(shelf => bookShelfIds.includes(shelf.id));
}
}
}