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
>
+
+
@@ -174,6 +194,26 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar
>
+
+
@@ -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
>
+
+
@@ -150,6 +170,26 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment } from '@librar
>
+
+
@@ -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
>
+
+
@@ -170,6 +190,26 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment
>
+
+
@@ -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();