Füge Puzzle-Kontext zu Kommentaren hinzu und verbessere Sichtbarkeit neuer Kommentare
- Puzzle-Kontext: Hördle #, Genre/Special, Titel werden jetzt angezeigt - API erweitert: puzzleNumber wird berechnet, special-Informationen inkludiert - Badge für neue Kommentare: zeigt Anzahl ungelesener Kommentare - Verbesserte Kommentar-Anzeige mit vollständigem Rätsel-Kontext - UI-Anpassungen: nur Badge für neue Kommentare, keine übermäßige Hervorhebung
This commit is contained in:
@@ -44,6 +44,12 @@ export async function GET(request: NextRequest) {
|
||||
id: true,
|
||||
name: true
|
||||
}
|
||||
},
|
||||
special: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,24 +63,68 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
});
|
||||
|
||||
// Format the response
|
||||
const formattedComments = comments.map(recipient => ({
|
||||
id: recipient.comment.id,
|
||||
message: recipient.comment.message,
|
||||
createdAt: recipient.comment.createdAt,
|
||||
readAt: recipient.readAt,
|
||||
puzzle: {
|
||||
id: recipient.comment.puzzle.id,
|
||||
date: recipient.comment.puzzle.date,
|
||||
song: {
|
||||
title: recipient.comment.puzzle.song.title,
|
||||
artist: recipient.comment.puzzle.song.artist
|
||||
},
|
||||
genre: recipient.comment.puzzle.genre ? {
|
||||
id: recipient.comment.puzzle.genre.id,
|
||||
name: recipient.comment.puzzle.genre.name
|
||||
} : null
|
||||
// Format the response with puzzle context
|
||||
const formattedComments = await Promise.all(comments.map(async (recipient) => {
|
||||
const puzzle = recipient.comment.puzzle;
|
||||
|
||||
// Calculate puzzle number
|
||||
let puzzleNumber = 0;
|
||||
if (puzzle.specialId) {
|
||||
// Special puzzle
|
||||
puzzleNumber = await prisma.dailyPuzzle.count({
|
||||
where: {
|
||||
specialId: puzzle.specialId,
|
||||
date: {
|
||||
lte: puzzle.date
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (puzzle.genreId) {
|
||||
// Genre puzzle
|
||||
puzzleNumber = await prisma.dailyPuzzle.count({
|
||||
where: {
|
||||
genreId: puzzle.genreId,
|
||||
date: {
|
||||
lte: puzzle.date
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Global puzzle
|
||||
puzzleNumber = await prisma.dailyPuzzle.count({
|
||||
where: {
|
||||
genreId: null,
|
||||
specialId: null,
|
||||
date: {
|
||||
lte: puzzle.date
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: recipient.comment.id,
|
||||
message: recipient.comment.message,
|
||||
createdAt: recipient.comment.createdAt,
|
||||
readAt: recipient.readAt,
|
||||
puzzle: {
|
||||
id: puzzle.id,
|
||||
date: puzzle.date,
|
||||
puzzleNumber: puzzleNumber,
|
||||
song: {
|
||||
title: puzzle.song.title,
|
||||
artist: puzzle.song.artist
|
||||
},
|
||||
genre: puzzle.genre ? {
|
||||
id: puzzle.genre.id,
|
||||
name: puzzle.genre.name
|
||||
} : null,
|
||||
special: puzzle.special ? {
|
||||
id: puzzle.special.id,
|
||||
name: puzzle.special.name
|
||||
} : null
|
||||
}
|
||||
};
|
||||
}));
|
||||
|
||||
return NextResponse.json(formattedComments);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
|
||||
interface Genre {
|
||||
id: number;
|
||||
@@ -45,6 +45,7 @@ interface CuratorComment {
|
||||
puzzle: {
|
||||
id: number;
|
||||
date: string;
|
||||
puzzleNumber: number;
|
||||
song: {
|
||||
title: string;
|
||||
artist: string;
|
||||
@@ -53,6 +54,10 @@ interface CuratorComment {
|
||||
id: number;
|
||||
name: any;
|
||||
} | null;
|
||||
special: {
|
||||
id: number;
|
||||
name: any;
|
||||
} | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,6 +82,7 @@ function getCuratorUploadHeaders() {
|
||||
|
||||
export default function CuratorPageClient() {
|
||||
const t = useTranslations('Curator');
|
||||
const tNav = useTranslations('Navigation');
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
@@ -713,28 +719,52 @@ export default function CuratorPageClient() {
|
||||
)}
|
||||
|
||||
{/* Comments Section */}
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
|
||||
<h2 style={{ fontSize: '1.25rem', marginBottom: 0 }}>
|
||||
{t('commentsTitle')} ({comments.length})
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowComments(!showComments);
|
||||
}}
|
||||
style={{
|
||||
padding: '0.4rem 0.8rem',
|
||||
borderRadius: '0.25rem',
|
||||
border: '1px solid #d1d5db',
|
||||
background: showComments ? '#3b82f6' : '#fff',
|
||||
color: showComments ? '#fff' : '#000',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
>
|
||||
{showComments ? t('hideComments') : t('showComments')}
|
||||
</button>
|
||||
</div>
|
||||
{(() => {
|
||||
const unreadCount = comments.filter(c => !c.readAt).length;
|
||||
const hasUnread = unreadCount > 0;
|
||||
|
||||
return (
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: '0.75rem' }}>
|
||||
<h2 style={{ fontSize: '1.25rem', marginBottom: 0 }}>
|
||||
{t('commentsTitle')} ({comments.length})
|
||||
</h2>
|
||||
{hasUnread && (
|
||||
<span style={{
|
||||
padding: '0.25rem 0.75rem',
|
||||
borderRadius: '1rem',
|
||||
background: '#ef4444',
|
||||
color: 'white',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 'bold',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.25rem',
|
||||
verticalAlign: 'baseline',
|
||||
lineHeight: '1'
|
||||
}}>
|
||||
{unreadCount} {t('newComments')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowComments(!showComments);
|
||||
}}
|
||||
style={{
|
||||
padding: '0.4rem 0.8rem',
|
||||
borderRadius: '0.25rem',
|
||||
border: '1px solid #d1d5db',
|
||||
background: showComments ? '#3b82f6' : '#fff',
|
||||
color: showComments ? '#fff' : '#000',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
>
|
||||
{showComments ? t('hideComments') : t('showComments')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showComments && (
|
||||
<>
|
||||
@@ -750,8 +780,23 @@ export default function CuratorPageClient() {
|
||||
? comment.puzzle.genre.name
|
||||
: comment.puzzle.genre.name?.de ?? comment.puzzle.genre.name?.en
|
||||
: null;
|
||||
const specialName = comment.puzzle.special
|
||||
? typeof comment.puzzle.special.name === 'string'
|
||||
? comment.puzzle.special.name
|
||||
: comment.puzzle.special.name?.de ?? comment.puzzle.special.name?.en
|
||||
: null;
|
||||
const isRead = comment.readAt !== null;
|
||||
|
||||
// Determine category label
|
||||
let categoryLabel = '';
|
||||
if (specialName) {
|
||||
categoryLabel = `★ ${specialName}`;
|
||||
} else if (genreName) {
|
||||
categoryLabel = genreName;
|
||||
} else {
|
||||
categoryLabel = tNav('global');
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={comment.id}
|
||||
@@ -784,20 +829,18 @@ export default function CuratorPageClient() {
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem', alignItems: 'center' }}>
|
||||
<div>
|
||||
<strong style={{ fontSize: '0.9rem' }}>
|
||||
{t('commentFromPuzzle')} #{comment.puzzle.id}
|
||||
<strong style={{ fontSize: '0.95rem' }}>
|
||||
Hördle #{comment.puzzle.puzzleNumber}
|
||||
</strong>
|
||||
{genreName && (
|
||||
<span style={{ marginLeft: '0.5rem', fontSize: '0.85rem', color: '#6b7280' }}>
|
||||
({t('commentGenre')}: {genreName})
|
||||
</span>
|
||||
)}
|
||||
<span style={{ marginLeft: '0.5rem', fontSize: '0.85rem', color: '#6b7280' }}>
|
||||
({categoryLabel})
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: '0.8rem', color: '#6b7280' }}>
|
||||
{new Date(comment.createdAt).toLocaleDateString()} {new Date(comment.createdAt).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ marginBottom: '0.5rem', fontSize: '0.85rem', color: '#6b7280' }}>
|
||||
<div style={{ marginBottom: '0.75rem', fontSize: '0.9rem', fontWeight: '500' }}>
|
||||
{comment.puzzle.song.title} - {comment.puzzle.song.artist}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.9rem', whiteSpace: 'pre-wrap', marginBottom: '0.5rem' }}>
|
||||
@@ -832,7 +875,9 @@ export default function CuratorPageClient() {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
})()}
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1.25rem', marginBottom: '0.75rem' }}>{t('uploadSectionTitle')}</h2>
|
||||
|
||||
@@ -251,7 +251,8 @@
|
||||
"unreadComment": "Ungelesen",
|
||||
"archiveComment": "Archivieren",
|
||||
"archiveCommentConfirm": "Möchtest du diesen Kommentar wirklich archivieren?",
|
||||
"archiveCommentError": "Fehler beim Archivieren des Kommentars."
|
||||
"archiveCommentError": "Fehler beim Archivieren des Kommentars.",
|
||||
"newComments": "neu"
|
||||
},
|
||||
"About": {
|
||||
"title": "Über Hördle & Impressum",
|
||||
|
||||
@@ -251,7 +251,8 @@
|
||||
"unreadComment": "Unread",
|
||||
"archiveComment": "Archive",
|
||||
"archiveCommentConfirm": "Do you really want to archive this comment?",
|
||||
"archiveCommentError": "Error archiving comment."
|
||||
"archiveCommentError": "Error archiving comment.",
|
||||
"newComments": "new"
|
||||
},
|
||||
"About": {
|
||||
"title": "About Hördle & Imprint",
|
||||
|
||||
Reference in New Issue
Block a user