diff --git a/Domains/Frontend/MiniProjects/TypingSpeedRaceGame/README.md b/Domains/Frontend/MiniProjects/TypingSpeedRaceGame/README.md new file mode 100644 index 00000000..22ec1b89 --- /dev/null +++ b/Domains/Frontend/MiniProjects/TypingSpeedRaceGame/README.md @@ -0,0 +1,171 @@ +# ๐Ÿ Typing Speed Race Game + +**Contributor:** Soham-Gadekar +**Domain:** Frontend +**Difficulty:** Intermediate +**Tech Stack:** HTML, CSS, JavaScript (ES6+), LocalStorage API + +--- + +## ๐Ÿ“ Description + +A fun and interactive **Typing Speed Race Game** that challenges players to type as fast and accurately as possible. +The app displays random paragraphs and measures typing speed (WPM), accuracy, and time taken. +It also features a **ghost racer** based on your best WPM for an extra challenge! + +Perfect for improving typing speed and practicing JavaScript DOM manipulation, event handling, and local storage integration. + +--- + +## ๐ŸŽฏ Features + +* ๐Ÿ–‹๏ธ Random paragraph generation +* โšก Real-time WPM (Words Per Minute) and accuracy tracking +* ๐ŸŽฎ Ghost racer feature (race against your previous best speed) +* ๐Ÿง  LocalStorage for best score persistence +* ๐Ÿ•’ Live timer display +* ๐Ÿงน Reset and restart functionality +* ๐Ÿ“Š Progress bars for player and ghost +* ๐Ÿ’ฅ Animated result modal with โ€œNew Recordโ€ badge +* ๐Ÿ“ฑ Fully responsive design + +--- + +## ๐Ÿ› ๏ธ Tech Stack + +* **HTML5** - Semantic structure +* **CSS3** - Responsive and modern UI +* **JavaScript (ES6+)** - Game logic, DOM updates +* **LocalStorage API** - Store best WPM + +--- + +## ๐Ÿš€ How to Run + +### Method 1: Direct Browser + +1. Download or clone the repository +2. Open `index.html` in your browser +3. Click โ€œStart Raceโ€ and begin typing! + +### Method 2: Live Server (Recommended) + +1. Install VS Code and Live Server extension +2. Right-click `index.html` โ†’ **Open with Live Server** +3. The app will open at `http://localhost:5500` + +--- + +## ๐Ÿ“ Project Structure + +``` +TypingRace/ +โ”œโ”€โ”€ index.html # Main HTML structure +โ”œโ”€โ”€ styles.css # Styling and layout +โ”œโ”€โ”€ script.js # Game logic +โ””โ”€โ”€ README.md # Project documentation +``` + +--- + +## ๐Ÿ’ป Code Highlights + +### Random Paragraph Selection + +```javascript +currentText = paragraphs[Math.floor(Math.random() * paragraphs.length)]; +displayText(); +``` + +### Ghost Racer Logic + +```javascript +const charsPerSecond = (bestWpm * 5) / 60; +ghostInterval = setInterval(() => { + const elapsed = (Date.now() - ghostStartTime) / 1000; + const ghostChars = Math.floor(charsPerSecond * elapsed); + const ghostProgressPercent = Math.min((ghostChars / textLength) * 100, 100); + ghostProgress.style.width = ghostProgressPercent + '%'; +}, 100); +``` + +### LocalStorage Integration + +```javascript +if (wpm > bestWpm) { + bestWpm = wpm; + localStorage.setItem('typingRaceBestWpm', wpm); +} +``` + +--- + +## ๐Ÿ“š Learning Outcomes + +### Skills Practiced + +* โœ… DOM Manipulation +* โœ… Real-time event handling +* โœ… JavaScript timing functions +* โœ… Dynamic progress visualization +* โœ… LocalStorage API usage +* โœ… Responsive UI design + +### Concepts Learned + +* Handling and visualizing live data +* Optimizing UI updates +* Persisting user data in browser +* Building mini-games with pure JavaScript + +--- + +## ๐ŸŽจ Customization Ideas + +1. **Dark/Light Theme Toggle** +2. **Leaderboard** - Global or local high scores +3. **Multiple Difficulty Levels** +4. **Sound Effects** for typing and completion +5. **Multiplayer Mode** (Socket.io or Firebase) +6. **Animated Ghost Character** +7. **Typing Error Highlighting with Suggestions** +8. **Countdown Timer before start** + +--- + +## ๐Ÿ› Known Issues + +* None currently. +* If the progress bar desyncs, refresh the page. + +--- + +## ๐Ÿš€ Future Enhancements + +* [ ] Add difficulty modes +* [ ] Include typing statistics history +* [ ] Introduce multiplayer ghost race +* [ ] Add customizable typing texts +* [ ] Enhance animations and UI polish + +--- + +## ๐Ÿ“„ License + +MIT License โ€” Free to use, modify, and share! + +--- + +## ๐Ÿค Contributing + +This project is open for contributions! +Feel free to: + +* Fork and improve +* Report issues +* Add new features +* Submit pull requests + +--- + +**Happy Typing! โŒจ๏ธ๐Ÿ”ฅ** diff --git a/Domains/Frontend/MiniProjects/TypingSpeedRaceGame/index.html b/Domains/Frontend/MiniProjects/TypingSpeedRaceGame/index.html new file mode 100644 index 00000000..aa7feec9 --- /dev/null +++ b/Domains/Frontend/MiniProjects/TypingSpeedRaceGame/index.html @@ -0,0 +1,92 @@ + + + + + + Typing Speed Race Game + + + +
+
+

โšก Typing Speed Race

+

Race against your best performance!

+
+ +
+
+
+ ๐Ÿƒ You + 0% +
+
+
+
+ +
+ ๐Ÿ‘ป Ghost (Best) + 0% +
+
+
+
+
+ +
+ + + +
+
+
WPM
+
0
+
+
+
Accuracy
+
100%
+
+
+
Time
+
0s
+
+
+
Best WPM
+
--
+
+
+ +
+ + +
+
+
+ +
+
+

๐Ÿ Race Complete!

+
+
+
+
Final WPM
+
0
+
+
+
Accuracy
+
100%
+
+
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/Domains/Frontend/MiniProjects/TypingSpeedRaceGame/script.js b/Domains/Frontend/MiniProjects/TypingSpeedRaceGame/script.js new file mode 100644 index 00000000..8538ca51 --- /dev/null +++ b/Domains/Frontend/MiniProjects/TypingSpeedRaceGame/script.js @@ -0,0 +1,208 @@ +const paragraphs = [ + "The quick brown fox jumps over the lazy dog while the sun sets behind the mountains painting the sky in shades of orange and purple.", + "Technology advances rapidly changing how we live and work. Innovation drives progress and creates new opportunities for everyone to explore and grow.", + "Practice makes perfect when learning new skills. Consistent effort and dedication lead to mastery over time with patience and perseverance.", + "Nature provides endless inspiration with its beauty and complexity. From tiny insects to massive mountains everything connects in harmony.", + "Music transcends language barriers bringing people together through rhythm and melody. Every culture contributes unique sounds to our world.", + "Reading opens doors to new worlds and perspectives. Books transport us across time and space teaching valuable lessons along the journey.", + "Creativity flourishes when we allow ourselves to think differently. Breaking free from conventional patterns leads to innovative solutions.", + "Friendship enriches our lives with shared experiences and support. True connections withstand the test of time through ups and downs." +]; + +let currentText = ''; +let startTime = null; +let isRacing = false; +let bestWpm = localStorage.getItem('typingRaceBestWpm') ? parseInt(localStorage.getItem('typingRaceBestWpm')) : 0; +let ghostInterval = null; +let ghostStartTime = null; + +const textDisplay = document.getElementById('textDisplay'); +const inputArea = document.getElementById('inputArea'); +const startBtn = document.getElementById('startBtn'); +const resetBtn = document.getElementById('resetBtn'); +const wpmDisplay = document.getElementById('wpmDisplay'); +const accuracyDisplay = document.getElementById('accuracyDisplay'); +const timeDisplay = document.getElementById('timeDisplay'); +const bestWpmDisplay = document.getElementById('bestWpmDisplay'); +const resultsModal = document.getElementById('resultsModal'); +const raceAgainBtn = document.getElementById('raceAgainBtn'); +const playerProgress = document.getElementById('playerProgress'); +const ghostProgress = document.getElementById('ghostProgress'); +const playerPercent = document.getElementById('playerPercent'); +const ghostPercent = document.getElementById('ghostPercent'); + +function initGame() { + currentText = paragraphs[Math.floor(Math.random() * paragraphs.length)]; + displayText(); + updateBestDisplay(); +} + +function displayText() { + textDisplay.innerHTML = currentText + .split('') + .map((char, i) => `${char}`) + .join(''); +} + +function startRace() { + isRacing = true; + startTime = Date.now(); + ghostStartTime = Date.now(); + inputArea.disabled = false; + inputArea.value = ''; + inputArea.focus(); + startBtn.disabled = true; + resetBtn.disabled = false; + playerProgress.style.width = '0%'; + ghostProgress.style.width = '0%'; + + if (bestWpm > 0) { + startGhostRace(); + } +} + +function startGhostRace() { + const textLength = currentText.length; + const charsPerSecond = (bestWpm * 5) / 60; + + ghostInterval = setInterval(() => { + if (!isRacing) { + clearInterval(ghostInterval); + return; + } + + const elapsed = (Date.now() - ghostStartTime) / 1000; + const ghostChars = Math.floor(charsPerSecond * elapsed); + const ghostProgressPercent = Math.min((ghostChars / textLength) * 100, 100); + + ghostProgress.style.width = ghostProgressPercent + '%'; + ghostPercent.textContent = Math.round(ghostProgressPercent) + '%'; + + if (ghostProgressPercent >= 100) { + clearInterval(ghostInterval); + } + }, 100); +} + +function updateStats() { + const typedText = inputArea.value; + const elapsed = (Date.now() - startTime) / 1000; + const chars = textDisplay.querySelectorAll('.char'); + + let correctChars = 0; + let totalTyped = typedText.length; + + chars.forEach((char, i) => { + char.classList.remove('correct', 'incorrect', 'current'); + + if (i < typedText.length) { + if (typedText[i] === currentText[i]) { + char.classList.add('correct'); + correctChars++; + } else { + char.classList.add('incorrect'); + } + } else if (i === typedText.length) { + char.classList.add('current'); + } + }); + + const words = correctChars / 5; + const minutes = elapsed / 60; + const wpm = Math.round(words / minutes) || 0; + const accuracy = totalTyped > 0 ? Math.round((correctChars / totalTyped) * 100) : 100; + + wpmDisplay.textContent = wpm; + accuracyDisplay.textContent = accuracy + '%'; + timeDisplay.textContent = elapsed.toFixed(1) + 's'; + + const progressPercent = (typedText.length / currentText.length) * 100; + playerProgress.style.width = progressPercent + '%'; + playerPercent.textContent = Math.round(progressPercent) + '%'; + + if (typedText === currentText) { + finishRace(wpm, accuracy); + } +} + +function finishRace(wpm, accuracy) { + isRacing = false; + inputArea.disabled = true; + startBtn.disabled = false; + resetBtn.disabled = false; + + if (ghostInterval) { + clearInterval(ghostInterval); + } + + const isNewRecord = wpm > bestWpm; + if (isNewRecord) { + bestWpm = wpm; + localStorage.setItem('typingRaceBestWpm', wpm); + updateBestDisplay(); + } + + showResults(wpm, accuracy, isNewRecord); +} + +function showResults(wpm, accuracy, isNewRecord) { + document.getElementById('finalWpm').textContent = wpm; + document.getElementById('finalAccuracy').textContent = accuracy + '%'; + + const badge = document.getElementById('newRecordBadge'); + if (isNewRecord) { + badge.innerHTML = '
๐ŸŽ‰ New Record!
'; + } else { + badge.innerHTML = ''; + } + + resultsModal.classList.add('active'); +} + +function resetRace() { + isRacing = false; + startTime = null; + ghostStartTime = null; + inputArea.value = ''; + inputArea.disabled = true; + startBtn.disabled = false; + resetBtn.disabled = true; + wpmDisplay.textContent = '0'; + accuracyDisplay.textContent = '100%'; + timeDisplay.textContent = '0s'; + playerProgress.style.width = '0%'; + ghostProgress.style.width = '0%'; + playerPercent.textContent = '0%'; + ghostPercent.textContent = '0%'; + + if (ghostInterval) { + clearInterval(ghostInterval); + } + + initGame(); +} + +function updateBestDisplay() { + bestWpmDisplay.textContent = bestWpm > 0 ? bestWpm : '--'; +} + +startBtn.addEventListener('click', startRace); +resetBtn.addEventListener('click', resetRace); +raceAgainBtn.addEventListener('click', () => { + resultsModal.classList.remove('active'); + resetRace(); +}); + +inputArea.addEventListener('input', () => { + if (isRacing) { + updateStats(); + } +}); + +resultsModal.addEventListener('click', (e) => { + if (e.target === resultsModal) { + resultsModal.classList.remove('active'); + } +}); + +initGame(); \ No newline at end of file diff --git a/Domains/Frontend/MiniProjects/TypingSpeedRaceGame/style.css b/Domains/Frontend/MiniProjects/TypingSpeedRaceGame/style.css new file mode 100644 index 00000000..89ec5fb9 --- /dev/null +++ b/Domains/Frontend/MiniProjects/TypingSpeedRaceGame/style.css @@ -0,0 +1,337 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary: #6366f1; + --primary-dark: #4f46e5; + --success: #10b981; + --error: #ef4444; + --bg-dark: #0f172a; + --bg-card: #1e293b; + --text: #f1f5f9; + --text-dim: #94a3b8; + --ghost: #8b5cf6; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, var(--bg-dark) 0%, #1a1a2e 100%); + color: var(--text); + min-height: 100vh; + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; +} + +.container { + max-width: 900px; + width: 100%; +} + +header { + text-align: center; + margin-bottom: 40px; + animation: fadeInDown 0.6s ease; +} + +h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, var(--primary), var(--ghost)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 10px; +} + +.tagline { + color: var(--text-dim); + font-size: 1.1rem; +} + +.race-track { + background: var(--bg-card); + border-radius: 16px; + padding: 30px; + margin-bottom: 30px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); + animation: fadeInUp 0.6s ease; +} + +.progress-section { + margin-bottom: 30px; +} + +.progress-label { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + font-size: 0.9rem; +} + +.racer-name { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; +} + +.progress-bar-container { + height: 40px; + background: rgba(0, 0, 0, 0.3); + border-radius: 20px; + overflow: hidden; + position: relative; + margin-bottom: 15px; +} + +.progress-bar { + height: 100%; + transition: width 0.3s ease; + display: flex; + align-items: center; + justify-content: flex-end; + padding-right: 15px; + font-weight: 600; + position: relative; +} + +.player-progress { + background: linear-gradient(90deg, var(--primary), var(--primary-dark)); + box-shadow: 0 0 20px rgba(99, 102, 241, 0.5); +} + +.ghost-progress { + background: linear-gradient(90deg, rgba(139, 92, 246, 0.6), rgba(139, 92, 246, 0.4)); + box-shadow: 0 0 20px rgba(139, 92, 246, 0.3); +} + +.racer-icon { + font-size: 1.5rem; +} + +.text-display { + background: rgba(0, 0, 0, 0.3); + padding: 30px; + border-radius: 12px; + font-size: 1.5rem; + line-height: 2; + letter-spacing: 0.5px; + margin-bottom: 25px; + min-height: 150px; + font-family: 'Courier New', monospace; +} + +.char { + transition: all 0.1s ease; +} + +.char.correct { + color: var(--success); + text-shadow: 0 0 8px rgba(16, 185, 129, 0.5); +} + +.char.incorrect { + color: var(--error); + background: rgba(239, 68, 68, 0.2); + text-shadow: 0 0 8px rgba(239, 68, 68, 0.5); +} + +.char.current { + background: var(--primary); + animation: pulse 1s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.input-area { + width: 100%; + padding: 20px; + font-size: 1.3rem; + background: rgba(0, 0, 0, 0.3); + border: 2px solid transparent; + border-radius: 12px; + color: var(--text); + font-family: 'Courier New', monospace; + transition: all 0.3s ease; + margin-bottom: 25px; +} + +.input-area:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 20px rgba(99, 102, 241, 0.3); +} + +.input-area:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 20px; + margin-bottom: 25px; +} + +.stat-card { + background: rgba(0, 0, 0, 0.3); + padding: 20px; + border-radius: 12px; + text-align: center; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.stat-label { + font-size: 0.9rem; + color: var(--text-dim); + margin-bottom: 8px; +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + color: var(--primary); +} + +.stat-value.accuracy { + color: var(--success); +} + +.stat-value.best { + color: var(--ghost); +} + +.controls { + display: flex; + gap: 15px; + justify-content: center; +} + +.btn { + padding: 15px 40px; + font-size: 1.1rem; + font-weight: 600; + border: none; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 1px; +} + +.btn-primary { + background: linear-gradient(135deg, var(--primary), var(--primary-dark)); + color: white; + box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(99, 102, 241, 0.6); +} + +.btn-secondary { + background: rgba(255, 255, 255, 0.1); + color: var(--text); + border: 2px solid rgba(255, 255, 255, 0.2); +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.15); + transform: translateY(-2px); +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none !important; +} + +.results-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: none; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 20px; +} + +.results-modal.active { + display: flex; + animation: fadeIn 0.3s ease; +} + +.results-content { + background: var(--bg-card); + padding: 40px; + border-radius: 20px; + max-width: 500px; + width: 100%; + text-align: center; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6); + animation: scaleIn 0.3s ease; +} + +.results-content h2 { + font-size: 2rem; + margin-bottom: 30px; + color: var(--primary); +} + +.new-record { + background: linear-gradient(135deg, var(--success), #059669); + color: white; + padding: 10px 20px; + border-radius: 8px; + margin-bottom: 20px; + font-weight: 600; + animation: bounce 0.6s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(30px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes fadeInDown { + from { opacity: 0; transform: translateY(-30px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes scaleIn { + from { transform: scale(0.9); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} + +@keyframes bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } +} + +@media (max-width: 768px) { + h1 { font-size: 2rem; } + .text-display { font-size: 1.2rem; padding: 20px; } + .input-area { font-size: 1.1rem; } + .controls { flex-direction: column; } + .btn { width: 100%; } + .stats-grid { grid-template-columns: 1fr 1fr; } +} \ No newline at end of file