feat: support cover arts

This commit is contained in:
2026-02-04 13:11:26 -08:00
parent 318f3bc500
commit d338c8b52f
3 changed files with 411 additions and 9 deletions
@@ -97,6 +97,26 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar
></textarea>
</div>
<div class="form-group">
<label for="coverImage">Cover Image (max 500KB)</label>
<input
type="file"
id="coverImage"
name="coverImage"
accept="image/*"
(change)="onImageSelected($event, 'new')"
>
@if (newBookImagePreview()) {
<div class="image-preview">
<img [src]="newBookImagePreview()" alt="Cover preview">
<button type="button" (click)="clearImage('new')" class="btn btn-danger btn-sm">Remove</button>
</div>
}
@if (imageError()) {
<span class="error-text">{{ imageError() }}</span>
}
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Add Book</button>
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
@@ -174,6 +194,26 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar
></textarea>
</div>
<div class="form-group">
<label for="edit-coverImage">Cover Image (max 500KB)</label>
<input
type="file"
id="edit-coverImage"
name="coverImage"
accept="image/*"
(change)="onImageSelected($event, 'edit')"
>
@if (editBookImagePreview()) {
<div class="image-preview">
<img [src]="editBookImagePreview()" alt="Cover preview">
<button type="button" (click)="clearImage('edit')" class="btn btn-danger btn-sm">Remove</button>
</div>
}
@if (imageError()) {
<span class="error-text">{{ imageError() }}</span>
}
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Changes</button>
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
@@ -662,6 +702,39 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar
color: var(--witch-mauve);
font-size: 0.9rem;
}
.image-preview {
margin-top: 0.5rem;
display: flex;
align-items: center;
gap: 1rem;
}
.image-preview img {
max-width: 100px;
max-height: 150px;
border-radius: 4px;
border: 2px solid var(--witch-lavender);
}
.error-text {
color: var(--witch-rose);
font-size: 0.875rem;
display: block;
margin-top: 0.25rem;
}
input[type="file"] {
padding: 0.5rem;
border: 2px dashed var(--witch-lavender);
border-radius: 4px;
background: var(--witch-moon);
cursor: pointer;
}
input[type="file"]:hover {
border-color: var(--witch-rose);
}
`]
})
export class BooksListComponent implements OnInit {
@@ -681,6 +754,12 @@ export class BooksListComponent implements OnInit {
expandedComments = signal<Record<string, boolean>>({});
newCommentContent: Record<string, string> = {};
// Image upload state
newBookImagePreview = signal<string | null>(null);
editBookImagePreview = signal<string | null>(null);
imageError = signal<string | null>(null);
private readonly MAX_IMAGE_SIZE = 500 * 1024; // 500KB
// Expose BookStatus enum to template
BookStatus = BookStatus;
@@ -751,8 +830,11 @@ export class BooksListComponent implements OnInit {
isbn: '',
status: BookStatus.toRead,
rating: undefined,
notes: ''
notes: '',
coverImage: undefined
};
this.newBookImagePreview.set(null);
this.imageError.set(null);
}
addBook() {
@@ -764,7 +846,8 @@ export class BooksListComponent implements OnInit {
isbn: this.newBook.isbn,
status: this.newBook.status,
rating: this.newBook.rating,
notes: this.newBook.notes
notes: this.newBook.notes,
coverImage: this.newBook.coverImage
};
this.booksService.createBook(bookToAdd).subscribe(() => {
@@ -789,14 +872,19 @@ export class BooksListComponent implements OnInit {
isbn: book.isbn,
status: book.status,
rating: book.rating,
notes: book.notes
notes: book.notes,
coverImage: book.coverImage
};
this.editBookImagePreview.set(book.coverImage || null);
this.showAddForm.set(false);
this.imageError.set(null);
}
cancelEdit() {
this.editingBook.set(null);
this.editBook = {};
this.editBookImagePreview.set(null);
this.imageError.set(null);
}
saveEdit() {
@@ -813,6 +901,52 @@ export class BooksListComponent implements OnInit {
return new Date(date).toLocaleDateString();
}
// Image handling methods
onImageSelected(event: Event, target: 'new' | 'edit') {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
this.imageError.set(null);
if (file.size > this.MAX_IMAGE_SIZE) {
this.imageError.set(`Image too large. Maximum size is ${this.MAX_IMAGE_SIZE / 1024}KB.`);
input.value = '';
return;
}
if (!file.type.startsWith('image/')) {
this.imageError.set('Please select an image file.');
input.value = '';
return;
}
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result as string;
if (target === 'new') {
this.newBookImagePreview.set(base64);
this.newBook.coverImage = base64;
} else {
this.editBookImagePreview.set(base64);
this.editBook.coverImage = base64;
}
};
reader.readAsDataURL(file);
}
clearImage(target: 'new' | 'edit') {
if (target === 'new') {
this.newBookImagePreview.set(null);
this.newBook.coverImage = undefined;
} else {
this.editBookImagePreview.set(null);
this.editBook.coverImage = undefined;
}
this.imageError.set(null);
}
// Comments methods
toggleComments(bookId: string) {
const expanded = this.expandedComments();