Compare commits

...

4 Commits

2 changed files with 50 additions and 20 deletions

View File

@@ -62,6 +62,7 @@ export default async function SpecialPage({ params }: PageProps) {
<Game <Game
dailyPuzzle={dailyPuzzle} dailyPuzzle={dailyPuzzle}
genre={decodedName} genre={decodedName}
isSpecial={true}
maxAttempts={dailyPuzzle?.maxAttempts} maxAttempts={dailyPuzzle?.maxAttempts}
unlockSteps={dailyPuzzle?.unlockSteps} unlockSteps={dailyPuzzle?.unlockSteps}
/> />

View File

@@ -17,18 +17,20 @@ interface GameProps {
coverImage: string | null; coverImage: string | null;
} | null; } | null;
genre?: string | null; genre?: string | null;
isSpecial?: boolean;
maxAttempts?: number; maxAttempts?: number;
unlockSteps?: number[]; unlockSteps?: number[];
} }
const DEFAULT_UNLOCK_STEPS = [2, 4, 7, 11, 16, 30, 60]; const DEFAULT_UNLOCK_STEPS = [2, 4, 7, 11, 16, 30, 60];
export default function Game({ dailyPuzzle, genre = null, maxAttempts = 7, unlockSteps = DEFAULT_UNLOCK_STEPS }: GameProps) { export default function Game({ dailyPuzzle, genre = null, isSpecial = false, maxAttempts = 7, unlockSteps = DEFAULT_UNLOCK_STEPS }: GameProps) {
const { gameState, statistics, addGuess } = useGameState(genre, maxAttempts); const { gameState, statistics, addGuess } = useGameState(genre, maxAttempts);
const [hasWon, setHasWon] = useState(false); const [hasWon, setHasWon] = useState(false);
const [hasLost, setHasLost] = useState(false); const [hasLost, setHasLost] = useState(false);
const [shareText, setShareText] = useState('Share Result'); const [shareText, setShareText] = useState('🔗 Share');
const [lastAction, setLastAction] = useState<'GUESS' | 'SKIP' | null>(null); const [lastAction, setLastAction] = useState<'GUESS' | 'SKIP' | null>(null);
const [isProcessingGuess, setIsProcessingGuess] = useState(false);
useEffect(() => { useEffect(() => {
if (gameState && dailyPuzzle) { if (gameState && dailyPuzzle) {
@@ -52,6 +54,9 @@ export default function Game({ dailyPuzzle, genre = null, maxAttempts = 7, unloc
if (!gameState) return <div>Loading state...</div>; if (!gameState) return <div>Loading state...</div>;
const handleGuess = (song: any) => { const handleGuess = (song: any) => {
if (isProcessingGuess) return; // Prevent multiple guesses
setIsProcessingGuess(true);
setLastAction('GUESS'); setLastAction('GUESS');
if (song.id === dailyPuzzle.songId) { if (song.id === dailyPuzzle.songId) {
addGuess(song.title, true); addGuess(song.title, true);
@@ -65,6 +70,8 @@ export default function Game({ dailyPuzzle, genre = null, maxAttempts = 7, unloc
sendGotifyNotification(maxAttempts, 'lost', dailyPuzzle.id, genre); sendGotifyNotification(maxAttempts, 'lost', dailyPuzzle.id, genre);
} }
} }
// Reset after a short delay to allow UI update
setTimeout(() => setIsProcessingGuess(false), 500);
}; };
const handleSkip = () => { const handleSkip = () => {
@@ -82,7 +89,7 @@ export default function Game({ dailyPuzzle, genre = null, maxAttempts = 7, unloc
const unlockedSeconds = unlockSteps[Math.min(gameState.guesses.length, unlockSteps.length - 1)]; const unlockedSeconds = unlockSteps[Math.min(gameState.guesses.length, unlockSteps.length - 1)];
const handleShare = () => { const handleShare = async () => {
let emojiGrid = ''; let emojiGrid = '';
const totalGuesses = maxAttempts; const totalGuesses = maxAttempts;
@@ -104,26 +111,48 @@ export default function Game({ dailyPuzzle, genre = null, maxAttempts = 7, unloc
const speaker = hasWon ? '🔉' : '🔇'; const speaker = hasWon ? '🔉' : '🔇';
const genreText = genre ? `Genre: ${genre}\n` : ''; const genreText = genre ? `Genre: ${genre}\n` : '';
const text = `Hördle #${dailyPuzzle.id}\n${genreText}\n${speaker}${emojiGrid}\n\n#Hördle #Music\n\nhttps://hoerdle.elpatron.me`;
// Fallback method for copying to clipboard // Generate URL with genre/special path
const textarea = document.createElement('textarea'); let shareUrl = 'https://hoerdle.elpatron.me';
textarea.value = text; if (genre) {
textarea.style.position = 'fixed'; if (isSpecial) {
textarea.style.opacity = '0'; shareUrl += `/special/${encodeURIComponent(genre)}`;
document.body.appendChild(textarea); } else {
textarea.select(); shareUrl += `/${encodeURIComponent(genre)}`;
}
}
const text = `Hördle #${dailyPuzzle.id}\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.id}`,
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 { try {
document.execCommand('copy'); await navigator.clipboard.writeText(text);
setShareText('Copied!'); setShareText('Copied!');
setTimeout(() => setShareText('Share Result'), 2000); setTimeout(() => setShareText('🔗 Share'), 2000);
} catch (err) { } catch (err) {
console.error('Failed to copy:', err); console.error('Clipboard failed:', err);
setShareText('Copy failed'); setShareText('✗ Failed');
setTimeout(() => setShareText('Share Result'), 2000); setTimeout(() => setShareText('🔗 Share'), 2000);
} finally {
document.body.removeChild(textarea);
} }
}; };
@@ -163,7 +192,7 @@ export default function Game({ dailyPuzzle, genre = null, maxAttempts = 7, unloc
{!hasWon && !hasLost && ( {!hasWon && !hasLost && (
<> <>
<GuessInput onGuess={handleGuess} disabled={false} /> <GuessInput onGuess={handleGuess} disabled={isProcessingGuess} />
{gameState.guesses.length < 6 ? ( {gameState.guesses.length < 6 ? (
<button <button
onClick={handleSkip} onClick={handleSkip}