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

Manga Not Found

{{ error() }}

Return to Manga
} @else if (manga()) {

{{ manga()!.title }}

{{ getStatusLabel(manga()!.status) }}

by {{ manga()!.author }}

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

Notes

{{ manga()!.notes }}

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

Tags

@for (tag of manga()!.tags; track tag) { {{ tag }} }
} @if (manga()!.links && manga()!.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: #f59e0b; 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; } .manga-detail-card { background: white; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .manga-cover-section { width: 100%; background: #f3f4f6; display: flex; justify-content: center; align-items: center; padding: 2rem; } .manga-cover-large { max-width: 100%; max-height: 400px; object-fit: contain; border-radius: 4px; } .manga-content { padding: 2rem; } .manga-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; } .manga-header h1 { margin: 0; font-size: 2rem; color: #1f2937; } .author { color: #6b7280; font-style: italic; font-size: 1.1rem; margin: 0 0 1rem 0; } .status { display: inline-block; padding: 0.5rem 1rem; border-radius: 4px; font-size: 0.9rem; font-weight: 500; } .status-reading { background: #fef3c7; color: #92400e; } .status-completed { background: #d1fae5; color: #065f46; } .status-wantToRead { 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: #f59e0b; 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: #f59e0b; 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: #f59e0b; text-decoration: none; font-size: 0.95rem; padding: 0.5rem 1rem; border: 2px solid #f59e0b; border-radius: 4px; transition: all 0.2s; font-weight: 500; } .external-link:hover { background: #f59e0b; 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: #f59e0b; } .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: #f59e0b; color: white; } @media (max-width: 640px) { .container { padding: 1rem; } .manga-header { flex-direction: column; align-items: flex-start; } .manga-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 MangaDetailComponent implements OnInit { private readonly mangaService = inject(MangaService); private readonly commentsService = inject(CommentsService); readonly authService = inject(AuthService); private readonly sanitizeService = inject(SanitizeService); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); manga = signal(null); comments = signal([]); loading = signal(true); commentsLoading = signal(false); error = signal(null); newCommentContent = ''; showEditForm = signal(false); ngOnInit() { const mangaId = this.route.snapshot.paramMap.get('id'); if (!mangaId) { this.error.set('No manga ID provided'); this.loading.set(false); return; } this.loadManga(mangaId); this.loadComments(mangaId); } private loadManga(mangaId: string) { this.loading.set(true); this.mangaService.getMangaById(mangaId).subscribe({ next: (manga) => { if (!manga) { this.error.set('Manga not found'); } else { this.manga.set(manga); } this.loading.set(false); }, error: () => { this.error.set('Failed to load manga. It may not exist or there was an error.'); this.loading.set(false); } }); } private loadComments(mangaId: string) { this.commentsLoading.set(true); this.commentsService.getCommentsForManga(mangaId).subscribe({ next: (comments) => { this.comments.set(comments); this.commentsLoading.set(false); }, error: () => { this.commentsLoading.set(false); } }); } addComment() { const manga = this.manga(); if (!manga || !this.newCommentContent.trim()) return; this.commentsService.addCommentToManga(manga.id, { content: this.newCommentContent }).subscribe({ next: (comment) => { this.comments.set([comment, ...this.comments()]); this.newCommentContent = ''; } }); } handleCommentEdit(event: { commentId: string; content: string }) { const manga = this.manga(); if (!manga) return; this.commentsService.updateCommentOnManga(manga.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 manga = this.manga(); if (!manga || !confirm('Are you sure you want to delete this comment?')) return; this.commentsService.deleteCommentFromManga(manga.id, commentId).subscribe({ next: () => { this.comments.set(this.comments().filter(c => c.id !== commentId)); } }); } getStatusLabel(status: MangaStatus): string { switch (status) { case MangaStatus.reading: return 'Currently Reading'; case MangaStatus.completed: return 'Completed'; case MangaStatus.wantToRead: return 'Want to Read'; case MangaStatus.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: UpdateMangaDto) { const manga = this.manga(); if (!manga) return; this.mangaService.updateManga(manga.id, data).subscribe({ next: (updatedManga) => { this.manga.set(updatedManga); this.showEditForm.set(false); }, error: (err) => { alert('Failed to update manga: ' + (err.error?.message || 'Unknown error')); } }); } cancelEdit() { this.showEditForm.set(false); } deleteManga() { const manga = this.manga(); if (!manga) return; if (!confirm(`Are you sure you want to delete "${manga.title}"? This action cannot be undone.`)) { return; } this.mangaService.deleteManga(manga.id).subscribe({ next: () => { this.router.navigate(['/manga']); }, error: (err) => { alert('Failed to delete manga: ' + (err.error?.message || 'Unknown error')); } }); } }