generated from nhcarrigan/template
feat: add manga and shows collections
This commit is contained in:
@@ -0,0 +1,893 @@
|
||||
/**
|
||||
* @copyright 2026 NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { Component, OnInit, inject, signal, computed } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MangaService } from '../../services/manga.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { CommentsService } from '../../services/comments.service';
|
||||
import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment } from '@library/shared-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-manga-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
template: `
|
||||
<div class="container">
|
||||
<div class="header-section">
|
||||
<h2>My Manga Collection</h2>
|
||||
@if (authService.isAdmin()) {
|
||||
<button (click)="toggleAddForm()" class="btn btn-primary">
|
||||
{{ showAddForm() ? 'Cancel' : 'Add Manga' }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (showAddForm() && authService.isAdmin()) {
|
||||
<form (ngSubmit)="addManga()" class="add-form">
|
||||
<h3>Add New Manga</h3>
|
||||
<div class="form-group">
|
||||
<label for="title">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
[(ngModel)]="newManga.title"
|
||||
name="title"
|
||||
required
|
||||
placeholder="Enter manga title"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="author">Author/Artist</label>
|
||||
<input
|
||||
type="text"
|
||||
id="author"
|
||||
[(ngModel)]="newManga.author"
|
||||
name="author"
|
||||
required
|
||||
placeholder="Enter author or artist name"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" [(ngModel)]="newManga.status" name="status" required>
|
||||
<option [value]="MangaStatus.reading">Currently Reading</option>
|
||||
<option [value]="MangaStatus.completed">Completed</option>
|
||||
<option [value]="MangaStatus.wantToRead">Want to Read</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="rating">Rating (1-10)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="rating"
|
||||
[(ngModel)]="newManga.rating"
|
||||
name="rating"
|
||||
min="1"
|
||||
max="10"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notes">Notes</label>
|
||||
<textarea
|
||||
id="notes"
|
||||
[(ngModel)]="newManga.notes"
|
||||
name="notes"
|
||||
rows="3"
|
||||
placeholder="Any thoughts about the manga..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="coverImage">Cover Art (max 500KB)</label>
|
||||
<input
|
||||
type="file"
|
||||
id="coverImage"
|
||||
name="coverImage"
|
||||
accept="image/*"
|
||||
(change)="onImageSelected($event, 'new')"
|
||||
>
|
||||
@if (newMangaImagePreview()) {
|
||||
<div class="image-preview">
|
||||
<img [src]="newMangaImagePreview()" alt="Cover preview">
|
||||
<button type="button" (click)="clearImage('new')" class="btn btn-danger btn-sm">Remove</button>
|
||||
</div>
|
||||
}
|
||||
@if (imageError()) {
|
||||
<span class="error-text">{{ imageError() }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Add Manga</button>
|
||||
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (editingManga() && authService.isAdmin()) {
|
||||
<form (ngSubmit)="saveEdit()" class="add-form">
|
||||
<h3>Edit Manga</h3>
|
||||
<div class="form-group">
|
||||
<label for="edit-title">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-title"
|
||||
[(ngModel)]="editManga.title"
|
||||
name="title"
|
||||
required
|
||||
placeholder="Enter manga title"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-author">Author/Artist</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-author"
|
||||
[(ngModel)]="editManga.author"
|
||||
name="author"
|
||||
required
|
||||
placeholder="Enter author or artist name"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-status">Status</label>
|
||||
<select id="edit-status" [(ngModel)]="editManga.status" name="status" required>
|
||||
<option [value]="MangaStatus.reading">Currently Reading</option>
|
||||
<option [value]="MangaStatus.completed">Completed</option>
|
||||
<option [value]="MangaStatus.wantToRead">Want to Read</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-rating">Rating (1-10)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="edit-rating"
|
||||
[(ngModel)]="editManga.rating"
|
||||
name="rating"
|
||||
min="1"
|
||||
max="10"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-notes">Notes</label>
|
||||
<textarea
|
||||
id="edit-notes"
|
||||
[(ngModel)]="editManga.notes"
|
||||
name="notes"
|
||||
rows="3"
|
||||
placeholder="Any thoughts about the manga..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-coverImage">Cover Art (max 500KB)</label>
|
||||
<input
|
||||
type="file"
|
||||
id="edit-coverImage"
|
||||
name="coverImage"
|
||||
accept="image/*"
|
||||
(change)="onImageSelected($event, 'edit')"
|
||||
>
|
||||
@if (editMangaImagePreview()) {
|
||||
<div class="image-preview">
|
||||
<img [src]="editMangaImagePreview()" alt="Cover preview">
|
||||
<button type="button" (click)="clearImage('edit')" class="btn btn-danger btn-sm">Remove</button>
|
||||
</div>
|
||||
}
|
||||
@if (imageError()) {
|
||||
<span class="error-text">{{ imageError() }}</span>
|
||||
}
|
||||
</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')"
|
||||
[class.active]="statusFilter() === 'all'"
|
||||
class="filter-btn"
|
||||
>
|
||||
All ({{ mangaList().length }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setFilter(MangaStatus.reading)"
|
||||
[class.active]="statusFilter() === MangaStatus.reading"
|
||||
class="filter-btn"
|
||||
>
|
||||
Reading ({{ readingCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setFilter(MangaStatus.completed)"
|
||||
[class.active]="statusFilter() === MangaStatus.completed"
|
||||
class="filter-btn"
|
||||
>
|
||||
Completed ({{ completedCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setFilter(MangaStatus.wantToRead)"
|
||||
[class.active]="statusFilter() === MangaStatus.wantToRead"
|
||||
class="filter-btn"
|
||||
>
|
||||
Want to Read ({{ wantToReadCount() }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (loading()) {
|
||||
<div class="loading">Loading manga...</div>
|
||||
} @else if (filteredManga().length === 0) {
|
||||
<div class="empty-state">
|
||||
<p>No manga found in this category.</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="manga-grid">
|
||||
@for (manga of filteredManga(); track manga.id) {
|
||||
<div class="manga-card" [class.completed]="manga.status === MangaStatus.completed">
|
||||
@if (manga.coverImage) {
|
||||
<img [src]="manga.coverImage" [alt]="manga.title" class="manga-cover">
|
||||
}
|
||||
|
||||
<div class="manga-info">
|
||||
<h3>{{ manga.title }}</h3>
|
||||
<p class="author">by {{ manga.author }}</p>
|
||||
<span class="status status-{{ manga.status }}">
|
||||
{{ getStatusLabel(manga.status) }}
|
||||
</span>
|
||||
|
||||
@if (manga.rating) {
|
||||
<div class="rating">
|
||||
@for (star of [1,2,3,4,5,6,7,8,9,10]; track star) {
|
||||
<span [class.filled]="star <= manga.rating!">★</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (manga.notes) {
|
||||
<p class="notes">{{ manga.notes }}</p>
|
||||
}
|
||||
|
||||
@if (authService.isAdmin()) {
|
||||
<div class="actions">
|
||||
<button (click)="startEdit(manga)" class="btn btn-secondary btn-sm">
|
||||
Edit
|
||||
</button>
|
||||
<button (click)="deleteManga(manga)" class="btn btn-danger btn-sm">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="comments-section">
|
||||
<button (click)="toggleComments(manga.id)" class="btn btn-secondary btn-sm comments-toggle">
|
||||
{{ expandedComments()[manga.id] ? 'Hide' : 'Show' }} Comments{{ comments()[manga.id] ? ' (' + getCommentCount(manga.id) + ')' : '' }}
|
||||
</button>
|
||||
|
||||
@if (expandedComments()[manga.id]) {
|
||||
<div class="comments-container">
|
||||
@if (authService.isAuthenticated()) {
|
||||
<form (ngSubmit)="addComment(manga.id)" class="comment-form">
|
||||
<textarea
|
||||
[(ngModel)]="newCommentContent[manga.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()[manga.id]) {
|
||||
<div class="comments-loading">Loading comments...</div>
|
||||
} @else {
|
||||
@for (comment of comments()[manga.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(manga.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>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.add-form {
|
||||
background: #f8f9fa;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #e5e7eb;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background: #d1d5db;
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: #ec4899;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.manga-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.manga-card {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.manga-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.manga-card.completed {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.manga-cover {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.manga-info {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.manga-info h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.author {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin: 0.5rem 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-READING { background: #fef3c7; color: #92400e; }
|
||||
.status-COMPLETED { background: #d1fae5; color: #065f46; }
|
||||
.status-WANT_TO_READ { background: #fce7f3; color: #9d174d; }
|
||||
|
||||
.rating {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.rating span {
|
||||
color: #e5e7eb;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.rating span.filled {
|
||||
color: #ec4899;
|
||||
}
|
||||
|
||||
.notes {
|
||||
font-size: 0.9rem;
|
||||
color: #4b5563;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.btn-primary { background: #ec4899; color: white; }
|
||||
.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;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
max-width: 100px;
|
||||
max-height: 150px;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #ef4444;
|
||||
font-size: 0.875rem;
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
padding: 0.5rem;
|
||||
border: 2px dashed #e5e7eb;
|
||||
border-radius: 4px;
|
||||
background: #f9fafb;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="file"]:hover {
|
||||
border-color: #ec4899;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class MangaListComponent implements OnInit {
|
||||
mangaService = inject(MangaService);
|
||||
authService = inject(AuthService);
|
||||
commentsService = inject(CommentsService);
|
||||
|
||||
mangaList = signal<Manga[]>([]);
|
||||
loading = signal(true);
|
||||
showAddForm = signal(false);
|
||||
editingManga = signal<Manga | null>(null);
|
||||
statusFilter = signal<'all' | MangaStatus>('all');
|
||||
|
||||
comments = signal<Record<string, Comment[]>>({});
|
||||
commentsLoading = signal<Record<string, boolean>>({});
|
||||
expandedComments = signal<Record<string, boolean>>({});
|
||||
newCommentContent: Record<string, string> = {};
|
||||
|
||||
newMangaImagePreview = signal<string | null>(null);
|
||||
editMangaImagePreview = signal<string | null>(null);
|
||||
imageError = signal<string | null>(null);
|
||||
private readonly MAX_IMAGE_SIZE = 500 * 1024;
|
||||
|
||||
MangaStatus = MangaStatus;
|
||||
|
||||
readingCount = computed(() => this.mangaList().filter(m => m.status === MangaStatus.reading).length);
|
||||
completedCount = computed(() => this.mangaList().filter(m => m.status === MangaStatus.completed).length);
|
||||
wantToReadCount = computed(() => this.mangaList().filter(m => m.status === MangaStatus.wantToRead).length);
|
||||
|
||||
filteredManga = computed(() => {
|
||||
const filter = this.statusFilter();
|
||||
if (filter === 'all') {
|
||||
return this.mangaList();
|
||||
}
|
||||
return this.mangaList().filter(m => m.status === filter);
|
||||
});
|
||||
|
||||
newManga: Partial<CreateMangaDto> = {
|
||||
title: '',
|
||||
author: '',
|
||||
status: MangaStatus.wantToRead,
|
||||
rating: undefined,
|
||||
notes: ''
|
||||
};
|
||||
|
||||
editManga: Partial<UpdateMangaDto> = {};
|
||||
|
||||
ngOnInit() {
|
||||
this.loadManga();
|
||||
}
|
||||
|
||||
loadManga() {
|
||||
this.loading.set(true);
|
||||
this.mangaService.getAllManga().subscribe({
|
||||
next: (manga) => {
|
||||
this.mangaList.set(manga);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.loading.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setFilter(filter: 'all' | MangaStatus) {
|
||||
this.statusFilter.set(filter);
|
||||
}
|
||||
|
||||
getStatusLabel(status: MangaStatus): string {
|
||||
switch (status) {
|
||||
case MangaStatus.reading: return 'Currently Reading';
|
||||
case MangaStatus.completed: return 'Completed';
|
||||
case MangaStatus.wantToRead: return 'Want to Read';
|
||||
}
|
||||
}
|
||||
|
||||
toggleAddForm() {
|
||||
this.showAddForm.update(v => !v);
|
||||
if (!this.showAddForm()) {
|
||||
this.resetForm();
|
||||
}
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
this.newManga = {
|
||||
title: '',
|
||||
author: '',
|
||||
status: MangaStatus.wantToRead,
|
||||
rating: undefined,
|
||||
notes: '',
|
||||
coverImage: undefined
|
||||
};
|
||||
this.newMangaImagePreview.set(null);
|
||||
this.imageError.set(null);
|
||||
}
|
||||
|
||||
addManga() {
|
||||
if (!this.newManga.title || !this.newManga.author || !this.newManga.status) return;
|
||||
|
||||
const mangaToAdd: CreateMangaDto = {
|
||||
title: this.newManga.title,
|
||||
author: this.newManga.author,
|
||||
status: this.newManga.status,
|
||||
rating: this.newManga.rating,
|
||||
notes: this.newManga.notes,
|
||||
coverImage: this.newManga.coverImage
|
||||
};
|
||||
|
||||
this.mangaService.createManga(mangaToAdd).subscribe(() => {
|
||||
this.loadManga();
|
||||
this.toggleAddForm();
|
||||
});
|
||||
}
|
||||
|
||||
deleteManga(manga: Manga) {
|
||||
if (confirm(`Are you sure you want to delete "${manga.title}"?`)) {
|
||||
this.mangaService.deleteManga(manga.id).subscribe(() => {
|
||||
this.loadManga();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
startEdit(manga: Manga) {
|
||||
this.editingManga.set(manga);
|
||||
this.editManga = {
|
||||
title: manga.title,
|
||||
author: manga.author,
|
||||
status: manga.status,
|
||||
rating: manga.rating,
|
||||
notes: manga.notes,
|
||||
coverImage: manga.coverImage
|
||||
};
|
||||
this.editMangaImagePreview.set(manga.coverImage || null);
|
||||
this.showAddForm.set(false);
|
||||
this.imageError.set(null);
|
||||
}
|
||||
|
||||
cancelEdit() {
|
||||
this.editingManga.set(null);
|
||||
this.editManga = {};
|
||||
this.editMangaImagePreview.set(null);
|
||||
this.imageError.set(null);
|
||||
}
|
||||
|
||||
saveEdit() {
|
||||
const manga = this.editingManga();
|
||||
if (!manga || !this.editManga.title || !this.editManga.author || !this.editManga.status) return;
|
||||
|
||||
this.mangaService.updateManga(manga.id, this.editManga).subscribe(() => {
|
||||
this.loadManga();
|
||||
this.cancelEdit();
|
||||
});
|
||||
}
|
||||
|
||||
onImageSelected(event: Event, target: 'new' | 'edit') {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
this.imageError.set(null);
|
||||
|
||||
if (file.size > this.MAX_IMAGE_SIZE) {
|
||||
this.imageError.set(`Image too large. Maximum size is ${this.MAX_IMAGE_SIZE / 1024}KB.`);
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
this.imageError.set('Please select an image file.');
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result as string;
|
||||
if (target === 'new') {
|
||||
this.newMangaImagePreview.set(base64);
|
||||
this.newManga.coverImage = base64;
|
||||
} else {
|
||||
this.editMangaImagePreview.set(base64);
|
||||
this.editManga.coverImage = base64;
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
clearImage(target: 'new' | 'edit') {
|
||||
if (target === 'new') {
|
||||
this.newMangaImagePreview.set(null);
|
||||
this.newManga.coverImage = undefined;
|
||||
} else {
|
||||
this.editMangaImagePreview.set(null);
|
||||
this.editManga.coverImage = undefined;
|
||||
}
|
||||
this.imageError.set(null);
|
||||
}
|
||||
|
||||
formatDate(date: Date | string): string {
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
|
||||
toggleComments(mangaId: string) {
|
||||
const expanded = this.expandedComments();
|
||||
const isCurrentlyExpanded = expanded[mangaId];
|
||||
|
||||
this.expandedComments.set({
|
||||
...expanded,
|
||||
[mangaId]: !isCurrentlyExpanded
|
||||
});
|
||||
|
||||
if (!isCurrentlyExpanded && !this.comments()[mangaId]) {
|
||||
this.loadComments(mangaId);
|
||||
}
|
||||
}
|
||||
|
||||
loadComments(mangaId: string) {
|
||||
this.commentsLoading.set({
|
||||
...this.commentsLoading(),
|
||||
[mangaId]: true
|
||||
});
|
||||
|
||||
this.commentsService.getCommentsForManga(mangaId).subscribe({
|
||||
next: (comments) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[mangaId]: comments
|
||||
});
|
||||
this.commentsLoading.set({
|
||||
...this.commentsLoading(),
|
||||
[mangaId]: false
|
||||
});
|
||||
},
|
||||
error: () => {
|
||||
this.commentsLoading.set({
|
||||
...this.commentsLoading(),
|
||||
[mangaId]: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCommentCount(mangaId: string): number {
|
||||
return this.comments()[mangaId]?.length || 0;
|
||||
}
|
||||
|
||||
addComment(mangaId: string) {
|
||||
const content = this.newCommentContent[mangaId];
|
||||
if (!content?.trim()) return;
|
||||
|
||||
this.commentsService.addCommentToManga(mangaId, { content }).subscribe({
|
||||
next: (comment) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[mangaId]: [comment, ...(this.comments()[mangaId] || [])]
|
||||
});
|
||||
this.newCommentContent[mangaId] = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteComment(mangaId: string, commentId: string) {
|
||||
if (!confirm('Are you sure you want to delete this comment?')) return;
|
||||
|
||||
this.commentsService.deleteCommentFromManga(mangaId, commentId).subscribe({
|
||||
next: () => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[mangaId]: (this.comments()[mangaId] || []).filter(c => c.id !== commentId)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user