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, id: true,
name: true name: true
} }
},
special: {
select: {
id: true,
name: true
}
} }
} }
} }
@@ -57,24 +63,68 @@ export async function GET(request: NextRequest) {
} }
}); });
// Format the response // Format the response with puzzle context
const formattedComments = comments.map(recipient => ({ const formattedComments = await Promise.all(comments.map(async (recipient) => {
id: recipient.comment.id, const puzzle = recipient.comment.puzzle;
message: recipient.comment.message,
createdAt: recipient.comment.createdAt, // Calculate puzzle number
readAt: recipient.readAt, let puzzleNumber = 0;
puzzle: { if (puzzle.specialId) {
id: recipient.comment.puzzle.id, // Special puzzle
date: recipient.comment.puzzle.date, puzzleNumber = await prisma.dailyPuzzle.count({
song: { where: {
title: recipient.comment.puzzle.song.title, specialId: puzzle.specialId,
artist: recipient.comment.puzzle.song.artist date: {
}, lte: puzzle.date
genre: recipient.comment.puzzle.genre ? { }
id: recipient.comment.puzzle.genre.id, }
name: recipient.comment.puzzle.genre.name });
} : null } 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); return NextResponse.json(formattedComments);

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations, useLocale } from 'next-intl';
interface Genre { interface Genre {
id: number; id: number;
@@ -45,6 +45,7 @@ interface CuratorComment {
puzzle: { puzzle: {
id: number; id: number;
date: string; date: string;
puzzleNumber: number;
song: { song: {
title: string; title: string;
artist: string; artist: string;
@@ -53,6 +54,10 @@ interface CuratorComment {
id: number; id: number;
name: any; name: any;
} | null; } | null;
special: {
id: number;
name: any;
} | null;
}; };
} }
@@ -77,6 +82,7 @@ function getCuratorUploadHeaders() {
export default function CuratorPageClient() { export default function CuratorPageClient() {
const t = useTranslations('Curator'); const t = useTranslations('Curator');
const tNav = useTranslations('Navigation');
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [isAuthenticated, setIsAuthenticated] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false);
@@ -713,28 +719,52 @@ export default function CuratorPageClient() {
)} )}
{/* Comments Section */} {/* Comments Section */}
<section style={{ marginBottom: '2rem' }}> {(() => {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}> const unreadCount = comments.filter(c => !c.readAt).length;
<h2 style={{ fontSize: '1.25rem', marginBottom: 0 }}> const hasUnread = unreadCount > 0;
{t('commentsTitle')} ({comments.length})
</h2> return (
<button <section style={{ marginBottom: '2rem' }}>
onClick={() => { <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
setShowComments(!showComments); <div style={{ display: 'flex', alignItems: 'baseline', gap: '0.75rem' }}>
}} <h2 style={{ fontSize: '1.25rem', marginBottom: 0 }}>
style={{ {t('commentsTitle')} ({comments.length})
padding: '0.4rem 0.8rem', </h2>
borderRadius: '0.25rem', {hasUnread && (
border: '1px solid #d1d5db', <span style={{
background: showComments ? '#3b82f6' : '#fff', padding: '0.25rem 0.75rem',
color: showComments ? '#fff' : '#000', borderRadius: '1rem',
cursor: 'pointer', background: '#ef4444',
fontSize: '0.9rem', color: 'white',
}} fontSize: '0.875rem',
> fontWeight: 'bold',
{showComments ? t('hideComments') : t('showComments')} display: 'inline-flex',
</button> alignItems: 'center',
</div> 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 && ( {showComments && (
<> <>
@@ -750,8 +780,23 @@ export default function CuratorPageClient() {
? comment.puzzle.genre.name ? comment.puzzle.genre.name
: comment.puzzle.genre.name?.de ?? comment.puzzle.genre.name?.en : comment.puzzle.genre.name?.de ?? comment.puzzle.genre.name?.en
: null; : 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; const isRead = comment.readAt !== null;
// Determine category label
let categoryLabel = '';
if (specialName) {
categoryLabel = `${specialName}`;
} else if (genreName) {
categoryLabel = genreName;
} else {
categoryLabel = tNav('global');
}
return ( return (
<div <div
key={comment.id} key={comment.id}
@@ -784,20 +829,18 @@ export default function CuratorPageClient() {
)} )}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem', alignItems: 'center' }}>
<div> <div>
<strong style={{ fontSize: '0.9rem' }}> <strong style={{ fontSize: '0.95rem' }}>
{t('commentFromPuzzle')} #{comment.puzzle.id} Hördle #{comment.puzzle.puzzleNumber}
</strong> </strong>
{genreName && ( <span style={{ marginLeft: '0.5rem', fontSize: '0.85rem', color: '#6b7280' }}>
<span style={{ marginLeft: '0.5rem', fontSize: '0.85rem', color: '#6b7280' }}> ({categoryLabel})
({t('commentGenre')}: {genreName}) </span>
</span>
)}
</div> </div>
<span style={{ fontSize: '0.8rem', color: '#6b7280' }}> <span style={{ fontSize: '0.8rem', color: '#6b7280' }}>
{new Date(comment.createdAt).toLocaleDateString()} {new Date(comment.createdAt).toLocaleTimeString()} {new Date(comment.createdAt).toLocaleDateString()} {new Date(comment.createdAt).toLocaleTimeString()}
</span> </span>
</div> </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} {comment.puzzle.song.title} - {comment.puzzle.song.artist}
</div> </div>
<div style={{ fontSize: '0.9rem', whiteSpace: 'pre-wrap', marginBottom: '0.5rem' }}> <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' }}> <section style={{ marginBottom: '2rem' }}>
<h2 style={{ fontSize: '1.25rem', marginBottom: '0.75rem' }}>{t('uploadSectionTitle')}</h2> <h2 style={{ fontSize: '1.25rem', marginBottom: '0.75rem' }}>{t('uploadSectionTitle')}</h2>

View File

@@ -251,7 +251,8 @@
"unreadComment": "Ungelesen", "unreadComment": "Ungelesen",
"archiveComment": "Archivieren", "archiveComment": "Archivieren",
"archiveCommentConfirm": "Möchtest du diesen Kommentar wirklich archivieren?", "archiveCommentConfirm": "Möchtest du diesen Kommentar wirklich archivieren?",
"archiveCommentError": "Fehler beim Archivieren des Kommentars." "archiveCommentError": "Fehler beim Archivieren des Kommentars.",
"newComments": "neu"
}, },
"About": { "About": {
"title": "Über Hördle & Impressum", "title": "Über Hördle & Impressum",

View File

@@ -251,7 +251,8 @@
"unreadComment": "Unread", "unreadComment": "Unread",
"archiveComment": "Archive", "archiveComment": "Archive",
"archiveCommentConfirm": "Do you really want to archive this comment?", "archiveCommentConfirm": "Do you really want to archive this comment?",
"archiveCommentError": "Error archiving comment." "archiveCommentError": "Error archiving comment.",
"newComments": "new"
}, },
"About": { "About": {
"title": "About Hördle & Imprint", "title": "About Hördle & Imprint",