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();