Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>CodeTyper Racer - Challenge</title> | |
| <!-- Tailwind --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Feather Icons (ONLY ONCE β FIXED) --> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| </head> | |
| <body class="bg-gray-900 text-gray-100 min-h-screen"> | |
| <!-- ================= HEADER ================= --> | |
| <header class="bg-gray-800 py-4 shadow-lg"> | |
| <div class="container mx-auto px-4 flex justify-between items-center"> | |
| <h1 class="text-xl font-bold">CodeTyper Racer π</h1> | |
| <div class="flex items-center space-x-4"> | |
| <div id="timerDisplay" class="bg-gray-700 px-4 py-2 rounded-lg font-mono">00:00</div> | |
| <button id="leaderboardBtn" class="flex items-center space-x-1 bg-gray-700 px-3 py-2 rounded-lg hover:bg-gray-600"> | |
| <i data-feather="bar-chart-2"></i> | |
| <span>Leaderboard</span> | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- ================= MAIN ================= --> | |
| <main class="container mx-auto px-4 py-8"> | |
| <div class="flex flex-col lg:flex-row gap-8"> | |
| <!-- SETTINGS --> | |
| <div class="lg:w-1/4 bg-gray-800 p-6 rounded-xl shadow-lg"> | |
| <h2 class="text-lg font-semibold mb-3">Language</h2> | |
| <button data-language="html" class="language-btn w-full bg-blue-600 py-2 rounded mb-2">HTML</button> | |
| <button data-language="css" class="language-btn w-full bg-blue-600 py-2 rounded mb-2">CSS</button> | |
| <button data-language="javascript" class="language-btn w-full bg-blue-600 py-2 rounded">JavaScript</button> | |
| <h2 class="text-lg font-semibold mt-6 mb-3">Timer</h2> | |
| <button data-time="1" class="time-btn w-full bg-gray-700 py-2 rounded mb-2">1 min</button> | |
| <button data-time="5" class="time-btn w-full bg-gray-700 py-2 rounded mb-2">5 min</button> | |
| <button data-time="10" class="time-btn w-full bg-gray-700 py-2 rounded">10 min</button> | |
| <h2 class="text-lg font-semibold mt-6 mb-3">Difficulty</h2> | |
| <button data-difficulty="easy" class="difficulty-btn w-full bg-green-600 py-2 rounded mb-2">Easy</button> | |
| <button data-difficulty="medium" class="difficulty-btn w-full bg-yellow-600 py-2 rounded mb-2">Medium</button> | |
| <button data-difficulty="hard" class="difficulty-btn w-full bg-red-600 py-2 rounded">Hard</button> | |
| <button id="startTestBtn" class="mt-6 w-full bg-purple-600 py-3 rounded font-bold">Start Test</button> | |
| </div> | |
| <!-- TYPING AREA --> | |
| <div class="lg:w-3/4"> | |
| <div id="codeDisplay" class="bg-gray-800 p-6 rounded-xl font-mono h-64 overflow-y-auto mb-4"> | |
| <p class="italic text-gray-500">Select settings and start</p> | |
| </div> | |
| <textarea id="typingArea" disabled class="w-full h-48 bg-gray-800 p-4 rounded font-mono"></textarea> | |
| <div class="h-2 bg-gray-700 rounded mt-2"> | |
| <div id="progressFill" class="h-full bg-blue-500 rounded" style="width:0%"></div> | |
| </div> | |
| <div class="flex justify-between mt-4"> | |
| <div id="wpmDisplay">WPM: 0</div> | |
| <div id="accuracyDisplay">Accuracy: 0%</div> | |
| <button id="submitTestBtn" disabled class="bg-green-600 px-4 py-2 rounded">Submit</button> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- ================= SCRIPT ================= --> | |
| <script> | |
| feather.replace(); | |
| /* ---------- USER DATA ---------- */ | |
| if (!localStorage.getItem('userData')) { | |
| localStorage.setItem('userData', JSON.stringify({ | |
| name: prompt("Name:") || "Anonymous", | |
| gradeLevel: prompt("Grade:") || "-", | |
| section: prompt("Section:") || "-" | |
| })); | |
| } | |
| /* ---------- CODE SNIPPETS ---------- */ | |
| const codeSnippets = { | |
| html:{easy:`<h1>Hello World</h1>`,medium:`<div><p>Sample</p></div>`,hard:`<!DOCTYPE html><html><body><h1>Hello</h1></body></html>`}, | |
| css:{easy:`body{margin:0}`,medium:`.box{padding:10px}`,hard:`@media(max-width:600px){body{background:red}}`}, | |
| javascript:{easy:`console.log("Hi")`,medium:`function add(a,b){return a+b}`,hard:`class App{constructor(){console.log("Start")}}`} | |
| }; | |
| /* ---------- STATE ---------- */ | |
| let selectedLanguage="html",selectedTime=1,selectedDifficulty="easy"; | |
| let startTime,timer,testActive=false,currentCode="",correctChars=0; | |
| /* ---------- DOM ---------- */ | |
| const codeDisplay=document.getElementById("codeDisplay"); | |
| const typingArea=document.getElementById("typingArea"); | |
| const timerDisplay=document.getElementById("timerDisplay"); | |
| const progressFill=document.getElementById("progressFill"); | |
| const wpmDisplay=document.getElementById("wpmDisplay"); | |
| const accuracyDisplay=document.getElementById("accuracyDisplay"); | |
| /* ---------- BUTTONS ---------- */ | |
| document.querySelectorAll(".language-btn").forEach(b => | |
| b.onclick = (e) => { | |
| selectedLanguage = b.dataset.language; | |
| setActiveButton(e.target, 'language'); | |
| } | |
| ); | |
| document.querySelectorAll(".time-btn").forEach(b => | |
| b.onclick = (e) => { | |
| selectedTime = +b.dataset.time; | |
| setActiveButton(e.target, 'time'); | |
| } | |
| ); | |
| document.querySelectorAll(".difficulty-btn").forEach(b => | |
| b.onclick = (e) => { | |
| selectedDifficulty = b.dataset.difficulty; | |
| setActiveButton(e.target, 'difficulty'); | |
| } | |
| ); | |
| // Set first button in each category as active by default | |
| document.querySelector('.language-btn').click(); | |
| document.querySelector('.time-btn').click(); | |
| document.querySelector('.difficulty-btn').click(); | |
| /* ---------- START ---------- */ | |
| document.getElementById("startTestBtn").onclick=()=>{ | |
| currentCode = codeSnippets[selectedLanguage][selectedDifficulty]; | |
| renderCodeDisplay(); | |
| typingArea.disabled = false; | |
| typingArea.value = ""; | |
| typingArea.focus(); | |
| correctChars = 0; | |
| progressFill.style.width = "0%"; | |
| wpmDisplay.textContent = "WPM: 0"; | |
| accuracyDisplay.textContent = "Accuracy: 0%"; | |
| startTime=Date.now(); | |
| testActive=true; | |
| let sec=selectedTime*60; | |
| timerDisplay.textContent=`00:${sec}`; | |
| clearInterval(timer); | |
| timer=setInterval(()=>{ | |
| sec--; | |
| timerDisplay.textContent=`${Math.floor(sec/60).toString().padStart(2,"0")}:${(sec%60).toString().padStart(2,"0")}`; | |
| if(sec<=0) endTest(); | |
| },1000); | |
| }; | |
| /* ---------- TYPING ---------- */ | |
| function renderCodeDisplay() { | |
| const typed = typingArea.value; | |
| let html = ''; | |
| for (let i = 0; i < currentCode.length; i++) { | |
| let char = currentCode[i]; | |
| let spanClass = ''; | |
| if (i < typed.length) { | |
| spanClass = typed[i] === char ? 'correct' : 'incorrect'; | |
| } else if (i === typed.length) { | |
| spanClass = 'current cursor'; | |
| } | |
| char = char === '\n' ? '<br>' : | |
| char === ' ' ? ' ' : | |
| char === '<' ? '<' : | |
| char === '>' ? '>' : char; | |
| html += spanClass ? `<span class="${spanClass}">${char}</span>` : char; | |
| } | |
| codeDisplay.innerHTML = html; | |
| } | |
| typingArea.oninput = () => { | |
| if (!testActive) return; | |
| const typed = typingArea.value; | |
| correctChars = 0; | |
| for (let i = 0; i < typed.length; i++) { | |
| if (typed[i] === currentCode[i]) correctChars++; | |
| } | |
| const acc = Math.round((correctChars / typed.length) * 100) || 0; | |
| const wpm = Math.round((correctChars / 5) / ((Date.now() - startTime) / 60000)) || 0; | |
| accuracyDisplay.textContent = `Accuracy: ${acc}%`; | |
| wpmDisplay.textContent = `WPM: ${wpm}`; | |
| progressFill.style.width = `${Math.min(typed.length / currentCode.length * 100, 100)}%`; | |
| renderCodeDisplay(); | |
| if (typed.length >= currentCode.length) { | |
| endTest(); | |
| } | |
| }; | |
| /* ---------- END ---------- */ | |
| function endTest() { | |
| if (!testActive) return; | |
| testActive = false; | |
| clearInterval(timer); | |
| typingArea.disabled = true; | |
| const endTime = Date.now(); | |
| const stats = calculateTypingStats( | |
| startTime, | |
| endTime, | |
| correctChars, | |
| currentCode.length | |
| ); | |
| saveToLeaderboard(stats); | |
| showResultsModal(stats); | |
| } | |
| </script> | |
| </body> | |
| </html> | |