'use client';
import { useEffect, useState } from 'react';
import AudioPlayer from './AudioPlayer';
import GuessInput from './GuessInput';
import Statistics from './Statistics';
import { useGameState } from '../lib/gameState';
import { sendGotifyNotification } from '../app/actions';
interface GameProps {
dailyPuzzle: {
id: number;
puzzleNumber: number;
audioUrl: string;
songId: number;
title: string;
artist: string;
coverImage: string | null;
startTime?: number;
} | null;
genre?: string | null;
isSpecial?: boolean;
maxAttempts?: number;
unlockSteps?: number[];
}
const DEFAULT_UNLOCK_STEPS = [2, 4, 7, 11, 16, 30, 60];
export default function Game({ dailyPuzzle, genre = null, isSpecial = false, maxAttempts = 7, unlockSteps = DEFAULT_UNLOCK_STEPS }: GameProps) {
const { gameState, statistics, addGuess } = useGameState(genre, maxAttempts);
const [hasWon, setHasWon] = useState(false);
const [hasLost, setHasLost] = useState(false);
const [shareText, setShareText] = useState('🔗 Share');
const [lastAction, setLastAction] = useState<'GUESS' | 'SKIP' | null>(null);
const [isProcessingGuess, setIsProcessingGuess] = useState(false);
const [timeUntilNext, setTimeUntilNext] = useState('');
useEffect(() => {
const updateCountdown = () => {
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setHours(24, 0, 0, 0);
const diff = tomorrow.getTime() - now.getTime();
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
setTimeUntilNext(`${hours}h ${minutes}m`);
};
updateCountdown();
const interval = setInterval(updateCountdown, 1000); // Update every second to be accurate
return () => clearInterval(interval);
}, []);
useEffect(() => {
if (gameState && dailyPuzzle) {
setHasWon(gameState.isSolved);
setHasLost(gameState.isFailed);
}
}, [gameState, dailyPuzzle]);
useEffect(() => {
setLastAction(null);
}, [dailyPuzzle?.id]);
if (!dailyPuzzle) return (
No Puzzle Available
Could not generate a daily puzzle.
Please ensure there are songs in the database{genre ? ` for genre "${genre}"` : ''}.
Go to Admin Dashboard
);
if (!gameState) return Loading state...
;
const handleGuess = (song: any) => {
if (isProcessingGuess) return; // Prevent multiple guesses
setIsProcessingGuess(true);
setLastAction('GUESS');
if (song.id === dailyPuzzle.songId) {
addGuess(song.title, true);
setHasWon(true);
sendGotifyNotification(gameState.guesses.length + 1, 'won', dailyPuzzle.id, genre);
} else {
addGuess(song.title, false);
if (gameState.guesses.length + 1 >= maxAttempts) {
setHasLost(true);
setHasWon(false); // Ensure won is false
sendGotifyNotification(maxAttempts, 'lost', dailyPuzzle.id, genre);
}
}
// Reset after a short delay to allow UI update
setTimeout(() => setIsProcessingGuess(false), 500);
};
const handleSkip = () => {
setLastAction('SKIP');
addGuess("SKIPPED", false);
};
const handleGiveUp = () => {
setLastAction('SKIP');
addGuess("SKIPPED", false);
setHasLost(true);
setHasWon(false);
sendGotifyNotification(maxAttempts, 'lost', dailyPuzzle.id, genre);
};
const unlockedSeconds = unlockSteps[Math.min(gameState.guesses.length, unlockSteps.length - 1)];
const handleShare = async () => {
let emojiGrid = '';
const totalGuesses = maxAttempts;
// 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 if (gameState.guesses[i] === 'SKIPPED') {
// Skipped
emojiGrid += '⬛';
} else {
// Wrong guess
emojiGrid += '🟥';
}
} else {
// Unused attempts
emojiGrid += '⬜';
}
}
const speaker = hasWon ? '🔉' : '🔇';
const genreText = genre ? `Genre: ${genre}\n` : '';
// Generate URL with genre/special path
let shareUrl = 'https://hoerdle.elpatron.me';
if (genre) {
if (isSpecial) {
shareUrl += `/special/${encodeURIComponent(genre)}`;
} else {
shareUrl += `/${encodeURIComponent(genre)}`;
}
}
const text = `Hördle #${dailyPuzzle.puzzleNumber}\n${genreText}\n${speaker}${emojiGrid}\n\n#Hördle #Music\n\n${shareUrl}`;
// Try native Web Share API only on mobile devices
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (isMobile && navigator.share) {
try {
await navigator.share({
title: `Hördle #${dailyPuzzle.puzzleNumber}`,
text: text,
});
setShareText('✓ Shared!');
setTimeout(() => setShareText('🔗 Share'), 2000);
return;
} catch (err) {
// User cancelled or error - fall through to clipboard
if ((err as Error).name !== 'AbortError') {
console.error('Share failed:', err);
}
}
}
// Fallback: Copy to clipboard
try {
await navigator.clipboard.writeText(text);
setShareText('✓ Copied!');
setTimeout(() => setShareText('🔗 Share'), 2000);
} catch (err) {
console.error('Clipboard failed:', err);
setShareText('✗ Failed');
setTimeout(() => setShareText('🔗 Share'), 2000);
}
};
return (
Attempt {gameState.guesses.length + 1} / {maxAttempts}
{unlockedSeconds}s unlocked
{gameState.guesses.map((guess, i) => {
const isCorrect = hasWon && i === gameState.guesses.length - 1;
return (
#{i + 1}
{isCorrect ? 'Correct!' : guess}
);
})}
{!hasWon && !hasLost && (
<>
{gameState.guesses.length < 6 ? (
) : (
)}
>
)}
{hasWon && (
You won!
Come back tomorrow for a new song.
{/* Song Details */}
{dailyPuzzle.coverImage && (

)}
{dailyPuzzle.title}
{dailyPuzzle.artist}
{statistics &&
}
)}
{hasLost && (
Game Over
The song was:
{/* Song Details */}
{dailyPuzzle.coverImage && (

)}
{dailyPuzzle.title}
{dailyPuzzle.artist}
{statistics &&
}
)}
);
}