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:
Hördle Bot
2025-12-03 23:09:45 +01:00
parent 95bcf9ed1e
commit 0054facbe7
4 changed files with 148 additions and 51 deletions

View File

@@ -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);

View File

@@ -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>

View File

@@ -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",

View File

@@ -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",