Files
hoerdle/components/Game.tsx
2025-11-21 12:25:19 +01:00

146 lines
5.2 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import AudioPlayer from './AudioPlayer';
import GuessInput from './GuessInput';
import { useGameState } from '../lib/gameState';
interface GameProps {
dailyPuzzle: {
id: number;
audioUrl: string;
songId: number;
} | null;
}
const UNLOCK_STEPS = [2, 4, 7, 11, 16, 30];
export default function Game({ dailyPuzzle }: GameProps) {
const { gameState, addGuess } = useGameState();
const [hasWon, setHasWon] = useState(false);
const [hasLost, setHasLost] = useState(false);
const [shareText, setShareText] = useState('Share Result');
useEffect(() => {
if (gameState && dailyPuzzle) {
setHasWon(gameState.isSolved);
setHasLost(gameState.isFailed);
}
}, [gameState, dailyPuzzle]);
if (!dailyPuzzle) return <div>Loading puzzle...</div>;
if (!gameState) return <div>Loading state...</div>;
const handleGuess = (song: any) => {
if (song.id === dailyPuzzle.songId) {
addGuess(song.title, true);
setHasWon(true);
} else {
addGuess(song.title, false);
if (gameState.guesses.length + 1 >= 6) {
setHasLost(true);
}
}
};
const unlockedSeconds = UNLOCK_STEPS[Math.min(gameState.guesses.length, 5)];
const handleShare = () => {
let emojiGrid = '';
const totalGuesses = 6;
// Build the grid
for (let i = 0; i < totalGuesses; i++) {
if (i < gameState.guesses.length) {
// If this was the winning guess (last one and won)
if (hasWon && i === gameState.guesses.length - 1) {
emojiGrid += '🟩';
} else {
// Wrong or skipped
emojiGrid += '⬛';
}
} else {
// Unused attempts
emojiGrid += '⬜';
}
}
const speaker = hasWon ? '🔉' : '🔇';
const text = `Hördle #${dailyPuzzle.id}\n\n${speaker}${emojiGrid}\n\n#Hördle #Music\n\nhttps://hoerdle.elpatron.me`;
navigator.clipboard.writeText(text).then(() => {
setShareText('Copied!');
setTimeout(() => setShareText('Share Result'), 2000);
});
};
return (
<div className="container">
<header className="header">
<h1 className="title">Hördle #{dailyPuzzle.id}</h1>
</header>
<main className="game-board">
<div style={{ borderBottom: '1px solid #e5e7eb', paddingBottom: '1rem' }}>
<div className="status-bar">
<span>Attempt {gameState.guesses.length + 1} / 6</span>
<span>{unlockedSeconds}s unlocked</span>
</div>
<AudioPlayer
src={dailyPuzzle.audioUrl}
unlockedSeconds={unlockedSeconds}
/>
</div>
<div className="guess-list">
{gameState.guesses.map((guess, i) => {
const isCorrect = hasWon && i === gameState.guesses.length - 1;
return (
<div key={i} className="guess-item">
<span className="guess-number">#{i + 1}</span>
<span className={`guess-text ${guess === 'SKIPPED' ? 'skipped' : ''} ${isCorrect ? 'correct' : ''}`}>
{isCorrect ? 'Correct!' : guess}
</span>
</div>
);
})}
</div>
{!hasWon && !hasLost && (
<>
<GuessInput onGuess={handleGuess} disabled={false} />
<button
onClick={() => addGuess("SKIPPED", false)}
className="skip-button"
>
Skip (+{UNLOCK_STEPS[Math.min(gameState.guesses.length + 1, 5)] - unlockedSeconds}s)
</button>
</>
)}
{hasWon && (
<div className="message-box success">
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', marginBottom: '0.5rem' }}>You won!</h2>
<p>Come back tomorrow for a new song.</p>
<button onClick={handleShare} className="btn-primary" style={{ marginTop: '1rem' }}>
{shareText}
</button>
</div>
)}
{hasLost && (
<div className="message-box failure">
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', marginBottom: '0.5rem' }}>Game Over</h2>
<p>The song was hidden.</p>
<button onClick={handleShare} className="btn-primary" style={{ marginTop: '1rem' }}>
{shareText}
</button>
</div>
)}
</main>
</div>
);
}