Spaces:
Sleeping
Sleeping
| const codeDisplay = document.getElementById('code-display'); | |
| const hiddenInput = document.getElementById('hidden-input'); | |
| const wpmDisplay = document.getElementById('wpm'); | |
| const accuracyDisplay = document.getElementById('accuracy'); | |
| const progressDisplay = document.getElementById('progress'); | |
| const langDisplay = document.getElementById('lang-display'); | |
| const resultOverlay = document.getElementById('result-overlay'); | |
| const finalWpmDisplay = document.getElementById('final-wpm'); | |
| const finalAccuracyDisplay = document.getElementById('final-accuracy'); | |
| const restartBtn = document.getElementById('restart-btn'); | |
| let currentCode = ""; | |
| let currentIndex = 0; | |
| let startTime = null; | |
| let timer = null; | |
| let mistakes = 0; | |
| let totalTyped = 0; | |
| let isFinished = false; | |
| // Initialize | |
| function init() { | |
| fetchSnippet(); | |
| hiddenInput.focus(); | |
| // Keep focus on hidden input | |
| document.addEventListener('click', () => { | |
| if (!isFinished) hiddenInput.focus(); | |
| }); | |
| } | |
| async function fetchSnippet() { | |
| try { | |
| const response = await fetch('/api/snippet'); | |
| const data = await response.json(); | |
| setupGame(data.code, data.language); | |
| } catch (e) { | |
| console.error("Failed to fetch snippet", e); | |
| setupGame("print('Hello World')", "Python"); | |
| } | |
| } | |
| function setupGame(code, language) { | |
| // Reset state | |
| currentCode = code.replace(/\t/g, " "); // Replace tabs with spaces | |
| currentIndex = 0; | |
| startTime = null; | |
| mistakes = 0; | |
| totalTyped = 0; | |
| isFinished = false; | |
| if (timer) clearInterval(timer); | |
| // Update UI | |
| langDisplay.textContent = language; | |
| wpmDisplay.textContent = '0'; | |
| accuracyDisplay.textContent = '100%'; | |
| progressDisplay.textContent = '0%'; | |
| resultOverlay.classList.add('hidden'); | |
| // Render Code | |
| renderCode(); | |
| hiddenInput.value = ''; | |
| hiddenInput.focus(); | |
| } | |
| function renderCode() { | |
| codeDisplay.innerHTML = ''; | |
| currentCode.split('').forEach((char, index) => { | |
| const span = document.createElement('span'); | |
| span.innerText = char; | |
| if (index === 0) span.classList.add('cursor'); | |
| // Visual tweak for spaces/newlines | |
| if (char === '\n') { | |
| span.innerHTML = '↵\n'; | |
| span.classList.add('text-slate-700'); | |
| } | |
| codeDisplay.appendChild(span); | |
| }); | |
| } | |
| function startTimer() { | |
| if (!startTime) { | |
| startTime = new Date(); | |
| timer = setInterval(updateStats, 1000); | |
| } | |
| } | |
| function updateStats() { | |
| if (!startTime) return; | |
| const now = new Date(); | |
| const timeDiff = (now - startTime) / 1000 / 60; // in minutes | |
| if (timeDiff > 0) { | |
| // WPM = (Characters / 5) / Minutes | |
| const wpm = Math.round((currentIndex / 5) / timeDiff); | |
| wpmDisplay.textContent = wpm; | |
| } | |
| const accuracy = totalTyped === 0 ? 100 : Math.round(((totalTyped - mistakes) / totalTyped) * 100); | |
| accuracyDisplay.textContent = accuracy + '%'; | |
| const progress = Math.round((currentIndex / currentCode.length) * 100); | |
| progressDisplay.textContent = progress + '%'; | |
| } | |
| function finishGame() { | |
| isFinished = true; | |
| clearInterval(timer); | |
| updateStats(); | |
| finalWpmDisplay.textContent = wpmDisplay.textContent; | |
| finalAccuracyDisplay.textContent = accuracyDisplay.textContent; | |
| resultOverlay.classList.remove('hidden'); | |
| restartBtn.focus(); | |
| } | |
| // Input Handling | |
| hiddenInput.addEventListener('input', (e) => { | |
| if (isFinished) return; | |
| // We don't actually use the input value, we just catch the event | |
| // But to support backspace on mobile properly, we might need more complex logic. | |
| // For this desktop-first version, we'll listen to 'keydown' for control and just check logic here if needed. | |
| // Actually, 'input' event is safer for mobile software keyboards. | |
| const inputChar = e.data; | |
| // Reset input to empty to avoid scrolling or overflow | |
| // BUT, we need to handle the character. | |
| // This approach is tricky with 'input' event because 'data' can be null on some actions. | |
| // Let's rely on keydown for desktop/precision, and input for backup? | |
| // Let's stick to keydown for a "Pro" tool. | |
| }); | |
| window.addEventListener('keydown', (e) => { | |
| if (isFinished) { | |
| if (e.key === 'Enter') { | |
| init(); | |
| } | |
| return; | |
| } | |
| // Prevent default scrolling for Space | |
| if (e.key === ' ' && e.target === document.body) { | |
| e.preventDefault(); | |
| } | |
| if (e.key === 'Tab') { | |
| e.preventDefault(); | |
| init(); // Quick restart | |
| return; | |
| } | |
| // Ignore non-character keys (Shift, Ctrl, etc.) | |
| if (e.key.length > 1 && e.key !== 'Enter' && e.key !== 'Backspace') return; | |
| hiddenInput.focus(); | |
| startTimer(); | |
| const charToType = currentCode[currentIndex]; | |
| const spans = codeDisplay.querySelectorAll('span'); | |
| if (e.key === 'Backspace') { | |
| // Optional: Allow going back? | |
| // For strict practice, maybe not. Or yes. | |
| // Let's allow simple backspace. | |
| if (currentIndex > 0) { | |
| currentIndex--; | |
| const span = spans[currentIndex]; | |
| span.className = ''; // Reset classes | |
| span.classList.add('cursor'); // Move cursor back | |
| // Remove cursor from next char | |
| if (currentIndex + 1 < spans.length) { | |
| spans[currentIndex + 1].classList.remove('cursor'); | |
| } | |
| } | |
| return; | |
| } | |
| let typedChar = e.key; | |
| if (typedChar === 'Enter') typedChar = '\n'; | |
| totalTyped++; | |
| const currentSpan = spans[currentIndex]; | |
| if (typedChar === charToType) { | |
| currentSpan.classList.add('text-emerald-400'); // Correct | |
| currentSpan.classList.remove('text-red-500', 'bg-red-900/50', 'text-slate-500'); | |
| currentSpan.classList.remove('cursor'); | |
| currentIndex++; | |
| if (currentIndex >= currentCode.length) { | |
| finishGame(); | |
| } else { | |
| spans[currentIndex].classList.add('cursor'); | |
| // Auto-scroll if cursor is near bottom | |
| const cursorRect = spans[currentIndex].getBoundingClientRect(); | |
| const containerRect = codeDisplay.parentElement.getBoundingClientRect(); | |
| if (cursorRect.bottom > containerRect.bottom - 40) { | |
| codeDisplay.parentElement.scrollTop += 30; // Scroll down | |
| } | |
| } | |
| } else { | |
| mistakes++; | |
| currentSpan.classList.add('text-red-500', 'bg-red-900/50'); // Incorrect | |
| // We don't advance on mistake, user must type correct key | |
| // Or should we? | |
| // "Stop on error" is better for learning code. | |
| } | |
| }); | |
| restartBtn.addEventListener('click', init); | |
| // Start | |
| init(); | |