/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { Component, OnInit, inject, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterLink, ActivatedRoute, Router } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { MusicService } from '../../services/music.service'; import { CommentsService } from '../../services/comments.service'; import { AuthService } from '../../services/auth.service'; import { SanitizeService } from '../../services/sanitize.service'; import { CommentDisplayComponent } from '../comment-display/comment-display.component'; import { LikeButtonComponent } from '../shared/like-button.component'; import { MusicFormComponent } from '../shared/music-form.component'; import { Music, Comment, MusicStatus, MusicType, UpdateMusicDto } from '@library/shared-types'; @Component({ selector: 'app-music-detail', standalone: true, imports: [CommonModule, RouterLink, FormsModule, CommentDisplayComponent, LikeButtonComponent, MusicFormComponent], template: `
@if (showEditForm() && authService.user()?.isAdmin && music()) { } @if (loading()) {
Loading music details...
} @else if (error()) {

Music Not Found

{{ error() }}

Return to Music
} @else if (music()) {

{{ music()!.title }}

{{ getTypeLabel(music()!.type) }} {{ getStatusLabel(music()!.status) }}

by {{ music()!.artist }}

@if (authService.user()?.isAdmin) {
} @if (music()!.rating) {
Rating:
@for (star of [1,2,3,4,5,6,7,8,9,10]; track star) { } ({{ music()!.rating }}/10)
} @if (music()!.timeSpent) {
Listening Time: {{ formatTimeSpent(music()!.timeSpent!) }}
} @if (music()!.dateStarted) {
Started: {{ formatDate(music()!.dateStarted!) }}
} @if (music()!.dateFinished) {
Finished: {{ formatDate(music()!.dateFinished!) }}
}
Added: {{ formatDate(music()!.createdAt) }}
Last Updated: {{ formatDate(music()!.updatedAt) }}
@if (music()!.notes) {

Notes

{{ music()!.notes }}

} @if (music()!.tags && music()!.tags.length > 0) {

Tags

@for (tag of music()!.tags; track tag) { {{ tag }} }
} @if (music()!.links && music()!.links.length > 0) { }

Comments

@if (authService.isAuthenticated()) { @if (authService.user()?.isBanned) {
You have been banned from commenting.
} @else {
} } @else {

Please sign in to comment.

} @if (commentsLoading()) {
Loading comments...
} @else { }
}
`, styles: [` .container { max-width: 900px; margin: 0 auto; padding: 2rem; } .breadcrumb { margin-bottom: 1.5rem; } .breadcrumb-link { color: #74b9ff; text-decoration: none; font-weight: 500; transition: opacity 0.3s; } .breadcrumb-link:hover { opacity: 0.8; } .loading { text-align: center; padding: 3rem; color: #666; font-size: 1.1rem; } .error-state { text-align: center; padding: 3rem; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; } .error-state h2 { color: #991b1b; margin-bottom: 1rem; } .error-state p { color: #dc2626; margin-bottom: 1.5rem; } .music-detail-card { background: white; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .music-cover-section { width: 100%; background: #f3f4f6; display: flex; justify-content: center; align-items: center; padding: 2rem; } .music-cover-large { max-width: 100%; max-height: 400px; object-fit: contain; border-radius: 4px; } .music-content { padding: 2rem; } .music-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; } .music-header h1 { margin: 0; font-size: 2rem; color: #1f2937; } .artist { color: #6b7280; font-weight: 500; font-size: 1.1rem; margin: 0 0 1rem 0; } .type-badge { display: inline-block; padding: 0.5rem 1rem; border-radius: 4px; font-size: 0.9rem; font-weight: 500; } .type-album { background: #a78bfa; color: white; } .type-single { background: #fb923c; color: white; } .type-ep { background: #60a5fa; color: white; } .status { display: inline-block; padding: 0.5rem 1rem; border-radius: 4px; font-size: 0.9rem; font-weight: 500; } .status-listening { background: #fef3c7; color: #92400e; } .status-completed { background: #d1fae5; color: #065f46; } .status-wantToListen { background: #e0e7ff; color: #3730a3; } .status-retired { background: #f3f4f6; color: #4b5563; } .admin-actions { display: flex; gap: 0.75rem; margin-bottom: 1.5rem; padding: 1rem; background: #fef3c7; border-radius: 4px; border: 1px solid #fbbf24; } .btn-edit { background: #3b82f6; color: white; } .btn-edit:hover { background: #2563eb; } .btn-delete { background: #ef4444; color: white; } .btn-delete:hover { background: #dc2626; } .info-row { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem; padding: 0.5rem 0; border-bottom: 1px solid #f3f4f6; } .info-label { font-weight: 600; color: #4b5563; min-width: 120px; } .info-value { color: #1f2937; } .time-spent { color: #8b5cf6; font-weight: 600; } .rating { display: flex; align-items: center; gap: 0.25rem; } .rating span { color: #e5e7eb; font-size: 1.2rem; } .rating span.filled { color: #f59e0b; } .rating-text { margin-left: 0.5rem; font-size: 1rem; color: #6b7280; font-weight: 500; } .like-section { margin: 1.5rem 0; } .notes-section, .tags-section, .links-section { margin-top: 2rem; } .notes-section h3, .tags-section h3, .links-section h3, .comments-section h3 { font-size: 1.25rem; color: #1f2937; margin-bottom: 1rem; } .notes { font-size: 1rem; color: #4b5563; line-height: 1.6; white-space: pre-wrap; } .tags-display { display: flex; flex-wrap: wrap; gap: 0.5rem; } .tag-chip { background: #74b9ff; color: white; padding: 0.375rem 0.875rem; border-radius: 12px; font-size: 0.875rem; font-weight: 500; } .links-display { display: flex; flex-wrap: wrap; gap: 0.75rem; } .external-link { color: #74b9ff; text-decoration: none; font-size: 0.95rem; padding: 0.5rem 1rem; border: 2px solid #74b9ff; border-radius: 4px; transition: all 0.2s; font-weight: 500; } .external-link:hover { background: #74b9ff; color: white; } .comments-section { margin-top: 2rem; padding-top: 2rem; border-top: 2px solid #e5e7eb; } .comment-form { margin-bottom: 1.5rem; } .comment-form textarea { width: 100%; padding: 0.75rem; border: 1px solid #d1d5db; border-radius: 4px; font-size: 1rem; resize: vertical; margin-bottom: 0.75rem; font-family: inherit; } .comment-form textarea:focus { outline: none; border-color: #74b9ff; } .banned-notice { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; padding: 1rem; border-radius: 4px; text-align: center; margin-bottom: 1.5rem; font-size: 0.95rem; } .auth-prompt { background: #f3f4f6; padding: 1rem; border-radius: 4px; text-align: center; margin-bottom: 1.5rem; } .auth-prompt p { margin: 0; color: #4b5563; } .comments-loading { text-align: center; padding: 1.5rem; color: #6b7280; font-size: 0.95rem; } .btn { padding: 0.625rem 1.25rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; transition: all 0.3s; font-weight: 500; } .btn:hover { opacity: 0.9; } .btn-primary { background: #74b9ff; color: white; } @media (max-width: 640px) { .container { padding: 1rem; } .music-header { flex-direction: column; align-items: flex-start; } .music-header h1 { font-size: 1.5rem; } .admin-actions { display: flex; gap: 0.75rem; margin-bottom: 1.5rem; padding: 1rem; background: #fef3c7; border-radius: 4px; border: 1px solid #fbbf24; } .btn-edit { background: #3b82f6; color: white; } .btn-edit:hover { background: #2563eb; } .btn-delete { background: #ef4444; color: white; } .btn-delete:hover { background: #dc2626; } .info-row { flex-direction: column; align-items: flex-start; gap: 0.25rem; } .info-label { min-width: auto; } } `] }) export class MusicDetailComponent implements OnInit { private readonly musicService = inject(MusicService); private readonly commentsService = inject(CommentsService); readonly authService = inject(AuthService); private readonly sanitizeService = inject(SanitizeService); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); music = signal(null); comments = signal([]); loading = signal(true); commentsLoading = signal(false); error = signal(null); newCommentContent = ''; showEditForm = signal(false); ngOnInit() { const musicId = this.route.snapshot.paramMap.get('id'); if (!musicId) { this.error.set('No music ID provided'); this.loading.set(false); return; } this.loadMusic(musicId); this.loadComments(musicId); } private loadMusic(musicId: string) { this.loading.set(true); this.musicService.getMusicById(musicId).subscribe({ next: (music) => { if (!music) { this.error.set('Music not found'); } else { this.music.set(music); } this.loading.set(false); }, error: () => { this.error.set('Failed to load music. It may not exist or there was an error.'); this.loading.set(false); } }); } private loadComments(musicId: string) { this.commentsLoading.set(true); this.commentsService.getCommentsForMusic(musicId).subscribe({ next: (comments) => { this.comments.set(comments); this.commentsLoading.set(false); }, error: () => { this.commentsLoading.set(false); } }); } addComment() { const music = this.music(); if (!music || !this.newCommentContent.trim()) return; this.commentsService.addCommentToMusic(music.id, { content: this.newCommentContent }).subscribe({ next: (comment) => { this.comments.set([comment, ...this.comments()]); this.newCommentContent = ''; } }); } handleCommentEdit(event: { commentId: string; content: string }) { const music = this.music(); if (!music) return; this.commentsService.updateCommentOnMusic(music.id, event.commentId, event.content).subscribe({ next: (updatedComment) => { this.comments.set( this.comments().map(c => c.id === event.commentId ? updatedComment : c) ); } }); } deleteComment(commentId: string) { const music = this.music(); if (!music || !confirm('Are you sure you want to delete this comment?')) return; this.commentsService.deleteCommentFromMusic(music.id, commentId).subscribe({ next: () => { this.comments.set(this.comments().filter(c => c.id !== commentId)); } }); } getTypeLabel(type: MusicType): string { switch (type) { case MusicType.album: return 'Album'; case MusicType.single: return 'Single'; case MusicType.ep: return 'EP'; } } getStatusLabel(status: MusicStatus): string { switch (status) { case MusicStatus.listening: return 'Currently Listening'; case MusicStatus.completed: return 'Completed'; case MusicStatus.wantToListen: return 'Want to Listen'; case MusicStatus.retired: return 'Retired'; } } formatDate(date: Date | string): string { return new Date(date).toLocaleDateString('en-GB', { year: 'numeric', month: 'long', day: 'numeric' }); } formatTimeSpent(minutes: number): string { const hours = Math.floor(minutes / 60); const mins = minutes % 60; if (hours === 0) { return `${mins} minutes`; } else if (mins === 0) { return `${hours} hour${hours === 1 ? '' : 's'}`; } else { return `${hours} hour${hours === 1 ? '' : 's'} ${mins} minute${mins === 1 ? '' : 's'}`; } } editMusic() { this.showEditForm.set(true); } cancelEdit() { this.showEditForm.set(false); } saveEdit(data: UpdateMusicDto) { const music = this.music(); if (!music) return; this.musicService.updateMusic(music.id, data).subscribe({ next: (updatedMusic) => { this.music.set(updatedMusic); this.showEditForm.set(false); }, error: (err) => { alert('Failed to update music: ' + (err.error?.message || 'Unknown error')); } }); } deleteMusic() { const music = this.music(); if (!music) return; if (!confirm(`Are you sure you want to delete "${music.title}"? This action cannot be undone.`)) { return; } this.musicService.deleteMusic(music.id).subscribe({ next: () => { this.router.navigate(['/music']); }, error: (err) => { alert('Failed to delete music: ' + (err.error?.message || 'Unknown error')); } }); } }