Compare commits

...

6 Commits

8 changed files with 72 additions and 15 deletions

View File

@@ -0,0 +1,21 @@
import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Öffentliche, schreibgeschützte Song-Liste für das Spiel (GuessInput etc.).
// Kein Auth, nur Lesen der nötigsten Felder.
export async function GET() {
const songs = await prisma.song.findMany({
orderBy: { createdAt: 'desc' },
select: {
id: true,
title: true,
artist: true,
},
});
return NextResponse.json(songs);
}

View File

@@ -103,9 +103,11 @@ export async function GET(request: NextRequest) {
visibleSongs = songs.filter(song => { visibleSongs = songs.filter(song => {
const songGenreIds = song.genres.map(g => g.id); const songGenreIds = song.genres.map(g => g.id);
// `song.specials` ist hier ein Array von SpecialSong mit Relation `special`, // `song.specials` ist hier ein Array von SpecialSong mit Relation `special`.
// wir nutzen konsistent die Special-ID. // Es kann theoretisch verwaiste Einträge ohne `special` geben → defensiv optional chainen.
const songSpecialIds = song.specials.map(ss => ss.special.id); const songSpecialIds = song.specials
.map(ss => ss.special?.id)
.filter((id): id is number => typeof id === 'number');
// Songs ohne Genres/Specials sind immer sichtbar // Songs ohne Genres/Specials sind immer sichtbar
if (songGenreIds.length === 0 && songSpecialIds.length === 0) { if (songGenreIds.length === 0 && songSpecialIds.length === 0) {
@@ -131,7 +133,10 @@ export async function GET(request: NextRequest) {
activations: song.puzzles.length, activations: song.puzzles.length,
puzzles: song.puzzles, puzzles: song.puzzles,
genres: song.genres, genres: song.genres,
specials: song.specials.map(ss => ss.special), // Nur Specials mit existierender Relation durchreichen, um undefinierte Einträge zu vermeiden.
specials: song.specials
.map(ss => ss.special)
.filter((s): s is any => !!s),
averageRating: song.averageRating, averageRating: song.averageRating,
ratingCount: song.ratingCount, ratingCount: song.ratingCount,
excludeFromGlobal: song.excludeFromGlobal, excludeFromGlobal: song.excludeFromGlobal,

View File

@@ -1037,7 +1037,7 @@ export default function CuratorPage() {
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
{genres {genres
.filter(g => curatorInfo?.genreIds.includes(g.id)) .filter(g => curatorInfo?.genreIds?.includes(g.id))
.map(genre => ( .map(genre => (
<label <label
key={genre.id} key={genre.id}
@@ -1074,7 +1074,7 @@ export default function CuratorPage() {
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
{song.genres {song.genres
.filter( .filter(
g => !curatorInfo?.genreIds.includes(g.id) g => !curatorInfo?.genreIds?.includes(g.id)
) )
.map(g => ( .map(g => (
<span <span

View File

@@ -391,6 +391,13 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
} }
}; };
// Aktuelle Attempt-Anzeige:
// - Während des Spiels: nächster Versuch = guesses.length + 1
// - Nach Spielende (gelöst oder verloren): letzter Versuch = guesses.length
const currentAttempt = (gameState.isSolved || gameState.isFailed)
? gameState.guesses.length
: gameState.guesses.length + 1;
return ( return (
<div className="container"> <div className="container">
<header className="header"> <header className="header">
@@ -403,7 +410,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
<main className="game-board"> <main className="game-board">
<div style={{ borderBottom: '1px solid #e5e7eb', paddingBottom: '1rem' }}> <div style={{ borderBottom: '1px solid #e5e7eb', paddingBottom: '1rem' }}>
<div id="tour-status" className="status-bar"> <div id="tour-status" className="status-bar">
<span>{t('attempt')} {gameState.guesses.length + 1} / {maxAttempts}</span> <span>{t('attempt')} {currentAttempt} / {maxAttempts}</span>
<span>{unlockedSeconds}s {t('unlocked')}</span> <span>{unlockedSeconds}s {t('unlocked')}</span>
</div> </div>
@@ -512,14 +519,20 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
</audio> </audio>
</div> </div>
<div style={{ marginBottom: '1rem' }}> <div style={{ marginBottom: '1.25rem' }}>
<StarRating onRate={handleRatingSubmit} hasRated={hasRated} /> <StarRating onRate={handleRatingSubmit} hasRated={hasRated} />
</div> </div>
<div style={{ marginBottom: '1.25rem', textAlign: 'center' }}>
<p style={{ fontSize: '0.85rem', color: 'var(--muted-foreground)', marginBottom: '0.5rem' }}>
{t('shareExplanation')}
</p>
<button onClick={handleShare} className="btn-primary">
{shareText}
</button>
</div>
{statistics && <Statistics statistics={statistics} />} {statistics && <Statistics statistics={statistics} />}
<button onClick={handleShare} className="btn-primary" style={{ marginTop: '1rem' }}>
{shareText}
</button>
</div> </div>
)} )}
</main> </main>

View File

@@ -22,9 +22,25 @@ export default function GuessInput({ onGuess, disabled }: GuessInputProps) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
useEffect(() => { useEffect(() => {
fetch('/api/songs') fetch('/api/public-songs')
.then(res => res.json()) .then(res => {
.then(data => setSongs(data)); if (!res.ok) {
throw new Error(`Failed to load songs: ${res.status}`);
}
return res.json();
})
.then(data => {
if (Array.isArray(data)) {
setSongs(data);
} else {
console.error('Unexpected songs payload in GuessInput:', data);
setSongs([]);
}
})
.catch(err => {
console.error('Error loading songs for GuessInput:', err);
setSongs([]);
});
}, []); }, []);
useEffect(() => { useEffect(() => {

View File

@@ -41,6 +41,7 @@
"comeBackTomorrow": "Komm morgen zurück für ein neues Lied.", "comeBackTomorrow": "Komm morgen zurück für ein neues Lied.",
"theSongWas": "Das Lied war:", "theSongWas": "Das Lied war:",
"score": "Punkte", "score": "Punkte",
"shareExplanation": "Teile dein Ergebnis mit Freund:innen so hilfst du, Hördle bekannter zu machen.",
"scoreBreakdown": "Punkteaufschlüsselung", "scoreBreakdown": "Punkteaufschlüsselung",
"albumCover": "Album-Cover", "albumCover": "Album-Cover",
"released": "Veröffentlicht", "released": "Veröffentlicht",

View File

@@ -41,6 +41,7 @@
"comeBackTomorrow": "Come back tomorrow for a new song.", "comeBackTomorrow": "Come back tomorrow for a new song.",
"theSongWas": "The song was:", "theSongWas": "The song was:",
"score": "Score", "score": "Score",
"shareExplanation": "Share your result with friends your support helps Hördle grow.",
"scoreBreakdown": "Score Breakdown", "scoreBreakdown": "Score Breakdown",
"albumCover": "Album Cover", "albumCover": "Album Cover",
"released": "Released", "released": "Released",

View File

@@ -1,6 +1,6 @@
{ {
"name": "hoerdle", "name": "hoerdle",
"version": "0.1.4.11", "version": "0.1.5.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",