Füge Archivierungs-Funktion für Kommentare hinzu und fixe initiales Laden

- Archivierungs-Funktionalität: Kuratoren können Kommentare archivieren
- archived-Flag in CuratorCommentRecipient hinzugefügt
- API-Route für Archivieren: /api/curator-comments/[id]/archive
- Kommentare werden beim initialen Laden automatisch abgerufen
- Archivierte Kommentare werden nicht mehr in der Liste angezeigt
- Archivieren-Button in der UI hinzugefügt
- Migration für archived-Feld
- Übersetzungen für Archivierung (DE/EN)
This commit is contained in:
Hördle Bot
2025-12-03 22:57:28 +01:00
parent 08fedf9881
commit 95bcf9ed1e
7 changed files with 123 additions and 14 deletions

View File

@@ -0,0 +1,66 @@
import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
import { requireStaffAuth } from '@/lib/auth';
const prisma = new PrismaClient();
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
// Require curator authentication
const { error, context } = await requireStaffAuth(request);
if (error || !context) {
return error!;
}
// Only curators can archive comments
if (context.role !== 'curator') {
return NextResponse.json(
{ error: 'Only curators can archive comments' },
{ status: 403 }
);
}
try {
const { id } = await params;
const commentId = Number(id);
const curatorId = context.curator.id;
// Verify that this comment belongs to this curator
const recipient = await prisma.curatorCommentRecipient.findUnique({
where: {
commentId_curatorId: {
commentId: commentId,
curatorId: curatorId
}
}
});
if (!recipient) {
return NextResponse.json(
{ error: 'Comment not found or access denied' },
{ status: 404 }
);
}
// Update archived flag
await prisma.curatorCommentRecipient.update({
where: {
id: recipient.id
},
data: {
archived: true
}
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error archiving comment:', error);
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}

View File

@@ -22,10 +22,11 @@ export async function GET(request: NextRequest) {
try {
const curatorId = context.curator.id;
// Get all comments for this curator, ordered by creation date (newest first)
// Get all non-archived comments for this curator, ordered by creation date (newest first)
const comments = await prisma.curatorCommentRecipient.findMany({
where: {
curatorId: curatorId
curatorId: curatorId,
archived: false
},
include: {
comment: {

View File

@@ -133,16 +133,11 @@ export default function CuratorPageClient() {
}
}, []);
useEffect(() => {
if (showComments && isAuthenticated) {
fetchComments();
}
}, [showComments, isAuthenticated]);
const bootstrapCuratorData = async () => {
try {
setLoading(true);
await Promise.all([fetchCuratorInfo(), fetchSongs(), fetchGenres(), fetchSpecials()]);
await Promise.all([fetchCuratorInfo(), fetchSongs(), fetchGenres(), fetchSpecials(), fetchComments()]);
} finally {
setLoading(false);
}
@@ -184,6 +179,24 @@ export default function CuratorPageClient() {
}
};
const archiveComment = async (commentId: number) => {
try {
const res = await fetch(`/api/curator-comments/${commentId}/archive`, {
method: 'POST',
headers: getCuratorAuthHeaders(),
});
if (res.ok) {
// Remove comment from local state (archived comments are not shown)
setComments(comments.filter(c => c.id !== commentId));
} else {
setMessage(t('archiveCommentError'));
}
} catch (error) {
console.error('Error archiving comment:', error);
setMessage(t('archiveCommentError'));
}
};
const fetchCuratorInfo = async () => {
const res = await fetch('/api/curator/me', {
headers: getCuratorAuthHeaders(),
@@ -708,9 +721,6 @@ export default function CuratorPageClient() {
<button
onClick={() => {
setShowComments(!showComments);
if (!showComments && comments.length === 0) {
fetchComments();
}
}}
style={{
padding: '0.4rem 0.8rem',
@@ -790,9 +800,31 @@ export default function CuratorPageClient() {
<div style={{ marginBottom: '0.5rem', fontSize: '0.85rem', color: '#6b7280' }}>
{comment.puzzle.song.title} - {comment.puzzle.song.artist}
</div>
<div style={{ fontSize: '0.9rem', whiteSpace: 'pre-wrap' }}>
<div style={{ fontSize: '0.9rem', whiteSpace: 'pre-wrap', marginBottom: '0.5rem' }}>
{comment.message}
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem', marginTop: '0.5rem' }}>
<button
onClick={(e) => {
e.stopPropagation();
if (confirm(t('archiveCommentConfirm'))) {
archiveComment(comment.id);
}
}}
style={{
padding: '0.4rem 0.8rem',
borderRadius: '0.25rem',
border: '1px solid #d1d5db',
background: '#fff',
color: '#6b7280',
cursor: 'pointer',
fontSize: '0.85rem',
}}
title={t('archiveComment')}
>
{t('archiveComment')}
</button>
</div>
</div>
);
})}

View File

@@ -248,7 +248,10 @@
"loadCommentsError": "Fehler beim Laden der Kommentare.",
"commentFromPuzzle": "Kommentar zu Puzzle",
"commentGenre": "Genre",
"unreadComment": "Ungelesen"
"unreadComment": "Ungelesen",
"archiveComment": "Archivieren",
"archiveCommentConfirm": "Möchtest du diesen Kommentar wirklich archivieren?",
"archiveCommentError": "Fehler beim Archivieren des Kommentars."
},
"About": {
"title": "Über Hördle & Impressum",

View File

@@ -248,7 +248,10 @@
"loadCommentsError": "Error loading comments.",
"commentFromPuzzle": "Comment from puzzle",
"commentGenre": "Genre",
"unreadComment": "Unread"
"unreadComment": "Unread",
"archiveComment": "Archive",
"archiveCommentConfirm": "Do you really want to archive this comment?",
"archiveCommentError": "Error archiving comment."
},
"About": {
"title": "About Hördle & Imprint",

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "CuratorCommentRecipient" ADD COLUMN "archived" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -176,6 +176,7 @@ model CuratorCommentRecipient {
curatorId Int
curator Curator @relation(fields: [curatorId], references: [id], onDelete: Cascade)
readAt DateTime?
archived Boolean @default(false)
@@unique([commentId, curatorId])
@@index([curatorId])