Compare commits

...

5 Commits

7 changed files with 70 additions and 13 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 => {
const songGenreIds = song.genres.map(g => g.id);
// `song.specials` ist hier ein Array von SpecialSong mit Relation `special`,
// wir nutzen konsistent die Special-ID.
const songSpecialIds = song.specials.map(ss => ss.special.id);
// `song.specials` ist hier ein Array von SpecialSong mit Relation `special`.
// Es kann theoretisch verwaiste Einträge ohne `special` geben → defensiv optional chainen.
const songSpecialIds = song.specials
.map(ss => ss.special?.id)
.filter((id): id is number => typeof id === 'number');
// Songs ohne Genres/Specials sind immer sichtbar
if (songGenreIds.length === 0 && songSpecialIds.length === 0) {
@@ -131,7 +133,10 @@ export async function GET(request: NextRequest) {
activations: song.puzzles.length,
puzzles: song.puzzles,
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,
ratingCount: song.ratingCount,
excludeFromGlobal: song.excludeFromGlobal,

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 (
<div className="container">
<header className="header">
@@ -403,7 +410,7 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
<main className="game-board">
<div style={{ borderBottom: '1px solid #e5e7eb', paddingBottom: '1rem' }}>
<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>
</div>
@@ -512,14 +519,20 @@ export default function Game({ dailyPuzzle, genre = null, isSpecial = false, max
</audio>
</div>
<div style={{ marginBottom: '1rem' }}>
<div style={{ marginBottom: '1.25rem' }}>
<StarRating onRate={handleRatingSubmit} hasRated={hasRated} />
</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} />}
<button onClick={handleShare} className="btn-primary" style={{ marginTop: '1rem' }}>
{shareText}
</button>
</div>
)}
</main>

View File

@@ -22,9 +22,25 @@ export default function GuessInput({ onGuess, disabled }: GuessInputProps) {
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
fetch('/api/songs')
.then(res => res.json())
.then(data => setSongs(data));
fetch('/api/public-songs')
.then(res => {
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(() => {

View File

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

View File

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

View File

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