generated from nhcarrigan/template
feat: ability to edit and delete comments
This commit is contained in:
@@ -104,7 +104,12 @@ import type { AuditLog, AuditAction, AuditCategory } from '@library/shared-types
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ auditService.getActionLabel(log.action) }}</td>
|
||||
<td class="details">{{ log.details ?? '-' }}</td>
|
||||
<td class="details" [class.expanded]="expandedRows()[log.id]" (click)="toggleRowExpand(log.id)">
|
||||
<span class="details-content">{{ log.details ?? '-' }}</span>
|
||||
@if (log.details && log.details.length > 50) {
|
||||
<span class="expand-hint">{{ expandedRows()[log.id] ? '(click to collapse)' : '(click to expand)' }}</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge" [class.success]="log.success" [class.failed]="!log.success">
|
||||
{{ log.success ? '✓' : '✗' }}
|
||||
@@ -258,11 +263,33 @@ import type { AuditLog, AuditAction, AuditCategory } from '@library/shared-types
|
||||
|
||||
.details {
|
||||
max-width: 400px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details .details-content {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.details.expanded .details-content {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.details .expand-hint {
|
||||
display: block;
|
||||
font-size: 0.7rem;
|
||||
color: #9ca3af;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.details:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
@@ -320,6 +347,7 @@ export class AdminAuditComponent implements OnInit {
|
||||
loading = signal(true);
|
||||
currentPage = signal(1);
|
||||
totalPages = signal(1);
|
||||
expandedRows = signal<Record<string, boolean>>({});
|
||||
|
||||
selectedCategory = '';
|
||||
selectedAction = '';
|
||||
@@ -370,4 +398,11 @@ export class AdminAuditComponent implements OnInit {
|
||||
const d = new Date(date);
|
||||
return d.toLocaleString();
|
||||
}
|
||||
|
||||
toggleRowExpand(logId: string) {
|
||||
this.expandedRows.set({
|
||||
...this.expandedRows(),
|
||||
[logId]: !this.expandedRows()[logId]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,11 +239,28 @@ import { Art, CreateArtDto, UpdateArtDto, Comment } from '@library/shared-types'
|
||||
}
|
||||
<span class="comment-author">{{ comment.user.username }}</span>
|
||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||
@if (authService.isAdmin()) {
|
||||
@if (canEditComment(comment)) {
|
||||
<button (click)="startEditComment(art.id, comment)" class="btn btn-secondary btn-xs">Edit</button>
|
||||
}
|
||||
@if (canDeleteComment(comment)) {
|
||||
<button (click)="deleteComment(art.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||
}
|
||||
</div>
|
||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||
@if (editingCommentId() === comment.id) {
|
||||
<div class="comment-edit-form">
|
||||
<textarea
|
||||
[(ngModel)]="editCommentContent"
|
||||
name="editComment"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<div class="comment-edit-actions">
|
||||
<button (click)="saveCommentEdit(art.id, comment.id)" class="btn btn-primary btn-xs">Save</button>
|
||||
<button (click)="cancelCommentEdit()" class="btn btn-secondary btn-xs">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||
}
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||
@@ -639,6 +656,32 @@ import { Art, CreateArtDto, UpdateArtDto, Comment } from '@library/shared-types'
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.comment-edit-form {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-edit-form textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 2px solid var(--witch-lavender);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
background-color: var(--witch-moon);
|
||||
color: var(--witch-purple);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.comment-edit-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--witch-rose);
|
||||
}
|
||||
|
||||
.comment-edit-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class ArtGalleryComponent implements OnInit {
|
||||
@@ -658,6 +701,8 @@ export class ArtGalleryComponent implements OnInit {
|
||||
commentsLoading = signal<Record<string, boolean>>({});
|
||||
expandedComments = signal<Record<string, boolean>>({});
|
||||
newCommentContent: Record<string, string> = {};
|
||||
editingCommentId = signal<string | null>(null);
|
||||
editCommentContent = '';
|
||||
|
||||
newArt: Partial<CreateArtDto> = {
|
||||
title: '',
|
||||
@@ -840,4 +885,42 @@ export class ArtGalleryComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
canEditComment(comment: Comment): boolean {
|
||||
const user = this.authService.user();
|
||||
if (!user) return false;
|
||||
return comment.userId === user.id || this.authService.isAdmin();
|
||||
}
|
||||
|
||||
canDeleteComment(comment: Comment): boolean {
|
||||
const user = this.authService.user();
|
||||
if (!user) return false;
|
||||
return comment.userId === user.id || this.authService.isAdmin();
|
||||
}
|
||||
|
||||
startEditComment(artId: string, comment: Comment) {
|
||||
this.editingCommentId.set(comment.id);
|
||||
this.editCommentContent = comment.rawContent ?? comment.content;
|
||||
}
|
||||
|
||||
cancelCommentEdit() {
|
||||
this.editingCommentId.set(null);
|
||||
this.editCommentContent = '';
|
||||
}
|
||||
|
||||
saveCommentEdit(artId: string, commentId: string) {
|
||||
if (!this.editCommentContent.trim()) return;
|
||||
|
||||
this.commentsService.updateCommentOnArt(artId, commentId, this.editCommentContent).subscribe({
|
||||
next: (updatedComment) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[artId]: (this.comments()[artId] || []).map(c =>
|
||||
c.id === commentId ? updatedComment : c
|
||||
)
|
||||
});
|
||||
this.cancelCommentEdit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,11 +346,28 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar
|
||||
}
|
||||
<span class="comment-author">{{ comment.user.username }}</span>
|
||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||
@if (authService.isAdmin()) {
|
||||
@if (canEditComment(comment)) {
|
||||
<button (click)="startEditComment(book.id, comment)" class="btn btn-secondary btn-xs">Edit</button>
|
||||
}
|
||||
@if (canDeleteComment(comment)) {
|
||||
<button (click)="deleteComment(book.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||
}
|
||||
</div>
|
||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||
@if (editingCommentId() === comment.id) {
|
||||
<div class="comment-edit-form">
|
||||
<textarea
|
||||
[(ngModel)]="editCommentContent"
|
||||
name="editComment"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<div class="comment-edit-actions">
|
||||
<button (click)="saveCommentEdit(book.id, comment.id)" class="btn btn-primary btn-xs">Save</button>
|
||||
<button (click)="cancelCommentEdit()" class="btn btn-secondary btn-xs">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||
}
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||
@@ -721,6 +738,32 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.comment-edit-form {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-edit-form textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 2px solid var(--witch-lavender);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
background-color: var(--witch-moon);
|
||||
color: var(--witch-purple);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.comment-edit-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--witch-rose);
|
||||
}
|
||||
|
||||
.comment-edit-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
@@ -772,6 +815,8 @@ export class BooksListComponent implements OnInit {
|
||||
commentsLoading = signal<Record<string, boolean>>({});
|
||||
expandedComments = signal<Record<string, boolean>>({});
|
||||
newCommentContent: Record<string, string> = {};
|
||||
editingCommentId = signal<string | null>(null);
|
||||
editCommentContent = '';
|
||||
|
||||
// Image upload state
|
||||
newBookImagePreview = signal<string | null>(null);
|
||||
@@ -1038,4 +1083,42 @@ export class BooksListComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
canEditComment(comment: Comment): boolean {
|
||||
const user = this.authService.user();
|
||||
if (!user) return false;
|
||||
return comment.userId === user.id || this.authService.isAdmin();
|
||||
}
|
||||
|
||||
canDeleteComment(comment: Comment): boolean {
|
||||
const user = this.authService.user();
|
||||
if (!user) return false;
|
||||
return comment.userId === user.id || this.authService.isAdmin();
|
||||
}
|
||||
|
||||
startEditComment(bookId: string, comment: Comment) {
|
||||
this.editingCommentId.set(comment.id);
|
||||
this.editCommentContent = comment.rawContent ?? comment.content;
|
||||
}
|
||||
|
||||
cancelCommentEdit() {
|
||||
this.editingCommentId.set(null);
|
||||
this.editCommentContent = '';
|
||||
}
|
||||
|
||||
saveCommentEdit(bookId: string, commentId: string) {
|
||||
if (!this.editCommentContent.trim()) return;
|
||||
|
||||
this.commentsService.updateCommentOnBook(bookId, commentId, this.editCommentContent).subscribe({
|
||||
next: (updatedComment) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[bookId]: (this.comments()[bookId] || []).map(c =>
|
||||
c.id === commentId ? updatedComment : c
|
||||
)
|
||||
});
|
||||
this.cancelCommentEdit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -311,11 +311,28 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment } from '@librar
|
||||
}
|
||||
<span class="comment-author">{{ comment.user.username }}</span>
|
||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||
@if (authService.isAdmin()) {
|
||||
@if (canEditComment(comment)) {
|
||||
<button (click)="startEditComment(game.id, comment)" class="btn btn-secondary btn-xs">Edit</button>
|
||||
}
|
||||
@if (canDeleteComment(comment)) {
|
||||
<button (click)="deleteComment(game.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||
}
|
||||
</div>
|
||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||
@if (editingCommentId() === comment.id) {
|
||||
<div class="comment-edit-form">
|
||||
<textarea
|
||||
[(ngModel)]="editCommentContent"
|
||||
name="editComment"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<div class="comment-edit-actions">
|
||||
<button (click)="saveCommentEdit(game.id, comment.id)" class="btn btn-primary btn-xs">Save</button>
|
||||
<button (click)="cancelCommentEdit()" class="btn btn-secondary btn-xs">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||
}
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||
@@ -600,6 +617,25 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment } from '@librar
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.comment-edit-form {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-edit-form textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.comment-edit-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
@@ -651,6 +687,8 @@ export class GamesListComponent implements OnInit {
|
||||
commentsLoading = signal<Record<string, boolean>>({});
|
||||
expandedComments = signal<Record<string, boolean>>({});
|
||||
newCommentContent: Record<string, string> = {};
|
||||
editingCommentId = signal<string | null>(null);
|
||||
editCommentContent = '';
|
||||
|
||||
// Image upload state
|
||||
newGameImagePreview = signal<string | null>(null);
|
||||
@@ -913,4 +951,42 @@ export class GamesListComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
canEditComment(comment: Comment): boolean {
|
||||
const user = this.authService.user();
|
||||
if (!user) return false;
|
||||
return comment.userId === user.id || this.authService.isAdmin();
|
||||
}
|
||||
|
||||
canDeleteComment(comment: Comment): boolean {
|
||||
const user = this.authService.user();
|
||||
if (!user) return false;
|
||||
return comment.userId === user.id || this.authService.isAdmin();
|
||||
}
|
||||
|
||||
startEditComment(gameId: string, comment: Comment) {
|
||||
this.editingCommentId.set(comment.id);
|
||||
this.editCommentContent = comment.rawContent ?? comment.content;
|
||||
}
|
||||
|
||||
cancelCommentEdit() {
|
||||
this.editingCommentId.set(null);
|
||||
this.editCommentContent = '';
|
||||
}
|
||||
|
||||
saveCommentEdit(gameId: string, commentId: string) {
|
||||
if (!this.editCommentContent.trim()) return;
|
||||
|
||||
this.commentsService.updateCommentOnGame(gameId, commentId, this.editCommentContent).subscribe({
|
||||
next: (updatedComment) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[gameId]: (this.comments()[gameId] || []).map(c =>
|
||||
c.id === commentId ? updatedComment : c
|
||||
)
|
||||
});
|
||||
this.cancelCommentEdit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -311,11 +311,28 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment } from '@li
|
||||
}
|
||||
<span class="comment-author">{{ comment.user.username }}</span>
|
||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||
@if (authService.isAdmin()) {
|
||||
@if (canEditComment(comment)) {
|
||||
<button (click)="startEditComment(manga.id, comment)" class="btn btn-secondary btn-xs">Edit</button>
|
||||
}
|
||||
@if (canDeleteComment(comment)) {
|
||||
<button (click)="deleteComment(manga.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||
}
|
||||
</div>
|
||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||
@if (editingCommentId() === comment.id) {
|
||||
<div class="comment-edit-form">
|
||||
<textarea
|
||||
[(ngModel)]="editCommentContent"
|
||||
name="editComment"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<div class="comment-edit-actions">
|
||||
<button (click)="saveCommentEdit(manga.id, comment.id)" class="btn btn-primary btn-xs">Save</button>
|
||||
<button (click)="cancelCommentEdit()" class="btn btn-secondary btn-xs">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||
}
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||
@@ -602,6 +619,25 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment } from '@li
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.comment-edit-form {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-edit-form textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.comment-edit-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
@@ -652,6 +688,8 @@ export class MangaListComponent implements OnInit {
|
||||
commentsLoading = signal<Record<string, boolean>>({});
|
||||
expandedComments = signal<Record<string, boolean>>({});
|
||||
newCommentContent: Record<string, string> = {};
|
||||
editingCommentId = signal<string | null>(null);
|
||||
editCommentContent = '';
|
||||
|
||||
newMangaImagePreview = signal<string | null>(null);
|
||||
editMangaImagePreview = signal<string | null>(null);
|
||||
@@ -909,4 +947,42 @@ export class MangaListComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
canEditComment(comment: Comment): boolean {
|
||||
const user = this.authService.user();
|
||||
if (!user) return false;
|
||||
return comment.userId === user.id || this.authService.isAdmin();
|
||||
}
|
||||
|
||||
canDeleteComment(comment: Comment): boolean {
|
||||
const user = this.authService.user();
|
||||
if (!user) return false;
|
||||
return comment.userId === user.id || this.authService.isAdmin();
|
||||
}
|
||||
|
||||
startEditComment(mangaId: string, comment: Comment) {
|
||||
this.editingCommentId.set(comment.id);
|
||||
this.editCommentContent = comment.rawContent ?? comment.content;
|
||||
}
|
||||
|
||||
cancelCommentEdit() {
|
||||
this.editingCommentId.set(null);
|
||||
this.editCommentContent = '';
|
||||
}
|
||||
|
||||
saveCommentEdit(mangaId: string, commentId: string) {
|
||||
if (!this.editCommentContent.trim()) return;
|
||||
|
||||
this.commentsService.updateCommentOnManga(mangaId, commentId, this.editCommentContent).subscribe({
|
||||
next: (updatedComment) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[mangaId]: (this.comments()[mangaId] || []).map(c =>
|
||||
c.id === commentId ? updatedComment : c
|
||||
)
|
||||
});
|
||||
this.cancelCommentEdit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,11 +384,28 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment
|
||||
}
|
||||
<span class="comment-author">{{ comment.user.username }}</span>
|
||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||
@if (authService.isAdmin()) {
|
||||
@if (canEditComment(comment)) {
|
||||
<button (click)="startEditComment(music.id, comment)" class="btn btn-secondary btn-xs">Edit</button>
|
||||
}
|
||||
@if (canDeleteComment(comment)) {
|
||||
<button (click)="deleteComment(music.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||
}
|
||||
</div>
|
||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||
@if (editingCommentId() === comment.id) {
|
||||
<div class="comment-edit-form">
|
||||
<textarea
|
||||
[(ngModel)]="editCommentContent"
|
||||
name="editComment"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<div class="comment-edit-actions">
|
||||
<button (click)="saveCommentEdit(music.id, comment.id)" class="btn btn-primary btn-xs">Save</button>
|
||||
<button (click)="cancelCommentEdit()" class="btn btn-secondary btn-xs">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||
}
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||
@@ -793,6 +810,32 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.comment-edit-form {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-edit-form textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 2px solid var(--witch-lavender);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
background-color: var(--witch-moon);
|
||||
color: var(--witch-purple);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.comment-edit-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--witch-rose);
|
||||
}
|
||||
|
||||
.comment-edit-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
@@ -845,6 +888,8 @@ export class MusicListComponent implements OnInit {
|
||||
commentsLoading = signal<Record<string, boolean>>({});
|
||||
expandedComments = signal<Record<string, boolean>>({});
|
||||
newCommentContent: Record<string, string> = {};
|
||||
editingCommentId = signal<string | null>(null);
|
||||
editCommentContent = '';
|
||||
|
||||
// Image upload state
|
||||
newMusicImagePreview = signal<string | null>(null);
|
||||
@@ -1137,4 +1182,42 @@ export class MusicListComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
canEditComment(comment: Comment): boolean {
|
||||
const user = this.authService.user();
|
||||
if (!user) return false;
|
||||
return comment.userId === user.id || this.authService.isAdmin();
|
||||
}
|
||||
|
||||
canDeleteComment(comment: Comment): boolean {
|
||||
const user = this.authService.user();
|
||||
if (!user) return false;
|
||||
return comment.userId === user.id || this.authService.isAdmin();
|
||||
}
|
||||
|
||||
startEditComment(musicId: string, comment: Comment) {
|
||||
this.editingCommentId.set(comment.id);
|
||||
this.editCommentContent = comment.rawContent ?? comment.content;
|
||||
}
|
||||
|
||||
cancelCommentEdit() {
|
||||
this.editingCommentId.set(null);
|
||||
this.editCommentContent = '';
|
||||
}
|
||||
|
||||
saveCommentEdit(musicId: string, commentId: string) {
|
||||
if (!this.editCommentContent.trim()) return;
|
||||
|
||||
this.commentsService.updateCommentOnMusic(musicId, commentId, this.editCommentContent).subscribe({
|
||||
next: (updatedComment) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[musicId]: (this.comments()[musicId] || []).map(c =>
|
||||
c.id === commentId ? updatedComment : c
|
||||
)
|
||||
});
|
||||
this.cancelCommentEdit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -307,11 +307,28 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment } fro
|
||||
}
|
||||
<span class="comment-author">{{ comment.user.username }}</span>
|
||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||
@if (authService.isAdmin()) {
|
||||
@if (canEditComment(comment)) {
|
||||
<button (click)="startEditComment(show.id, comment)" class="btn btn-secondary btn-xs">Edit</button>
|
||||
}
|
||||
@if (canDeleteComment(comment)) {
|
||||
<button (click)="deleteComment(show.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||
}
|
||||
</div>
|
||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||
@if (editingCommentId() === comment.id) {
|
||||
<div class="comment-edit-form">
|
||||
<textarea
|
||||
[(ngModel)]="editCommentContent"
|
||||
name="editComment"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<div class="comment-edit-actions">
|
||||
<button (click)="saveCommentEdit(show.id, comment.id)" class="btn btn-primary btn-xs">Save</button>
|
||||
<button (click)="cancelCommentEdit()" class="btn btn-secondary btn-xs">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="comment-content" [innerHTML]="sanitizeService.sanitizeHtml(comment.content)"></div>
|
||||
}
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||
@@ -597,6 +614,25 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment } fro
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.comment-edit-form {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.comment-edit-form textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.comment-edit-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
@@ -647,6 +683,8 @@ export class ShowsListComponent implements OnInit {
|
||||
commentsLoading = signal<Record<string, boolean>>({});
|
||||
expandedComments = signal<Record<string, boolean>>({});
|
||||
newCommentContent: Record<string, string> = {};
|
||||
editingCommentId = signal<string | null>(null);
|
||||
editCommentContent = '';
|
||||
|
||||
newShowImagePreview = signal<string | null>(null);
|
||||
editShowImagePreview = signal<string | null>(null);
|
||||
@@ -914,4 +952,42 @@ export class ShowsListComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
canEditComment(comment: Comment): boolean {
|
||||
const user = this.authService.user();
|
||||
if (!user) return false;
|
||||
return comment.userId === user.id || this.authService.isAdmin();
|
||||
}
|
||||
|
||||
canDeleteComment(comment: Comment): boolean {
|
||||
const user = this.authService.user();
|
||||
if (!user) return false;
|
||||
return comment.userId === user.id || this.authService.isAdmin();
|
||||
}
|
||||
|
||||
startEditComment(showId: string, comment: Comment) {
|
||||
this.editingCommentId.set(comment.id);
|
||||
this.editCommentContent = comment.rawContent ?? comment.content;
|
||||
}
|
||||
|
||||
cancelCommentEdit() {
|
||||
this.editingCommentId.set(null);
|
||||
this.editCommentContent = '';
|
||||
}
|
||||
|
||||
saveCommentEdit(showId: string, commentId: string) {
|
||||
if (!this.editCommentContent.trim()) return;
|
||||
|
||||
this.commentsService.updateCommentOnShow(showId, commentId, this.editCommentContent).subscribe({
|
||||
next: (updatedComment) => {
|
||||
this.comments.set({
|
||||
...this.comments(),
|
||||
[showId]: (this.comments()[showId] || []).map(c =>
|
||||
c.id === commentId ? updatedComment : c
|
||||
)
|
||||
});
|
||||
this.cancelCommentEdit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user