generated from nhcarrigan/template
feat: bunch of work done here, got comments and edit and delete
This commit is contained in:
@@ -4,12 +4,13 @@
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
||||
import { Component, OnInit, inject, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BooksService } from '../../services/books.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
|
||||
import { CommentsService } from '../../services/comments.service';
|
||||
import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@library/shared-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-books-list',
|
||||
@@ -67,9 +68,9 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
|
||||
<div class="form-group">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" [(ngModel)]="newBook.status" name="status" required>
|
||||
<option value="reading">Currently Reading</option>
|
||||
<option value="finished">Finished</option>
|
||||
<option value="toRead">To Read</option>
|
||||
<option [value]="BookStatus.reading">Currently Reading</option>
|
||||
<option [value]="BookStatus.finished">Finished</option>
|
||||
<option [value]="BookStatus.toRead">To Read</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -103,6 +104,83 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (editingBook() && authService.isAdmin()) {
|
||||
<form (ngSubmit)="saveEdit()" class="add-form">
|
||||
<h3>Edit Book</h3>
|
||||
<div class="form-group">
|
||||
<label for="edit-title">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-title"
|
||||
[(ngModel)]="editBook.title"
|
||||
name="title"
|
||||
required
|
||||
placeholder="Enter book title"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-author">Author</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-author"
|
||||
[(ngModel)]="editBook.author"
|
||||
name="author"
|
||||
required
|
||||
placeholder="Enter author name"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-isbn">ISBN</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-isbn"
|
||||
[(ngModel)]="editBook.isbn"
|
||||
name="isbn"
|
||||
placeholder="ISBN (optional)"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-status">Status</label>
|
||||
<select id="edit-status" [(ngModel)]="editBook.status" name="status" required>
|
||||
<option [value]="BookStatus.reading">Currently Reading</option>
|
||||
<option [value]="BookStatus.finished">Finished</option>
|
||||
<option [value]="BookStatus.toRead">To Read</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-rating">Rating (0-5)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="edit-rating"
|
||||
[(ngModel)]="editBook.rating"
|
||||
name="rating"
|
||||
min="0"
|
||||
max="5"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-notes">Notes</label>
|
||||
<textarea
|
||||
id="edit-notes"
|
||||
[(ngModel)]="editBook.notes"
|
||||
name="notes"
|
||||
rows="3"
|
||||
placeholder="Your thoughts about the book..."
|
||||
></textarea>
|
||||
</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>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
<div class="filters">
|
||||
<button
|
||||
(click)="setFilter('all')"
|
||||
@@ -116,21 +194,21 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
|
||||
[class.active]="statusFilter() === BookStatus.reading"
|
||||
class="filter-btn"
|
||||
>
|
||||
Reading ({{ getCountByStatus(BookStatus.reading) }})
|
||||
Reading ({{ readingCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setFilter(BookStatus.finished)"
|
||||
[class.active]="statusFilter() === BookStatus.finished"
|
||||
class="filter-btn"
|
||||
>
|
||||
Finished ({{ getCountByStatus(BookStatus.finished) }})
|
||||
Finished ({{ finishedCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setFilter(BookStatus.toRead)"
|
||||
[class.active]="statusFilter() === BookStatus.toRead"
|
||||
class="filter-btn"
|
||||
>
|
||||
To Read ({{ getCountByStatus(BookStatus.toRead) }})
|
||||
To Read ({{ toReadCount() }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -182,11 +260,58 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
|
||||
|
||||
@if (authService.isAdmin()) {
|
||||
<div class="actions">
|
||||
<button (click)="startEdit(book)" class="btn btn-secondary btn-sm">
|
||||
Edit
|
||||
</button>
|
||||
<button (click)="deleteBook(book)" class="btn btn-danger btn-sm">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="comments-section">
|
||||
<button (click)="toggleComments(book.id)" class="btn btn-secondary btn-sm comments-toggle">
|
||||
{{ expandedComments()[book.id] ? 'Hide' : 'Show' }} Comments{{ comments()[book.id] ? ' (' + getCommentCount(book.id) + ')' : '' }}
|
||||
</button>
|
||||
|
||||
@if (expandedComments()[book.id]) {
|
||||
<div class="comments-container">
|
||||
@if (authService.isAuthenticated()) {
|
||||
<form (ngSubmit)="addComment(book.id)" class="comment-form">
|
||||
<textarea
|
||||
[(ngModel)]="newCommentContent[book.id]"
|
||||
name="comment"
|
||||
placeholder="Add a comment (Markdown supported)..."
|
||||
rows="2"
|
||||
></textarea>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Post Comment</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (commentsLoading()[book.id]) {
|
||||
<div class="comments-loading">Loading comments...</div>
|
||||
} @else {
|
||||
@for (comment of comments()[book.id] || []; track comment.id) {
|
||||
<div class="comment">
|
||||
<div class="comment-header">
|
||||
@if (comment.user.avatar) {
|
||||
<img [src]="comment.user.avatar" [alt]="comment.user.username" class="comment-avatar">
|
||||
}
|
||||
<span class="comment-author">{{ comment.user.username }}</span>
|
||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||
@if (authService.isAdmin()) {
|
||||
<button (click)="deleteComment(book.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||
}
|
||||
</div>
|
||||
<div class="comment-content" [innerHTML]="comment.content"></div>
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -452,20 +577,126 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
|
||||
}
|
||||
|
||||
.btn-sm { padding: 0.25rem 0.75rem; font-size: 0.85rem; }
|
||||
.btn-xs { padding: 0.15rem 0.5rem; font-size: 0.75rem; }
|
||||
|
||||
.comments-section {
|
||||
margin-top: 1rem;
|
||||
border-top: 1px solid var(--witch-lavender);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.comments-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.comments-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.comment-form {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.comment-form textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 2px solid var(--witch-lavender);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
background-color: var(--witch-moon);
|
||||
color: var(--witch-purple);
|
||||
resize: vertical;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--witch-rose);
|
||||
}
|
||||
|
||||
.comment {
|
||||
background: var(--witch-moon);
|
||||
border: 1px solid var(--witch-lavender);
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: 500;
|
||||
color: var(--witch-plum);
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
font-size: 0.75rem;
|
||||
color: var(--witch-mauve);
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
font-size: 0.9rem;
|
||||
color: var(--witch-purple);
|
||||
}
|
||||
|
||||
.comment-content :deep(p) {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.comments-loading,
|
||||
.no-comments {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
color: var(--witch-mauve);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class BooksListComponent implements OnInit {
|
||||
booksService = inject(BooksService);
|
||||
authService = inject(AuthService);
|
||||
commentsService = inject(CommentsService);
|
||||
|
||||
books = signal<Book[]>([]);
|
||||
loading = signal(true);
|
||||
showAddForm = signal(false);
|
||||
editingBook = signal<Book | null>(null);
|
||||
statusFilter = signal<'all' | BookStatus>('all');
|
||||
|
||||
// Comments state
|
||||
comments = signal<Record<string, Comment[]>>({});
|
||||
commentsLoading = signal<Record<string, boolean>>({});
|
||||
expandedComments = signal<Record<string, boolean>>({});
|
||||
newCommentContent: Record<string, string> = {};
|
||||
|
||||
// Expose BookStatus enum to template
|
||||
BookStatus = BookStatus;
|
||||
|
||||
// Computed signals for reactive count updates
|
||||
readingCount = computed(() => this.books().filter(book => book.status === BookStatus.reading).length);
|
||||
finishedCount = computed(() => this.books().filter(book => book.status === BookStatus.finished).length);
|
||||
toReadCount = computed(() => this.books().filter(book => book.status === BookStatus.toRead).length);
|
||||
|
||||
filteredBooks = computed(() => {
|
||||
const filter = this.statusFilter();
|
||||
if (filter === 'all') {
|
||||
return this.books();
|
||||
}
|
||||
return this.books().filter(book => book.status === filter);
|
||||
});
|
||||
|
||||
newBook: Partial<CreateBookDto> = {
|
||||
title: '',
|
||||
author: '',
|
||||
@@ -475,6 +706,8 @@ export class BooksListComponent implements OnInit {
|
||||
notes: ''
|
||||
};
|
||||
|
||||
editBook: Partial<UpdateBookDto> = {};
|
||||
|
||||
ngOnInit() {
|
||||
this.loadBooks();
|
||||
}
|
||||
@@ -492,18 +725,6 @@ export class BooksListComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
filteredBooks() {
|
||||
const filter = this.statusFilter();
|
||||
if (filter === 'all') {
|
||||
return this.books();
|
||||
}
|
||||
return this.books().filter(book => book.status === filter);
|
||||
}
|
||||
|
||||
getCountByStatus(status: BookStatus): number {
|
||||
return this.books().filter(book => book.status === status).length;
|
||||
}
|
||||
|
||||
setFilter(filter: 'all' | BookStatus) {
|
||||
this.statusFilter.set(filter);
|
||||
}
|
||||
@@ -560,7 +781,108 @@ export class BooksListComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
startEdit(book: Book) {
|
||||
this.editingBook.set(book);
|
||||
this.editBook = {
|
||||
title: book.title,
|
||||
author: book.author,
|
||||
isbn: book.isbn,
|
||||
status: book.status,
|
||||
rating: book.rating,
|
||||
notes: book.notes
|
||||
};
|
||||
this.showAddForm.set(false);
|
||||
}
|
||||
|
||||
cancelEdit() {
|
||||
this.editingBook.set(null);
|
||||
this.editBook = {};
|
||||
}
|
||||
|
||||
saveEdit() {
|
||||
const book = this.editingBook();
|
||||
if (!book || !this.editBook.title || !this.editBook.author || !this.editBook.status) return;
|
||||
|
||||
this.booksService.updateBook(book.id, this.editBook).subscribe(() => {
|
||||
this.loadBooks();
|
||||
this.cancelEdit();
|
||||
});
|
||||
}
|
||||
|
||||
formatDate(date: Date | string): string {
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
|
||||
// Comments methods
|
||||
toggleComments(bookId: string) {
|
||||
const expanded = this.expandedComments();
|
||||
const isCurrentlyExpanded = expanded[bookId];
|
||||
|
||||
this.expandedComments.set({
|
||||
...expanded,
|
||||
[bookId]: !isCurrentlyExpanded
|
||||
});
|
||||
|
||||
if (!isCurrentlyExpanded && !this.comments()[bookId]) {
|
||||
this.loadComments(bookId);
|
||||
}
|
||||
}
|
||||
|
||||
loadComments(bookId: string) {
|
||||
this.commentsLoading.set({
|
||||
...this.commentsLoading(),
|
||||
[bookId]: true
|
||||
});
|
||||
|
||||
this.commentsService.getCommentsForBook(bookId).subscribe({
|
||||
next: (comments) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[bookId]: comments
|
||||
});
|
||||
this.commentsLoading.set({
|
||||
...this.commentsLoading(),
|
||||
[bookId]: false
|
||||
});
|
||||
},
|
||||
error: () => {
|
||||
this.commentsLoading.set({
|
||||
...this.commentsLoading(),
|
||||
[bookId]: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCommentCount(bookId: string): number {
|
||||
return this.comments()[bookId]?.length || 0;
|
||||
}
|
||||
|
||||
addComment(bookId: string) {
|
||||
const content = this.newCommentContent[bookId];
|
||||
if (!content?.trim()) return;
|
||||
|
||||
this.commentsService.addCommentToBook(bookId, { content }).subscribe({
|
||||
next: (comment) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[bookId]: [comment, ...(this.comments()[bookId] || [])]
|
||||
});
|
||||
this.newCommentContent[bookId] = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteComment(bookId: string, commentId: string) {
|
||||
if (!confirm('Are you sure you want to delete this comment?')) return;
|
||||
|
||||
this.commentsService.deleteCommentFromBook(bookId, commentId).subscribe({
|
||||
next: () => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[bookId]: (this.comments()[bookId] || []).filter(c => c.id !== commentId)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,13 @@
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
||||
import { Component, OnInit, inject, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { GamesService } from '../../services/games.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
||||
import { CommentsService } from '../../services/comments.service';
|
||||
import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment } from '@library/shared-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-games-list',
|
||||
@@ -55,9 +56,9 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
||||
<div class="form-group">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" [(ngModel)]="newGame.status" name="status" required>
|
||||
<option value="playing">Currently Playing</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="backlog">In Backlog</option>
|
||||
<option [value]="GameStatus.playing">Currently Playing</option>
|
||||
<option [value]="GameStatus.completed">Completed</option>
|
||||
<option [value]="GameStatus.backlog">In Backlog</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -91,6 +92,71 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (editingGame() && authService.isAdmin()) {
|
||||
<form (ngSubmit)="saveEdit()" class="add-form">
|
||||
<h3>Edit Game</h3>
|
||||
<div class="form-group">
|
||||
<label for="edit-title">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-title"
|
||||
[(ngModel)]="editGame.title"
|
||||
name="title"
|
||||
required
|
||||
placeholder="Enter game title"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-platform">Platform</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-platform"
|
||||
[(ngModel)]="editGame.platform"
|
||||
name="platform"
|
||||
placeholder="PC, PS5, Xbox, Switch, etc."
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-status">Status</label>
|
||||
<select id="edit-status" [(ngModel)]="editGame.status" name="status" required>
|
||||
<option [value]="GameStatus.playing">Currently Playing</option>
|
||||
<option [value]="GameStatus.completed">Completed</option>
|
||||
<option [value]="GameStatus.backlog">In Backlog</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-rating">Rating (0-10)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="edit-rating"
|
||||
[(ngModel)]="editGame.rating"
|
||||
name="rating"
|
||||
min="0"
|
||||
max="10"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-notes">Notes</label>
|
||||
<textarea
|
||||
id="edit-notes"
|
||||
[(ngModel)]="editGame.notes"
|
||||
name="notes"
|
||||
rows="3"
|
||||
placeholder="Any thoughts about the game..."
|
||||
></textarea>
|
||||
</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>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
<div class="filters">
|
||||
<button
|
||||
(click)="setFilter('all')"
|
||||
@@ -104,21 +170,21 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
||||
[class.active]="statusFilter() === GameStatus.playing"
|
||||
class="filter-btn"
|
||||
>
|
||||
Playing ({{ getCountByStatus(GameStatus.playing) }})
|
||||
Playing ({{ playingCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setFilter(GameStatus.completed)"
|
||||
[class.active]="statusFilter() === GameStatus.completed"
|
||||
class="filter-btn"
|
||||
>
|
||||
Completed ({{ getCountByStatus(GameStatus.completed) }})
|
||||
Completed ({{ completedCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setFilter(GameStatus.backlog)"
|
||||
[class.active]="statusFilter() === GameStatus.backlog"
|
||||
class="filter-btn"
|
||||
>
|
||||
Backlog ({{ getCountByStatus(GameStatus.backlog) }})
|
||||
Backlog ({{ backlogCount() }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -157,11 +223,58 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
||||
|
||||
@if (authService.isAdmin()) {
|
||||
<div class="actions">
|
||||
<button (click)="startEdit(game)" class="btn btn-secondary btn-sm">
|
||||
Edit
|
||||
</button>
|
||||
<button (click)="deleteGame(game)" class="btn btn-danger btn-sm">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="comments-section">
|
||||
<button (click)="toggleComments(game.id)" class="btn btn-secondary btn-sm comments-toggle">
|
||||
{{ expandedComments()[game.id] ? 'Hide' : 'Show' }} Comments{{ comments()[game.id] ? ' (' + getCommentCount(game.id) + ')' : '' }}
|
||||
</button>
|
||||
|
||||
@if (expandedComments()[game.id]) {
|
||||
<div class="comments-container">
|
||||
@if (authService.isAuthenticated()) {
|
||||
<form (ngSubmit)="addComment(game.id)" class="comment-form">
|
||||
<textarea
|
||||
[(ngModel)]="newCommentContent[game.id]"
|
||||
name="comment"
|
||||
placeholder="Add a comment (Markdown supported)..."
|
||||
rows="2"
|
||||
></textarea>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Post Comment</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (commentsLoading()[game.id]) {
|
||||
<div class="comments-loading">Loading comments...</div>
|
||||
} @else {
|
||||
@for (comment of comments()[game.id] || []; track comment.id) {
|
||||
<div class="comment">
|
||||
<div class="comment-header">
|
||||
@if (comment.user.avatar) {
|
||||
<img [src]="comment.user.avatar" [alt]="comment.user.username" class="comment-avatar">
|
||||
}
|
||||
<span class="comment-author">{{ comment.user.username }}</span>
|
||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||
@if (authService.isAdmin()) {
|
||||
<button (click)="deleteComment(game.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||
}
|
||||
</div>
|
||||
<div class="comment-content" [innerHTML]="comment.content"></div>
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -344,20 +457,115 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
||||
.btn-secondary { background: #6b7280; color: white; }
|
||||
.btn-danger { background: #ef4444; color: white; }
|
||||
.btn-sm { padding: 0.25rem 0.75rem; font-size: 0.85rem; }
|
||||
.btn-xs { padding: 0.15rem 0.5rem; font-size: 0.75rem; }
|
||||
|
||||
.comments-section {
|
||||
margin-top: 1rem;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.comments-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.comments-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.comment-form {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.comment-form textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
resize: vertical;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.comment {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
font-size: 0.9rem;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.comments-loading,
|
||||
.no-comments {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
color: #6b7280;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class GamesListComponent implements OnInit {
|
||||
gamesService = inject(GamesService);
|
||||
authService = inject(AuthService);
|
||||
commentsService = inject(CommentsService);
|
||||
|
||||
games = signal<Game[]>([]);
|
||||
loading = signal(true);
|
||||
showAddForm = signal(false);
|
||||
editingGame = signal<Game | null>(null);
|
||||
statusFilter = signal<'all' | GameStatus>('all');
|
||||
|
||||
// Comments state
|
||||
comments = signal<Record<string, Comment[]>>({});
|
||||
commentsLoading = signal<Record<string, boolean>>({});
|
||||
expandedComments = signal<Record<string, boolean>>({});
|
||||
newCommentContent: Record<string, string> = {};
|
||||
|
||||
// Expose GameStatus enum to template
|
||||
GameStatus = GameStatus;
|
||||
|
||||
// Computed signals for reactive count updates
|
||||
playingCount = computed(() => this.games().filter(game => game.status === GameStatus.playing).length);
|
||||
completedCount = computed(() => this.games().filter(game => game.status === GameStatus.completed).length);
|
||||
backlogCount = computed(() => this.games().filter(game => game.status === GameStatus.backlog).length);
|
||||
|
||||
filteredGames = computed(() => {
|
||||
const filter = this.statusFilter();
|
||||
if (filter === 'all') {
|
||||
return this.games();
|
||||
}
|
||||
return this.games().filter(game => game.status === filter);
|
||||
});
|
||||
|
||||
newGame: Partial<CreateGameDto> = {
|
||||
title: '',
|
||||
platform: '',
|
||||
@@ -366,6 +574,8 @@ export class GamesListComponent implements OnInit {
|
||||
notes: ''
|
||||
};
|
||||
|
||||
editGame: Partial<UpdateGameDto> = {};
|
||||
|
||||
ngOnInit() {
|
||||
this.loadGames();
|
||||
}
|
||||
@@ -383,18 +593,6 @@ export class GamesListComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
filteredGames() {
|
||||
const filter = this.statusFilter();
|
||||
if (filter === 'all') {
|
||||
return this.games();
|
||||
}
|
||||
return this.games().filter(game => game.status === filter);
|
||||
}
|
||||
|
||||
getCountByStatus(status: GameStatus): number {
|
||||
return this.games().filter(game => game.status === status).length;
|
||||
}
|
||||
|
||||
setFilter(filter: 'all' | GameStatus) {
|
||||
this.statusFilter.set(filter);
|
||||
}
|
||||
@@ -448,4 +646,108 @@ export class GamesListComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
startEdit(game: Game) {
|
||||
this.editingGame.set(game);
|
||||
this.editGame = {
|
||||
title: game.title,
|
||||
platform: game.platform,
|
||||
status: game.status,
|
||||
rating: game.rating,
|
||||
notes: game.notes
|
||||
};
|
||||
this.showAddForm.set(false);
|
||||
}
|
||||
|
||||
cancelEdit() {
|
||||
this.editingGame.set(null);
|
||||
this.editGame = {};
|
||||
}
|
||||
|
||||
saveEdit() {
|
||||
const game = this.editingGame();
|
||||
if (!game || !this.editGame.title || !this.editGame.status) return;
|
||||
|
||||
this.gamesService.updateGame(game.id, this.editGame).subscribe(() => {
|
||||
this.loadGames();
|
||||
this.cancelEdit();
|
||||
});
|
||||
}
|
||||
|
||||
// Comments methods
|
||||
formatDate(date: Date | string): string {
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
|
||||
toggleComments(gameId: string) {
|
||||
const expanded = this.expandedComments();
|
||||
const isCurrentlyExpanded = expanded[gameId];
|
||||
|
||||
this.expandedComments.set({
|
||||
...expanded,
|
||||
[gameId]: !isCurrentlyExpanded
|
||||
});
|
||||
|
||||
if (!isCurrentlyExpanded && !this.comments()[gameId]) {
|
||||
this.loadComments(gameId);
|
||||
}
|
||||
}
|
||||
|
||||
loadComments(gameId: string) {
|
||||
this.commentsLoading.set({
|
||||
...this.commentsLoading(),
|
||||
[gameId]: true
|
||||
});
|
||||
|
||||
this.commentsService.getCommentsForGame(gameId).subscribe({
|
||||
next: (comments) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[gameId]: comments
|
||||
});
|
||||
this.commentsLoading.set({
|
||||
...this.commentsLoading(),
|
||||
[gameId]: false
|
||||
});
|
||||
},
|
||||
error: () => {
|
||||
this.commentsLoading.set({
|
||||
...this.commentsLoading(),
|
||||
[gameId]: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCommentCount(gameId: string): number {
|
||||
return this.comments()[gameId]?.length || 0;
|
||||
}
|
||||
|
||||
addComment(gameId: string) {
|
||||
const content = this.newCommentContent[gameId];
|
||||
if (!content?.trim()) return;
|
||||
|
||||
this.commentsService.addCommentToGame(gameId, { content }).subscribe({
|
||||
next: (comment) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[gameId]: [comment, ...(this.comments()[gameId] || [])]
|
||||
});
|
||||
this.newCommentContent[gameId] = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteComment(gameId: string, commentId: string) {
|
||||
if (!confirm('Are you sure you want to delete this comment?')) return;
|
||||
|
||||
this.commentsService.deleteCommentFromGame(gameId, commentId).subscribe({
|
||||
next: () => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[gameId]: (this.comments()[gameId] || []).filter(c => c.id !== commentId)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,13 @@
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
||||
import { Component, OnInit, inject, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MusicService } from '../../services/music.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-types';
|
||||
import { CommentsService } from '../../services/comments.service';
|
||||
import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment } from '@library/shared-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-music-list',
|
||||
@@ -56,18 +57,18 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
||||
<div class="form-group">
|
||||
<label for="type">Type</label>
|
||||
<select id="type" [(ngModel)]="newMusic.type" name="type" required>
|
||||
<option value="album">Album</option>
|
||||
<option value="single">Single</option>
|
||||
<option value="ep">EP</option>
|
||||
<option [value]="MusicType.album">Album</option>
|
||||
<option [value]="MusicType.single">Single</option>
|
||||
<option [value]="MusicType.ep">EP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" [(ngModel)]="newMusic.status" name="status" required>
|
||||
<option value="listening">Currently Listening</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="wantToListen">Want to Listen</option>
|
||||
<option [value]="MusicStatus.listening">Currently Listening</option>
|
||||
<option [value]="MusicStatus.completed">Completed</option>
|
||||
<option [value]="MusicStatus.wantToListen">Want to Listen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -101,6 +102,81 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (editingMusic() && authService.isAdmin()) {
|
||||
<form (ngSubmit)="saveEdit()" class="add-form">
|
||||
<h3>Edit Music</h3>
|
||||
<div class="form-group">
|
||||
<label for="edit-title">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-title"
|
||||
[(ngModel)]="editMusicData.title"
|
||||
name="title"
|
||||
required
|
||||
placeholder="Album/Single/EP title"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-artist">Artist</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-artist"
|
||||
[(ngModel)]="editMusicData.artist"
|
||||
name="artist"
|
||||
required
|
||||
placeholder="Artist name"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-type">Type</label>
|
||||
<select id="edit-type" [(ngModel)]="editMusicData.type" name="type" required>
|
||||
<option [value]="MusicType.album">Album</option>
|
||||
<option [value]="MusicType.single">Single</option>
|
||||
<option [value]="MusicType.ep">EP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-status">Status</label>
|
||||
<select id="edit-status" [(ngModel)]="editMusicData.status" name="status" required>
|
||||
<option [value]="MusicStatus.listening">Currently Listening</option>
|
||||
<option [value]="MusicStatus.completed">Completed</option>
|
||||
<option [value]="MusicStatus.wantToListen">Want to Listen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-rating">Rating (0-5)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="edit-rating"
|
||||
[(ngModel)]="editMusicData.rating"
|
||||
name="rating"
|
||||
min="0"
|
||||
max="5"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-notes">Notes</label>
|
||||
<textarea
|
||||
id="edit-notes"
|
||||
[(ngModel)]="editMusicData.notes"
|
||||
name="notes"
|
||||
rows="3"
|
||||
placeholder="Your thoughts about this music..."
|
||||
></textarea>
|
||||
</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>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
<div class="filters">
|
||||
<div class="filter-group">
|
||||
<strong>Type:</strong>
|
||||
@@ -116,21 +192,21 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
||||
[class.active]="typeFilter() === MusicType.album"
|
||||
class="filter-btn"
|
||||
>
|
||||
Albums ({{ getCountByType(MusicType.album) }})
|
||||
Albums ({{ albumCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setTypeFilter(MusicType.single)"
|
||||
[class.active]="typeFilter() === MusicType.single"
|
||||
class="filter-btn"
|
||||
>
|
||||
Singles ({{ getCountByType(MusicType.single) }})
|
||||
Singles ({{ singleCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setTypeFilter(MusicType.ep)"
|
||||
[class.active]="typeFilter() === MusicType.ep"
|
||||
class="filter-btn"
|
||||
>
|
||||
EPs ({{ getCountByType(MusicType.ep) }})
|
||||
EPs ({{ epCount() }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -148,21 +224,21 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
||||
[class.active]="statusFilter() === MusicStatus.listening"
|
||||
class="filter-btn"
|
||||
>
|
||||
Listening ({{ getCountByStatus(MusicStatus.listening) }})
|
||||
Listening ({{ listeningCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setStatusFilter(MusicStatus.completed)"
|
||||
[class.active]="statusFilter() === MusicStatus.completed"
|
||||
class="filter-btn"
|
||||
>
|
||||
Completed ({{ getCountByStatus(MusicStatus.completed) }})
|
||||
Completed ({{ completedCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setStatusFilter(MusicStatus.wantToListen)"
|
||||
[class.active]="statusFilter() === MusicStatus.wantToListen"
|
||||
class="filter-btn"
|
||||
>
|
||||
Want to Listen ({{ getCountByStatus(MusicStatus.wantToListen) }})
|
||||
Want to Listen ({{ wantToListenCount() }})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,11 +298,58 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
||||
|
||||
@if (authService.isAdmin()) {
|
||||
<div class="actions">
|
||||
<button (click)="startEdit(music)" class="btn btn-secondary btn-sm">
|
||||
Edit
|
||||
</button>
|
||||
<button (click)="deleteMusic(music)" class="btn btn-danger btn-sm">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="comments-section">
|
||||
<button (click)="toggleComments(music.id)" class="btn btn-secondary btn-sm comments-toggle">
|
||||
{{ expandedComments()[music.id] ? 'Hide' : 'Show' }} Comments{{ comments()[music.id] ? ' (' + getCommentCount(music.id) + ')' : '' }}
|
||||
</button>
|
||||
|
||||
@if (expandedComments()[music.id]) {
|
||||
<div class="comments-container">
|
||||
@if (authService.isAuthenticated()) {
|
||||
<form (ngSubmit)="addComment(music.id)" class="comment-form">
|
||||
<textarea
|
||||
[(ngModel)]="newCommentContent[music.id]"
|
||||
name="comment"
|
||||
placeholder="Add a comment (Markdown supported)..."
|
||||
rows="2"
|
||||
></textarea>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Post Comment</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (commentsLoading()[music.id]) {
|
||||
<div class="comments-loading">Loading comments...</div>
|
||||
} @else {
|
||||
@for (comment of comments()[music.id] || []; track comment.id) {
|
||||
<div class="comment">
|
||||
<div class="comment-header">
|
||||
@if (comment.user.avatar) {
|
||||
<img [src]="comment.user.avatar" [alt]="comment.user.username" class="comment-avatar">
|
||||
}
|
||||
<span class="comment-author">{{ comment.user.username }}</span>
|
||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||
@if (authService.isAdmin()) {
|
||||
<button (click)="deleteComment(music.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||
}
|
||||
</div>
|
||||
<div class="comment-content" [innerHTML]="comment.content"></div>
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -529,22 +652,138 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-xs { padding: 0.15rem 0.5rem; font-size: 0.75rem; }
|
||||
|
||||
.comments-section {
|
||||
margin-top: 1rem;
|
||||
border-top: 1px solid var(--witch-lavender);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.comments-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.comments-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.comment-form {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.comment-form textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 2px solid var(--witch-lavender);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
background-color: var(--witch-moon);
|
||||
color: var(--witch-purple);
|
||||
resize: vertical;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--witch-rose);
|
||||
}
|
||||
|
||||
.comment {
|
||||
background: var(--witch-moon);
|
||||
border: 1px solid var(--witch-lavender);
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: 500;
|
||||
color: var(--witch-plum);
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
font-size: 0.75rem;
|
||||
color: var(--witch-mauve);
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
font-size: 0.9rem;
|
||||
color: var(--witch-purple);
|
||||
}
|
||||
|
||||
.comments-loading,
|
||||
.no-comments {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
color: var(--witch-mauve);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class MusicListComponent implements OnInit {
|
||||
musicService = inject(MusicService);
|
||||
authService = inject(AuthService);
|
||||
commentsService = inject(CommentsService);
|
||||
|
||||
music = signal<Music[]>([]);
|
||||
loading = signal(true);
|
||||
showAddForm = signal(false);
|
||||
editingMusic = signal<Music | null>(null);
|
||||
typeFilter = signal<'all' | MusicType>('all');
|
||||
statusFilter = signal<'all' | MusicStatus>('all');
|
||||
|
||||
// Comments state
|
||||
comments = signal<Record<string, Comment[]>>({});
|
||||
commentsLoading = signal<Record<string, boolean>>({});
|
||||
expandedComments = signal<Record<string, boolean>>({});
|
||||
newCommentContent: Record<string, string> = {};
|
||||
|
||||
// Expose enums to template
|
||||
MusicType = MusicType;
|
||||
MusicStatus = MusicStatus;
|
||||
|
||||
// Computed signals for reactive count updates (type)
|
||||
albumCount = computed(() => this.music().filter(m => m.type === MusicType.album).length);
|
||||
singleCount = computed(() => this.music().filter(m => m.type === MusicType.single).length);
|
||||
epCount = computed(() => this.music().filter(m => m.type === MusicType.ep).length);
|
||||
|
||||
// Computed signals for reactive count updates (status)
|
||||
listeningCount = computed(() => this.music().filter(m => m.status === MusicStatus.listening).length);
|
||||
completedCount = computed(() => this.music().filter(m => m.status === MusicStatus.completed).length);
|
||||
wantToListenCount = computed(() => this.music().filter(m => m.status === MusicStatus.wantToListen).length);
|
||||
|
||||
filteredMusic = computed(() => {
|
||||
let filtered = this.music();
|
||||
|
||||
const typeFilter = this.typeFilter();
|
||||
if (typeFilter !== 'all') {
|
||||
filtered = filtered.filter(music => music.type === typeFilter);
|
||||
}
|
||||
|
||||
const statusFilter = this.statusFilter();
|
||||
if (statusFilter !== 'all') {
|
||||
filtered = filtered.filter(music => music.status === statusFilter);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
newMusic: Partial<CreateMusicDto> = {
|
||||
title: '',
|
||||
artist: '',
|
||||
@@ -554,6 +793,8 @@ export class MusicListComponent implements OnInit {
|
||||
notes: ''
|
||||
};
|
||||
|
||||
editMusicData: Partial<UpdateMusicDto> = {};
|
||||
|
||||
ngOnInit() {
|
||||
this.loadMusic();
|
||||
}
|
||||
@@ -571,30 +812,6 @@ export class MusicListComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
filteredMusic() {
|
||||
let filtered = this.music();
|
||||
|
||||
const typeFilter = this.typeFilter();
|
||||
if (typeFilter !== 'all') {
|
||||
filtered = filtered.filter(music => music.type === typeFilter);
|
||||
}
|
||||
|
||||
const statusFilter = this.statusFilter();
|
||||
if (statusFilter !== 'all') {
|
||||
filtered = filtered.filter(music => music.status === statusFilter);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
getCountByType(type: MusicType): number {
|
||||
return this.music().filter(music => music.type === type).length;
|
||||
}
|
||||
|
||||
getCountByStatus(status: MusicStatus): number {
|
||||
return this.music().filter(music => music.status === status).length;
|
||||
}
|
||||
|
||||
setTypeFilter(filter: 'all' | MusicType) {
|
||||
this.typeFilter.set(filter);
|
||||
}
|
||||
@@ -663,7 +880,108 @@ export class MusicListComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
startEdit(music: Music) {
|
||||
this.editingMusic.set(music);
|
||||
this.editMusicData = {
|
||||
title: music.title,
|
||||
artist: music.artist,
|
||||
type: music.type,
|
||||
status: music.status,
|
||||
rating: music.rating,
|
||||
notes: music.notes
|
||||
};
|
||||
this.showAddForm.set(false);
|
||||
}
|
||||
|
||||
cancelEdit() {
|
||||
this.editingMusic.set(null);
|
||||
this.editMusicData = {};
|
||||
}
|
||||
|
||||
saveEdit() {
|
||||
const music = this.editingMusic();
|
||||
if (!music || !this.editMusicData.title || !this.editMusicData.artist || !this.editMusicData.type || !this.editMusicData.status) return;
|
||||
|
||||
this.musicService.updateMusic(music.id, this.editMusicData).subscribe(() => {
|
||||
this.loadMusic();
|
||||
this.cancelEdit();
|
||||
});
|
||||
}
|
||||
|
||||
formatDate(date: Date | string): string {
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
|
||||
// Comments methods
|
||||
toggleComments(musicId: string) {
|
||||
const expanded = this.expandedComments();
|
||||
const isCurrentlyExpanded = expanded[musicId];
|
||||
|
||||
this.expandedComments.set({
|
||||
...expanded,
|
||||
[musicId]: !isCurrentlyExpanded
|
||||
});
|
||||
|
||||
if (!isCurrentlyExpanded && !this.comments()[musicId]) {
|
||||
this.loadComments(musicId);
|
||||
}
|
||||
}
|
||||
|
||||
loadComments(musicId: string) {
|
||||
this.commentsLoading.set({
|
||||
...this.commentsLoading(),
|
||||
[musicId]: true
|
||||
});
|
||||
|
||||
this.commentsService.getCommentsForMusic(musicId).subscribe({
|
||||
next: (comments) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[musicId]: comments
|
||||
});
|
||||
this.commentsLoading.set({
|
||||
...this.commentsLoading(),
|
||||
[musicId]: false
|
||||
});
|
||||
},
|
||||
error: () => {
|
||||
this.commentsLoading.set({
|
||||
...this.commentsLoading(),
|
||||
[musicId]: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCommentCount(musicId: string): number {
|
||||
return this.comments()[musicId]?.length || 0;
|
||||
}
|
||||
|
||||
addComment(musicId: string) {
|
||||
const content = this.newCommentContent[musicId];
|
||||
if (!content?.trim()) return;
|
||||
|
||||
this.commentsService.addCommentToMusic(musicId, { content }).subscribe({
|
||||
next: (comment) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[musicId]: [comment, ...(this.comments()[musicId] || [])]
|
||||
});
|
||||
this.newCommentContent[musicId] = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteComment(musicId: string, commentId: string) {
|
||||
if (!confirm('Are you sure you want to delete this comment?')) return;
|
||||
|
||||
this.commentsService.deleteCommentFromMusic(musicId, commentId).subscribe({
|
||||
next: () => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[musicId]: (this.comments()[musicId] || []).filter(c => c.id !== commentId)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user