feat: bunch of work done here, got comments and edit and delete

This commit is contained in:
2026-02-04 13:00:16 -08:00
parent b6d66d34cb
commit 318f3bc500
19 changed files with 1868 additions and 117 deletions
@@ -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)
});
}
});
}
}