/** * @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 { ShowsService } from '../../services/shows.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 { ShowFormComponent } from '../shared/show-form.component'; import { Show, Comment, ShowStatus, ShowType, UpdateShowDto } from '@library/shared-types'; @Component({ selector: 'app-show-detail', standalone: true, imports: [CommonModule, RouterLink, FormsModule, CommentDisplayComponent, LikeButtonComponent, ShowFormComponent], template: `
@if (showEditForm() && authService.user()?.isAdmin && show()) { } @if (loading()) {
Loading show details...
} @else if (error()) {

Show Not Found

{{ error() }}

Return to Shows
} @else if (show()) {

{{ show()!.title }}

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

Notes

{{ show()!.notes }}

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

Tags

@for (tag of show()!.tags; track tag) { {{ tag }} }
} @if (show()!.links && show()!.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: #10b981; 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; } .show-detail-card { background: white; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .show-poster-section { width: 100%; background: #f3f4f6; display: flex; justify-content: center; align-items: center; padding: 2rem; } .show-poster-large { max-width: 100%; max-height: 400px; object-fit: contain; border-radius: 4px; } .show-content { padding: 2rem; } .show-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; } .show-header h1 { margin: 0; font-size: 2rem; color: #1f2937; } .type-badge { display: inline-block; padding: 0.5rem 1rem; border-radius: 4px; font-size: 0.9rem; font-weight: 500; } .type-tvSeries { background: #3b82f6; color: white; } .type-anime { background: #ec4899; color: white; } .type-film { background: #8b5cf6; color: white; } .type-documentary { background: #14b8a6; color: white; } .status { display: inline-block; padding: 0.5rem 1rem; border-radius: 4px; font-size: 0.9rem; font-weight: 500; } .status-watching { background: #fef3c7; color: #92400e; } .status-completed { background: #d1fae5; color: #065f46; } .status-wantToWatch { 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: #10b981; 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: #10b981; 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: #10b981; text-decoration: none; font-size: 0.95rem; padding: 0.5rem 1rem; border: 2px solid #10b981; border-radius: 4px; transition: all 0.2s; font-weight: 500; } .external-link:hover { background: #10b981; 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: #10b981; } .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: #10b981; color: white; } @media (max-width: 640px) { .container { padding: 1rem; } .show-header { flex-direction: column; align-items: flex-start; } .show-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 ShowDetailComponent implements OnInit { private readonly showsService = inject(ShowsService); private readonly commentsService = inject(CommentsService); readonly authService = inject(AuthService); private readonly sanitizeService = inject(SanitizeService); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); show = signal(null); comments = signal([]); loading = signal(true); commentsLoading = signal(false); error = signal(null); newCommentContent = ''; showEditForm = signal(false); ngOnInit() { const showId = this.route.snapshot.paramMap.get('id'); if (!showId) { this.error.set('No show ID provided'); this.loading.set(false); return; } this.loadShow(showId); this.loadComments(showId); } private loadShow(showId: string) { this.loading.set(true); this.showsService.getShowById(showId).subscribe({ next: (show) => { if (!show) { this.error.set('Show not found'); } else { this.show.set(show); } this.loading.set(false); }, error: () => { this.error.set('Failed to load show. It may not exist or there was an error.'); this.loading.set(false); } }); } private loadComments(showId: string) { this.commentsLoading.set(true); this.commentsService.getCommentsForShow(showId).subscribe({ next: (comments) => { this.comments.set(comments); this.commentsLoading.set(false); }, error: () => { this.commentsLoading.set(false); } }); } addComment() { const show = this.show(); if (!show || !this.newCommentContent.trim()) return; this.commentsService.addCommentToShow(show.id, { content: this.newCommentContent }).subscribe({ next: (comment) => { this.comments.set([comment, ...this.comments()]); this.newCommentContent = ''; } }); } handleCommentEdit(event: { commentId: string; content: string }) { const show = this.show(); if (!show) return; this.commentsService.updateCommentOnShow(show.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 show = this.show(); if (!show || !confirm('Are you sure you want to delete this comment?')) return; this.commentsService.deleteCommentFromShow(show.id, commentId).subscribe({ next: () => { this.comments.set(this.comments().filter(c => c.id !== commentId)); } }); } getTypeLabel(type: ShowType): string { switch (type) { case ShowType.tvSeries: return 'TV Series'; case ShowType.anime: return 'Anime'; case ShowType.film: return 'Film'; case ShowType.documentary: return 'Documentary'; } } getStatusLabel(status: ShowStatus): string { switch (status) { case ShowStatus.watching: return 'Currently Watching'; case ShowStatus.completed: return 'Completed'; case ShowStatus.wantToWatch: return 'Want to Watch'; case ShowStatus.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'}`; } } toggleEditForm() { this.showEditForm.update(value => !value); } saveEdit(data: UpdateShowDto) { const show = this.show(); if (!show) return; this.showsService.updateShow(show.id, data).subscribe({ next: (updatedShow) => { this.show.set(updatedShow); this.showEditForm.set(false); }, error: (err) => { alert('Failed to update show: ' + (err.error?.message || 'Unknown error')); } }); } cancelEdit() { this.showEditForm.set(false); } deleteShow() { const show = this.show(); if (!show) return; if (!confirm(`Are you sure you want to delete "${show.title}"? This action cannot be undone.`)) { return; } this.showsService.deleteShow(show.id).subscribe({ next: () => { this.router.navigate(['/shows']); }, error: (err) => { alert('Failed to delete show: ' + (err.error?.message || 'Unknown error')); } }); } }