generated from nhcarrigan/template
feat: support cover arts
This commit is contained in:
@@ -97,6 +97,26 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</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">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Add Book</button>
|
<button type="submit" class="btn btn-primary">Add Book</button>
|
||||||
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</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>
|
></textarea>
|
||||||
</div>
|
</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">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</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);
|
color: var(--witch-mauve);
|
||||||
font-size: 0.9rem;
|
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 {
|
export class BooksListComponent implements OnInit {
|
||||||
@@ -681,6 +754,12 @@ export class BooksListComponent implements OnInit {
|
|||||||
expandedComments = signal<Record<string, boolean>>({});
|
expandedComments = signal<Record<string, boolean>>({});
|
||||||
newCommentContent: Record<string, string> = {};
|
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
|
// Expose BookStatus enum to template
|
||||||
BookStatus = BookStatus;
|
BookStatus = BookStatus;
|
||||||
|
|
||||||
@@ -751,8 +830,11 @@ export class BooksListComponent implements OnInit {
|
|||||||
isbn: '',
|
isbn: '',
|
||||||
status: BookStatus.toRead,
|
status: BookStatus.toRead,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: ''
|
notes: '',
|
||||||
|
coverImage: undefined
|
||||||
};
|
};
|
||||||
|
this.newBookImagePreview.set(null);
|
||||||
|
this.imageError.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
addBook() {
|
addBook() {
|
||||||
@@ -764,7 +846,8 @@ export class BooksListComponent implements OnInit {
|
|||||||
isbn: this.newBook.isbn,
|
isbn: this.newBook.isbn,
|
||||||
status: this.newBook.status,
|
status: this.newBook.status,
|
||||||
rating: this.newBook.rating,
|
rating: this.newBook.rating,
|
||||||
notes: this.newBook.notes
|
notes: this.newBook.notes,
|
||||||
|
coverImage: this.newBook.coverImage
|
||||||
};
|
};
|
||||||
|
|
||||||
this.booksService.createBook(bookToAdd).subscribe(() => {
|
this.booksService.createBook(bookToAdd).subscribe(() => {
|
||||||
@@ -789,14 +872,19 @@ export class BooksListComponent implements OnInit {
|
|||||||
isbn: book.isbn,
|
isbn: book.isbn,
|
||||||
status: book.status,
|
status: book.status,
|
||||||
rating: book.rating,
|
rating: book.rating,
|
||||||
notes: book.notes
|
notes: book.notes,
|
||||||
|
coverImage: book.coverImage
|
||||||
};
|
};
|
||||||
|
this.editBookImagePreview.set(book.coverImage || null);
|
||||||
this.showAddForm.set(false);
|
this.showAddForm.set(false);
|
||||||
|
this.imageError.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelEdit() {
|
cancelEdit() {
|
||||||
this.editingBook.set(null);
|
this.editingBook.set(null);
|
||||||
this.editBook = {};
|
this.editBook = {};
|
||||||
|
this.editBookImagePreview.set(null);
|
||||||
|
this.imageError.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEdit() {
|
saveEdit() {
|
||||||
@@ -813,6 +901,52 @@ export class BooksListComponent implements OnInit {
|
|||||||
return new Date(date).toLocaleDateString();
|
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
|
// Comments methods
|
||||||
toggleComments(bookId: string) {
|
toggleComments(bookId: string) {
|
||||||
const expanded = this.expandedComments();
|
const expanded = this.expandedComments();
|
||||||
|
|||||||
@@ -85,6 +85,26 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment } from '@librar
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="coverImage">Box Art (max 500KB)</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="coverImage"
|
||||||
|
name="coverImage"
|
||||||
|
accept="image/*"
|
||||||
|
(change)="onImageSelected($event, 'new')"
|
||||||
|
>
|
||||||
|
@if (newGameImagePreview()) {
|
||||||
|
<div class="image-preview">
|
||||||
|
<img [src]="newGameImagePreview()" alt="Box art 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">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Add Game</button>
|
<button type="submit" class="btn btn-primary">Add Game</button>
|
||||||
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -150,6 +170,26 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment } from '@librar
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-coverImage">Box Art (max 500KB)</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="edit-coverImage"
|
||||||
|
name="coverImage"
|
||||||
|
accept="image/*"
|
||||||
|
(change)="onImageSelected($event, 'edit')"
|
||||||
|
>
|
||||||
|
@if (editGameImagePreview()) {
|
||||||
|
<div class="image-preview">
|
||||||
|
<img [src]="editGameImagePreview()" alt="Box art 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">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -531,6 +571,39 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment } from '@librar
|
|||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
font-size: 0.9rem;
|
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 {
|
export class GamesListComponent implements OnInit {
|
||||||
@@ -550,6 +623,12 @@ export class GamesListComponent implements OnInit {
|
|||||||
expandedComments = signal<Record<string, boolean>>({});
|
expandedComments = signal<Record<string, boolean>>({});
|
||||||
newCommentContent: Record<string, string> = {};
|
newCommentContent: Record<string, string> = {};
|
||||||
|
|
||||||
|
// Image upload state
|
||||||
|
newGameImagePreview = signal<string | null>(null);
|
||||||
|
editGameImagePreview = signal<string | null>(null);
|
||||||
|
imageError = signal<string | null>(null);
|
||||||
|
private readonly MAX_IMAGE_SIZE = 500 * 1024; // 500KB
|
||||||
|
|
||||||
// Expose GameStatus enum to template
|
// Expose GameStatus enum to template
|
||||||
GameStatus = GameStatus;
|
GameStatus = GameStatus;
|
||||||
|
|
||||||
@@ -618,8 +697,11 @@ export class GamesListComponent implements OnInit {
|
|||||||
platform: '',
|
platform: '',
|
||||||
status: GameStatus.backlog,
|
status: GameStatus.backlog,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: ''
|
notes: '',
|
||||||
|
coverImage: undefined
|
||||||
};
|
};
|
||||||
|
this.newGameImagePreview.set(null);
|
||||||
|
this.imageError.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
addGame() {
|
addGame() {
|
||||||
@@ -630,7 +712,8 @@ export class GamesListComponent implements OnInit {
|
|||||||
platform: this.newGame.platform,
|
platform: this.newGame.platform,
|
||||||
status: this.newGame.status,
|
status: this.newGame.status,
|
||||||
rating: this.newGame.rating,
|
rating: this.newGame.rating,
|
||||||
notes: this.newGame.notes
|
notes: this.newGame.notes,
|
||||||
|
coverImage: this.newGame.coverImage
|
||||||
};
|
};
|
||||||
|
|
||||||
this.gamesService.createGame(gameToAdd).subscribe(() => {
|
this.gamesService.createGame(gameToAdd).subscribe(() => {
|
||||||
@@ -654,14 +737,19 @@ export class GamesListComponent implements OnInit {
|
|||||||
platform: game.platform,
|
platform: game.platform,
|
||||||
status: game.status,
|
status: game.status,
|
||||||
rating: game.rating,
|
rating: game.rating,
|
||||||
notes: game.notes
|
notes: game.notes,
|
||||||
|
coverImage: game.coverImage
|
||||||
};
|
};
|
||||||
|
this.editGameImagePreview.set(game.coverImage || null);
|
||||||
this.showAddForm.set(false);
|
this.showAddForm.set(false);
|
||||||
|
this.imageError.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelEdit() {
|
cancelEdit() {
|
||||||
this.editingGame.set(null);
|
this.editingGame.set(null);
|
||||||
this.editGame = {};
|
this.editGame = {};
|
||||||
|
this.editGameImagePreview.set(null);
|
||||||
|
this.imageError.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEdit() {
|
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
|
// Comments methods
|
||||||
formatDate(date: Date | string): string {
|
formatDate(date: Date | string): string {
|
||||||
return new Date(date).toLocaleDateString();
|
return new Date(date).toLocaleDateString();
|
||||||
|
|||||||
@@ -95,6 +95,26 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="coverArt">Album Art (max 500KB)</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="coverArt"
|
||||||
|
name="coverArt"
|
||||||
|
accept="image/*"
|
||||||
|
(change)="onImageSelected($event, 'new')"
|
||||||
|
>
|
||||||
|
@if (newMusicImagePreview()) {
|
||||||
|
<div class="image-preview">
|
||||||
|
<img [src]="newMusicImagePreview()" alt="Album art 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">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Add Music</button>
|
<button type="submit" class="btn btn-primary">Add Music</button>
|
||||||
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -170,6 +190,26 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-coverArt">Album Art (max 500KB)</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="edit-coverArt"
|
||||||
|
name="coverArt"
|
||||||
|
accept="image/*"
|
||||||
|
(change)="onImageSelected($event, 'edit')"
|
||||||
|
>
|
||||||
|
@if (editMusicImagePreview()) {
|
||||||
|
<div class="image-preview">
|
||||||
|
<img [src]="editMusicImagePreview()" alt="Album art 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">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
||||||
@@ -734,6 +774,39 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment
|
|||||||
color: var(--witch-mauve);
|
color: var(--witch-mauve);
|
||||||
font-size: 0.9rem;
|
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 {
|
export class MusicListComponent implements OnInit {
|
||||||
@@ -754,6 +827,12 @@ export class MusicListComponent implements OnInit {
|
|||||||
expandedComments = signal<Record<string, boolean>>({});
|
expandedComments = signal<Record<string, boolean>>({});
|
||||||
newCommentContent: Record<string, string> = {};
|
newCommentContent: Record<string, string> = {};
|
||||||
|
|
||||||
|
// Image upload state
|
||||||
|
newMusicImagePreview = signal<string | null>(null);
|
||||||
|
editMusicImagePreview = signal<string | null>(null);
|
||||||
|
imageError = signal<string | null>(null);
|
||||||
|
private readonly MAX_IMAGE_SIZE = 500 * 1024; // 500KB
|
||||||
|
|
||||||
// Expose enums to template
|
// Expose enums to template
|
||||||
MusicType = MusicType;
|
MusicType = MusicType;
|
||||||
MusicStatus = MusicStatus;
|
MusicStatus = MusicStatus;
|
||||||
@@ -850,8 +929,11 @@ export class MusicListComponent implements OnInit {
|
|||||||
type: MusicType.album,
|
type: MusicType.album,
|
||||||
status: MusicStatus.wantToListen,
|
status: MusicStatus.wantToListen,
|
||||||
rating: undefined,
|
rating: undefined,
|
||||||
notes: ''
|
notes: '',
|
||||||
|
coverArt: undefined
|
||||||
};
|
};
|
||||||
|
this.newMusicImagePreview.set(null);
|
||||||
|
this.imageError.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
addMusic() {
|
addMusic() {
|
||||||
@@ -863,7 +945,8 @@ export class MusicListComponent implements OnInit {
|
|||||||
type: this.newMusic.type,
|
type: this.newMusic.type,
|
||||||
status: this.newMusic.status,
|
status: this.newMusic.status,
|
||||||
rating: this.newMusic.rating,
|
rating: this.newMusic.rating,
|
||||||
notes: this.newMusic.notes
|
notes: this.newMusic.notes,
|
||||||
|
coverArt: this.newMusic.coverArt
|
||||||
};
|
};
|
||||||
|
|
||||||
this.musicService.createMusic(musicToAdd).subscribe(() => {
|
this.musicService.createMusic(musicToAdd).subscribe(() => {
|
||||||
@@ -888,14 +971,19 @@ export class MusicListComponent implements OnInit {
|
|||||||
type: music.type,
|
type: music.type,
|
||||||
status: music.status,
|
status: music.status,
|
||||||
rating: music.rating,
|
rating: music.rating,
|
||||||
notes: music.notes
|
notes: music.notes,
|
||||||
|
coverArt: music.coverArt
|
||||||
};
|
};
|
||||||
|
this.editMusicImagePreview.set(music.coverArt || null);
|
||||||
this.showAddForm.set(false);
|
this.showAddForm.set(false);
|
||||||
|
this.imageError.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelEdit() {
|
cancelEdit() {
|
||||||
this.editingMusic.set(null);
|
this.editingMusic.set(null);
|
||||||
this.editMusicData = {};
|
this.editMusicData = {};
|
||||||
|
this.editMusicImagePreview.set(null);
|
||||||
|
this.imageError.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveEdit() {
|
saveEdit() {
|
||||||
@@ -912,6 +1000,52 @@ export class MusicListComponent implements OnInit {
|
|||||||
return new Date(date).toLocaleDateString();
|
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
|
// Comments methods
|
||||||
toggleComments(musicId: string) {
|
toggleComments(musicId: string) {
|
||||||
const expanded = this.expandedComments();
|
const expanded = this.expandedComments();
|
||||||
|
|||||||
Reference in New Issue
Block a user