⌨️
🎯

हिंदी Phonetic Layout टाइपिंग स्पीड टेस्ट

Phonetic layout for intuitive Hindi/Marathi typing

⌨️ Phonetic
🎯 Hindi Language
🔤 Unicode
Time
0:00
Gross WPM
0
Net WPM
0
Accuracy
100%

Passage to Type:

पर्यावरण संरक्षण आज की सबसे बड़ी चुनौती है। हमें पेड़ लगाने, पानी बचाने और प्रदूषण कम करने की दिशा में काम करना होगा। भावी पीढ़ियों के लिए स्वच्छ पर्यावरण छोड़ना हमारी जिम्मेदारी है।

Type Here (Phonetic Layout layout):

Input: 0 / 182 chars Correct: 0 Incorrect: 0
// Toggle main keymap guide with improved accessibility document.getElementById('toggleMainKeymap')?.addEventListener('click', function () { const guide = document.getElementById('mainKeymapGuide'); const chevron = document.getElementById('mainChevron'); const isHidden = guide.classList.contains('hidden'); guide.classList.toggle('hidden'); chevron.classList.toggle('rotate-180'); // Update ARIA attributes for accessibility this.setAttribute('aria-expanded', !isHidden); guide.setAttribute('aria-hidden', isHidden); }); // CSRF Token const csrfToken = document.getElementById('csrfToken').value; // DOM Elements const typingInput = document.getElementById('typingInput'); const passageDisplay = document.getElementById('passageDisplay'); const timeDisplay = document.getElementById('timeDisplay'); const grossWpmDisplay = document.getElementById('grossWpmDisplay'); const netWpmDisplay = document.getElementById('netWpmDisplay'); const accuracyDisplay = document.getElementById('accuracyDisplay'); const charCount = document.getElementById('charCount'); const correctCount = document.getElementById('correctCount'); const incorrectCount = document.getElementById('incorrectCount'); const resultSummary = document.getElementById('resultSummary'); const originalPassage = document.getElementById('originalPassage').value; // Debug: Check if DOM elements are found console.log('DOM Elements Check:'); console.log('typingInput:', typingInput); console.log('passageDisplay:', passageDisplay); console.log('originalPassage:', originalPassage); // Test State let testState = { started: false, completed: false, startTime: null, timerInterval: null, originalText: originalPassage, correctChars: 0, incorrectChars: 0, totalChars: originalPassage.length, rawInput: '', lastUpdateTime: 0, updateThrottle: 16 // ~60fps for smooth updates }; // Performance optimization: Throttle function function throttle(func, limit) { let inThrottle; return function () { const args = arguments; const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } } } // Prevent copy-paste with better user feedback ['paste', 'copy', 'cut'].forEach(event => { typingInput.addEventListener(event, (e) => { e.preventDefault(); if (event === 'paste') { // Show temporary visual feedback instead of alert showTemporaryMessage('⛔ Copy-paste is disabled! This is a typing practice tool.'); } }); }); // Show temporary message function function showTemporaryMessage(message) { const messageEl = document.createElement('div'); messageEl.className = 'fixed top-4 right-4 bg-red-500 text-white px-4 py-2 rounded-lg shadow-lg z-50 animate-pulse'; messageEl.textContent = message; document.body.appendChild(messageEl); setTimeout(() => { messageEl.remove(); }, 3000); } // Optimized input handler with throttling const handleInput = throttle(function (e) { if (!testState.started && !testState.completed) { startTest(); } testState.rawInput = e.target.value; updateProgress(); }, testState.updateThrottle); typingInput.addEventListener('input', handleInput); // Add input conversion for non-Remington layouts only if (inputConverter.isEnabled) { // Handle character input conversion typingInput.addEventListener('beforeinput', function(e) { if (e.inputType === 'insertText' && e.data) { e.preventDefault(); const convertedChar = inputConverter.convertInput(e.data, e.getModifierState('Shift')); // Insert converted character const start = this.selectionStart; const end = this.selectionEnd; const currentValue = this.value; const newValue = currentValue.substring(0, start) + convertedChar + currentValue.substring(end); this.value = newValue; // Set cursor position const newCursorPos = start + convertedChar.length; this.setSelectionRange(newCursorPos, newCursorPos); // Trigger the existing input handler const inputEvent = new Event('input', { bubbles: true }); this.dispatchEvent(inputEvent); } }); // Handle special key combinations typingInput.addEventListener('keydown', function(e) { inputConverter.handleKeyDown(this, e); }); } // Start test function startTest() { testState.started = true; testState.startTime = new Date(); testState.timerInterval = setInterval(updateTimer, 500); // Reduced frequency for better mobile performance // Add visual feedback for test start typingInput.classList.add('ring-2', 'ring-green-400'); setTimeout(() => { typingInput.classList.remove('ring-2', 'ring-green-400'); }, 1000); } // Update timer with reduced frequency function updateTimer() { if (!testState.started || testState.completed) return; const elapsed = Math.floor((new Date() - testState.startTime) / 1000); const minutes = Math.floor(elapsed / 60); const seconds = elapsed % 60; timeDisplay.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; calculateWPM(); } // Optimized progress update with incremental DOM updates function updateProgress() { const rawInput = testState.rawInput; const originalText = testState.originalText; let correct = 0; let incorrect = 0; // Use DocumentFragment for better performance const fragment = document.createDocumentFragment(); // Character-by-character comparison with optimized DOM manipulation for (let i = 0; i < originalText.length; i++) { const originalChar = originalText[i]; const typedChar = rawInput[i]; const span = document.createElement('span'); if (typedChar === undefined) { // Character not yet typed - show as pending span.className = 'char-pending'; span.innerHTML = originalChar === ' ' ? ' ' : originalChar; } else if (typedChar === originalChar) { // Correct character span.className = 'char-correct'; span.innerHTML = originalChar === ' ' ? ' ' : originalChar; correct++; } else { // Incorrect character span.className = 'char-incorrect'; if (originalChar === ' ') { span.innerHTML = '␣'; // Use visible space symbol for incorrect spaces } else { span.innerHTML = originalChar; } incorrect++; } fragment.appendChild(span); } // Single DOM update for better performance passageDisplay.innerHTML = ''; passageDisplay.appendChild(fragment); testState.correctChars = correct; testState.incorrectChars = incorrect; // Update counters charCount.textContent = rawInput.length; correctCount.textContent = correct; incorrectCount.textContent = incorrect; const totalTyped = rawInput.length; const accuracy = totalTyped > 0 ? ((correct / totalTyped) * 100).toFixed(1) : 100; accuracyDisplay.textContent = accuracy + '%'; if (rawInput.length >= originalText.length) { completeTest(); } } // Calculate WPM function calculateWPM() { if (!testState.started) return; const elapsedMinutes = (new Date() - testState.startTime) / 60000; if (elapsedMinutes === 0) return; const typedChars = testState.rawInput.length; const words = typedChars / 5; const grossWpm = Math.round(words / elapsedMinutes); grossWpmDisplay.textContent = grossWpm; const netWpm = Math.round((words - (testState.incorrectChars / 5)) / elapsedMinutes); netWpmDisplay.textContent = Math.max(0, netWpm); } // Complete test function completeTest() { if (testState.completed) return; testState.completed = true; testState.endTime = new Date(); clearInterval(testState.timerInterval); typingInput.disabled = true; const timeTaken = Math.floor((testState.endTime - testState.startTime) / 1000); const minutes = Math.floor(timeTaken / 60); const seconds = timeTaken % 60; const elapsedMinutes = timeTaken / 60; const words = testState.originalText.length / 5; const grossWpm = (words / elapsedMinutes).toFixed(2); const netWpm = Math.max(0, ((words - (testState.incorrectChars / 5)) / elapsedMinutes)).toFixed(2); const accuracy = ((testState.correctChars / testState.totalChars) * 100).toFixed(2); document.getElementById('finalTime').textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; document.getElementById('finalWpm').textContent = netWpm + ' WPM'; document.getElementById('finalAccuracy').textContent = accuracy + '%'; resultSummary.classList.remove('hidden'); saveResult({ passage_id: document.getElementById('passageId').value, time_taken: timeTaken, total_characters: testState.totalChars, correct_characters: testState.correctChars, incorrect_characters: testState.incorrectChars }); } // Save result using AJAX function saveResult(data) { console.log('Saving result via AJAX:', data); makeAjaxRequest('https://writingschool.in/typing-test/store', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken }, body: JSON.stringify(data) }) .then(result => { if (result.success) { console.log('Result saved successfully!'); } else { console.error('Failed to save result:', result.message); } }) .catch(error => { console.error('Error saving result:', error); }); } // Reset test function resetTest() { clearInterval(testState.timerInterval); // Get the current passage content (might be updated by loadNewPassage) const currentPassage = document.getElementById('originalPassage').value; testState = { started: false, completed: false, startTime: null, timerInterval: null, originalText: currentPassage, correctChars: 0, incorrectChars: 0, totalChars: currentPassage.length, rawInput: '', lastUpdateTime: 0, updateThrottle: 16 }; typingInput.value = ''; typingInput.disabled = false; passageDisplay.innerHTML = currentPassage; timeDisplay.textContent = '0:00'; grossWpmDisplay.textContent = '0'; netWpmDisplay.textContent = '0'; accuracyDisplay.textContent = '100%'; charCount.textContent = '0'; correctCount.textContent = '0'; incorrectCount.textContent = '0'; resultSummary.classList.add('hidden'); typingInput.focus(); console.log('Test reset with passage length:', currentPassage.length); } // AJAX Helper Function function makeAjaxRequest(url, options = {}) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const method = options.method || 'GET'; const timeout = options.timeout || 15000; // Reduced timeout for mobile xhr.open(method, url, true); xhr.timeout = timeout; // Set headers xhr.setRequestHeader('Accept', 'application/json'); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); if (options.headers) { Object.keys(options.headers).forEach(key => { xhr.setRequestHeader(key, options.headers[key]); }); } xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status >= 200 && xhr.status < 300) { try { const response = JSON.parse(xhr.responseText); resolve(response); } catch (e) { reject(new Error('Invalid JSON response')); } } else { reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`)); } } }; xhr.onerror = () => reject(new Error('Network error')); xhr.ontimeout = () => reject(new Error('Request timeout')); xhr.send(options.body || null); }); } // Load new passage using AJAX function loadNewPassage() { const difficulty = document.getElementById('difficultySelect').value; const language = document.getElementById('currentLanguage').value; const layout = document.getElementById('currentLayout').value; // Add loading state const newPassageBtn = document.getElementById('newPassageBtn'); const originalText = newPassageBtn.textContent; newPassageBtn.textContent = 'Loading...'; newPassageBtn.disabled = true; // Add visual loading indicator newPassageBtn.innerHTML = 'Loading...'; console.log('Loading new passage via AJAX:', { difficulty, language, layout }); const url = `https://writingschool.in/typing-test/get-passage?difficulty=${encodeURIComponent(difficulty)}&language=${encodeURIComponent(language)}&layout=${encodeURIComponent(layout)}`; makeAjaxRequest(url) .then(result => { console.log('AJAX Response:', result); if (result.success && result.passage) { // Update passage data document.getElementById('originalPassage').value = result.passage.content; document.getElementById('passageId').value = result.passage.id; document.getElementById('totalChars').textContent = result.passage.character_count; // Update the global testState testState.originalText = result.passage.content; testState.totalChars = result.passage.character_count; // Reset the test with new passage resetTest(); console.log('New passage loaded successfully via AJAX'); // Show success feedback newPassageBtn.innerHTML = 'Loaded!'; setTimeout(() => { newPassageBtn.textContent = originalText; }, 1000); } else { throw new Error(result.message || 'Failed to load passage'); } }) .catch(error => { console.error('AJAX Error:', error); showTemporaryMessage(`Error loading passage: ${error.message}`); }) .finally(() => { // Restore button state setTimeout(() => { newPassageBtn.textContent = originalText; newPassageBtn.disabled = false; }, 1000); }); } // Event listeners document.getElementById('resetBtn').addEventListener('click', resetTest); document.getElementById('newPassageBtn').addEventListener('click', loadNewPassage); document.getElementById('tryAgainBtn')?.addEventListener('click', resetTest); document.getElementById('difficultySelect').addEventListener('change', loadNewPassage); window.addEventListener('load', () => { // Double-check DOM elements are available const elements = { typingInput: document.getElementById('typingInput'), passageDisplay: document.getElementById('passageDisplay'), originalPassage: document.getElementById('originalPassage') }; console.log('Elements check on load:', elements); typingInput.focus(); console.log('Hindi typing test loaded successfully'); });