'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, submitRating } 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(''); const [hasRated, setHasRated] = useState(false); 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); } }; useEffect(() => { if (dailyPuzzle) { const ratedPuzzles = JSON.parse(localStorage.getItem('hoerdle_rated_puzzles') || '[]'); if (ratedPuzzles.includes(dailyPuzzle.id)) { setHasRated(true); } else { setHasRated(false); } } }, [dailyPuzzle]); const handleRatingSubmit = async (rating: number) => { if (!dailyPuzzle) return; try { await submitRating(dailyPuzzle.songId, rating, genre); setHasRated(true); // Persist to localStorage const ratedPuzzles = JSON.parse(localStorage.getItem('hoerdle_rated_puzzles') || '[]'); if (!ratedPuzzles.includes(dailyPuzzle.id)) { ratedPuzzles.push(dailyPuzzle.id); localStorage.setItem('hoerdle_rated_puzzles', JSON.stringify(ratedPuzzles)); } } catch (error) { console.error('Failed to submit rating', error); } }; return (

Hördle #{dailyPuzzle.puzzleNumber}{genre ? ` / ${genre}` : ''}

Next puzzle in: {timeUntilNext}
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 && ( Album Cover )}

{dailyPuzzle.title}

{dailyPuzzle.artist}

{/* Rating Component */}
{statistics && }
)} {hasLost && (

Game Over

The song was:

{/* Song Details */}
{dailyPuzzle.coverImage && ( Album Cover )}

{dailyPuzzle.title}

{dailyPuzzle.artist}

{/* Rating Component */}
{statistics && }
)}
); } function StarRating({ onRate, hasRated }: { onRate: (rating: number) => void, hasRated: boolean }) { const [hover, setHover] = useState(0); const [rating, setRating] = useState(0); if (hasRated) { return
Thanks for rating!
; } return (
Rate this song:
{[...Array(5)].map((_, index) => { const ratingValue = index + 1; return ( ); })}
); }