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,
|
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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user