mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
Move book card to own component
WIP: Edit Book's shelf functionality
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
14
booklore-ui/src/app/book-card/book-card.component.html
Normal file
14
booklore-ui/src/app/book-card/book-card.component.html
Normal 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>
|
||||
67
booklore-ui/src/app/book-card/book-card.component.scss
Normal file
67
booklore-ui/src/app/book-card/book-card.component.scss
Normal 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;
|
||||
}
|
||||
|
||||
75
booklore-ui/src/app/book-card/book-card.component.ts
Normal file
75
booklore-ui/src/app/book-card/book-card.component.ts
Normal 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
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user