generated from nhcarrigan/template
feat: add reusable form components and inline editing for all media types
Created reusable form components for all media types (Game, Book, Music, Show, Manga, Art) to provide a consistent editing experience across the application. Changes: - Created 6 new form components in shared folder for all media types - Each form component supports both 'add' and 'edit' modes - Forms include all fields: title, author/artist/platform, status, dates, rating, time spent, notes, cover images, tags, and links - Added inline editing to all detail views using form components - Detail views now show edit form inline instead of navigating away - Integrated form components into admin suggestions workflow - Admins can now review and edit all suggestion details before accepting - Added scroll-to-top functionality when clicking edit in all list views - Ensures edit form is visible when opened from paginated lists Benefits: - Consistent UX across all media types - Better editing experience with inline forms - Improved admin suggestion workflow with full editing capabilities - No route navigation for edits (better for SEO and UX) - All forms follow the same styling and interaction patterns Co-Authored-By: Naomi Carrigan <commits@nhcarrigan.com>
This commit is contained in:
@@ -14,18 +14,28 @@ 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 { Show, Comment, ShowStatus, ShowType } from '@library/shared-types';
|
||||
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],
|
||||
imports: [CommonModule, RouterLink, FormsModule, CommentDisplayComponent, LikeButtonComponent, ShowFormComponent],
|
||||
template: `
|
||||
<div class="container">
|
||||
<div class="breadcrumb">
|
||||
<a routerLink="/shows" class="breadcrumb-link">← Back to Shows</a>
|
||||
</div>
|
||||
|
||||
@if (showEditForm() && authService.user()?.isAdmin && show()) {
|
||||
<app-show-form
|
||||
mode="edit"
|
||||
[show]="show()!"
|
||||
(save)="saveEdit($event)"
|
||||
(cancel)="cancelEdit()"
|
||||
></app-show-form>
|
||||
}
|
||||
|
||||
@if (loading()) {
|
||||
<div class="loading">Loading show details...</div>
|
||||
} @else if (error()) {
|
||||
@@ -52,6 +62,12 @@ import { Show, Comment, ShowStatus, ShowType } from '@library/shared-types';
|
||||
{{ getStatusLabel(show()!.status) }}
|
||||
</span>
|
||||
</div>
|
||||
@if (authService.user()?.isAdmin) {
|
||||
<div class="admin-actions">
|
||||
<button (click)="toggleEditForm()" class="btn btn-edit">✏️ {{ showEditForm() ? 'Cancel Edit' : 'Edit' }}</button>
|
||||
<button (click)="deleteShow()" class="btn btn-delete">🗑️ Delete</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (show()!.rating) {
|
||||
<div class="info-row">
|
||||
@@ -318,6 +334,35 @@ import { Show, Comment, ShowStatus, ShowType } from '@library/shared-types';
|
||||
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;
|
||||
@@ -517,7 +562,36 @@ import { Show, Comment, ShowStatus, ShowType } from '@library/shared-types';
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
|
||||
.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;
|
||||
@@ -543,6 +617,7 @@ export class ShowDetailComponent implements OnInit {
|
||||
commentsLoading = signal(false);
|
||||
error = signal<string | null>(null);
|
||||
newCommentContent = '';
|
||||
showEditForm = signal(false);
|
||||
|
||||
ngOnInit() {
|
||||
const showId = this.route.snapshot.paramMap.get('id');
|
||||
@@ -661,4 +736,45 @@ export class ShowDetailComponent implements OnInit {
|
||||
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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user