From d338c8b52f9fb2d75d2e3db8aab7fd38fe386031 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Wed, 4 Feb 2026 13:11:26 -0800 Subject: [PATCH] feat: support cover arts --- .../components/books/books-list.component.ts | 140 +++++++++++++++++- .../components/games/games-list.component.ts | 140 +++++++++++++++++- .../components/music/music-list.component.ts | 140 +++++++++++++++++- 3 files changed, 411 insertions(+), 9 deletions(-) diff --git a/apps/frontend/src/app/components/books/books-list.component.ts b/apps/frontend/src/app/components/books/books-list.component.ts index 48b6b7a..c04f683 100644 --- a/apps/frontend/src/app/components/books/books-list.component.ts +++ b/apps/frontend/src/app/components/books/books-list.component.ts @@ -97,6 +97,26 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar > +
+ + + @if (newBookImagePreview()) { +
+ Cover preview + +
+ } + @if (imageError()) { + {{ imageError() }} + } +
+
@@ -174,6 +194,26 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar >
+
+ + + @if (editBookImagePreview()) { +
+ Cover preview + +
+ } + @if (imageError()) { + {{ imageError() }} + } +
+
@@ -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>({}); newCommentContent: Record = {}; + // Image upload state + newBookImagePreview = signal(null); + editBookImagePreview = signal(null); + imageError = signal(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(); diff --git a/apps/frontend/src/app/components/games/games-list.component.ts b/apps/frontend/src/app/components/games/games-list.component.ts index 7c12a1a..c5aea2c 100644 --- a/apps/frontend/src/app/components/games/games-list.component.ts +++ b/apps/frontend/src/app/components/games/games-list.component.ts @@ -85,6 +85,26 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment } from '@librar >
+
+ + + @if (newGameImagePreview()) { +
+ Box art preview + +
+ } + @if (imageError()) { + {{ imageError() }} + } +
+
@@ -150,6 +170,26 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment } from '@librar >
+
+ + + @if (editGameImagePreview()) { +
+ Box art preview + +
+ } + @if (imageError()) { + {{ imageError() }} + } +
+
@@ -531,6 +571,39 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment } from '@librar color: #6b7280; 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 #e5e7eb; + } + + .error-text { + color: #ef4444; + font-size: 0.875rem; + display: block; + margin-top: 0.25rem; + } + + input[type="file"] { + padding: 0.5rem; + border: 2px dashed #e5e7eb; + border-radius: 4px; + background: #f9fafb; + cursor: pointer; + } + + input[type="file"]:hover { + border-color: #10b981; + } `] }) export class GamesListComponent implements OnInit { @@ -550,6 +623,12 @@ export class GamesListComponent implements OnInit { expandedComments = signal>({}); newCommentContent: Record = {}; + // Image upload state + newGameImagePreview = signal(null); + editGameImagePreview = signal(null); + imageError = signal(null); + private readonly MAX_IMAGE_SIZE = 500 * 1024; // 500KB + // Expose GameStatus enum to template GameStatus = GameStatus; @@ -618,8 +697,11 @@ export class GamesListComponent implements OnInit { platform: '', status: GameStatus.backlog, rating: undefined, - notes: '' + notes: '', + coverImage: undefined }; + this.newGameImagePreview.set(null); + this.imageError.set(null); } addGame() { @@ -630,7 +712,8 @@ export class GamesListComponent implements OnInit { platform: this.newGame.platform, status: this.newGame.status, rating: this.newGame.rating, - notes: this.newGame.notes + notes: this.newGame.notes, + coverImage: this.newGame.coverImage }; this.gamesService.createGame(gameToAdd).subscribe(() => { @@ -654,14 +737,19 @@ export class GamesListComponent implements OnInit { platform: game.platform, status: game.status, rating: game.rating, - notes: game.notes + notes: game.notes, + coverImage: game.coverImage }; + this.editGameImagePreview.set(game.coverImage || null); this.showAddForm.set(false); + this.imageError.set(null); } cancelEdit() { this.editingGame.set(null); this.editGame = {}; + this.editGameImagePreview.set(null); + this.imageError.set(null); } saveEdit() { @@ -674,6 +762,52 @@ export class GamesListComponent implements OnInit { }); } + // 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.newGameImagePreview.set(base64); + this.newGame.coverImage = base64; + } else { + this.editGameImagePreview.set(base64); + this.editGame.coverImage = base64; + } + }; + reader.readAsDataURL(file); + } + + clearImage(target: 'new' | 'edit') { + if (target === 'new') { + this.newGameImagePreview.set(null); + this.newGame.coverImage = undefined; + } else { + this.editGameImagePreview.set(null); + this.editGame.coverImage = undefined; + } + this.imageError.set(null); + } + // Comments methods formatDate(date: Date | string): string { return new Date(date).toLocaleDateString(); diff --git a/apps/frontend/src/app/components/music/music-list.component.ts b/apps/frontend/src/app/components/music/music-list.component.ts index 5fefee6..4a93a14 100644 --- a/apps/frontend/src/app/components/music/music-list.component.ts +++ b/apps/frontend/src/app/components/music/music-list.component.ts @@ -95,6 +95,26 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment >
+
+ + + @if (newMusicImagePreview()) { +
+ Album art preview + +
+ } + @if (imageError()) { + {{ imageError() }} + } +
+
@@ -170,6 +190,26 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment >
+
+ + + @if (editMusicImagePreview()) { +
+ Album art preview + +
+ } + @if (imageError()) { + {{ imageError() }} + } +
+
@@ -734,6 +774,39 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment 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: 100px; + 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 MusicListComponent implements OnInit { @@ -754,6 +827,12 @@ export class MusicListComponent implements OnInit { expandedComments = signal>({}); newCommentContent: Record = {}; + // Image upload state + newMusicImagePreview = signal(null); + editMusicImagePreview = signal(null); + imageError = signal(null); + private readonly MAX_IMAGE_SIZE = 500 * 1024; // 500KB + // Expose enums to template MusicType = MusicType; MusicStatus = MusicStatus; @@ -850,8 +929,11 @@ export class MusicListComponent implements OnInit { type: MusicType.album, status: MusicStatus.wantToListen, rating: undefined, - notes: '' + notes: '', + coverArt: undefined }; + this.newMusicImagePreview.set(null); + this.imageError.set(null); } addMusic() { @@ -863,7 +945,8 @@ export class MusicListComponent implements OnInit { type: this.newMusic.type, status: this.newMusic.status, rating: this.newMusic.rating, - notes: this.newMusic.notes + notes: this.newMusic.notes, + coverArt: this.newMusic.coverArt }; this.musicService.createMusic(musicToAdd).subscribe(() => { @@ -888,14 +971,19 @@ export class MusicListComponent implements OnInit { type: music.type, status: music.status, rating: music.rating, - notes: music.notes + notes: music.notes, + coverArt: music.coverArt }; + this.editMusicImagePreview.set(music.coverArt || null); this.showAddForm.set(false); + this.imageError.set(null); } cancelEdit() { this.editingMusic.set(null); this.editMusicData = {}; + this.editMusicImagePreview.set(null); + this.imageError.set(null); } saveEdit() { @@ -912,6 +1000,52 @@ export class MusicListComponent 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.newMusicImagePreview.set(base64); + this.newMusic.coverArt = base64; + } else { + this.editMusicImagePreview.set(base64); + this.editMusicData.coverArt = base64; + } + }; + reader.readAsDataURL(file); + } + + clearImage(target: 'new' | 'edit') { + if (target === 'new') { + this.newMusicImagePreview.set(null); + this.newMusic.coverArt = undefined; + } else { + this.editMusicImagePreview.set(null); + this.editMusicData.coverArt = undefined; + } + this.imageError.set(null); + } + // Comments methods toggleComments(musicId: string) { const expanded = this.expandedComments();