Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Nochmal - Reaction Trainer</title> | |
| <style> | |
| /* CSS Variables for consistent theming */ | |
| :root { | |
| --bg-color: #0f172a; | |
| --surface-color: #1e293b; | |
| --primary-color: #3b82f6; | |
| --accent-color: #8b5cf6; | |
| --success-color: #10b981; | |
| --error-color: #ef4444; | |
| --text-main: #f8fafc; | |
| --text-muted: #94a3b8; | |
| --font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| --card-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3); | |
| --transition-speed: 0.3s; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: var(--font-family); | |
| background-color: var(--bg-color); | |
| background-image: radial-gradient(circle at 10% 20%, #1e293b 0%, #0f172a 90%); | |
| color: var(--text-main); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow-x: hidden; | |
| } | |
| /* Header Styling */ | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 1.5rem 2rem; | |
| background: rgba(15, 23, 42, 0.8); | |
| backdrop-filter: blur(10px); | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .logo { | |
| font-size: 1.5rem; | |
| font-weight: 800; | |
| background: linear-gradient(to right, var(--primary-color), var(--accent-color)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| letter-spacing: -0.05em; | |
| } | |
| .anycoder-link { | |
| font-size: 0.85rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| padding: 0.5rem 1rem; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 20px; | |
| transition: all var(--transition-speed); | |
| } | |
| .anycoder-link:hover { | |
| color: var(--text-main); | |
| border-color: var(--primary-color); | |
| background: rgba(59, 130, 246, 0.1); | |
| } | |
| /* Main Layout */ | |
| main { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 2rem; | |
| gap: 2rem; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| /* Interactive Game Area */ | |
| .game-container { | |
| width: 100%; | |
| max-width: 600px; | |
| aspect-ratio: 16/9; | |
| background: var(--surface-color); | |
| border-radius: 24px; | |
| box-shadow: var(--card-shadow); | |
| position: relative; | |
| overflow: hidden; | |
| cursor: pointer; | |
| user-select: none; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; | |
| transition: transform 0.1s ease; | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| .game-container:active { | |
| transform: scale(0.98); | |
| } | |
| /* Dynamic background states for the game container */ | |
| .game-container.state-idle { | |
| border: 2px solid var(--primary-color); | |
| } | |
| .game-container.state-waiting { | |
| background: var(--error-color); | |
| border: none; | |
| } | |
| .game-container.state-go { | |
| background: var(--success-color); | |
| border: none; | |
| } | |
| .game-container.state-result { | |
| border: 2px solid var(--accent-color); | |
| } | |
| .icon-large { | |
| font-size: 4rem; | |
| margin-bottom: 1rem; | |
| opacity: 0.8; | |
| } | |
| .message-title { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| margin-bottom: 0.5rem; | |
| } | |
| .message-subtitle { | |
| font-size: 1rem; | |
| color: rgba(255, 255, 255, 0.8); | |
| } | |
| /* Stats Dashboard */ | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); | |
| gap: 1.5rem; | |
| width: 100%; | |
| max-width: 800px; | |
| } | |
| .stat-card { | |
| background: rgba(30, 41, 59, 0.5); | |
| backdrop-filter: blur(5px); | |
| padding: 1.5rem; | |
| border-radius: 16px; | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| text-align: center; | |
| transition: transform var(--transition-speed); | |
| } | |
| .stat-card:hover { | |
| transform: translateY(-5px); | |
| background: rgba(30, 41, 59, 0.8); | |
| } | |
| .stat-value { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| color: var(--primary-color); | |
| display: block; | |
| margin-bottom: 0.25rem; | |
| } | |
| .stat-label { | |
| font-size: 0.875rem; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| /* Controls */ | |
| .controls { | |
| display: flex; | |
| gap: 1rem; | |
| margin-top: 1rem; | |
| } | |
| .btn { | |
| padding: 1rem 2.5rem; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| border: none; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| transition: all var(--transition-speed); | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary-color), var(--accent-color)); | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4); | |
| } | |
| .btn-primary:hover { | |
| box-shadow: 0 6px 20px rgba(59, 130, 246, 0.6); | |
| transform: translateY(-2px); | |
| } | |
| .btn-primary:active { | |
| transform: translateY(0); | |
| } | |
| .btn-secondary { | |
| background: transparent; | |
| border: 2px solid var(--text-muted); | |
| color: var(--text-muted); | |
| } | |
| .btn-secondary:hover { | |
| border-color: var(--text-main); | |
| color: var(--text-main); | |
| } | |
| /* History List */ | |
| .history-container { | |
| width: 100%; | |
| max-width: 600px; | |
| background: rgba(15, 23, 42, 0.5); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| .history-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| padding-bottom: 0.5rem; | |
| } | |
| .history-list { | |
| list-style: none; | |
| max-height: 150px; | |
| overflow-y: auto; | |
| } | |
| .history-item { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 0.5rem 0; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05); | |
| font-size: 0.9rem; | |
| } | |
| .history-item:last-child { | |
| border-bottom: none; | |
| } | |
| .history-time { | |
| font-weight: 600; | |
| } | |
| .history-rank { | |
| color: var(--text-muted); | |
| } | |
| /* Footer */ | |
| footer { | |
| text-align: center; | |
| padding: 2rem; | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| } | |
| /* Utilities */ | |
| .hidden { | |
| display: none ; | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.3s ease-in; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* Mobile Adjustments */ | |
| @media (max-width: 600px) { | |
| .game-container { | |
| aspect-ratio: 1/1; | |
| } | |
| .message-title { | |
| font-size: 1.5rem; | |
| } | |
| .stats-grid { | |
| grid-template-columns: 1fr 1fr; | |
| } | |
| header { | |
| padding: 1rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo">NOCHMAL</div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder | |
| </a> | |
| </header> | |
| <main> | |
| <!-- The Interactive Game Stage --> | |
| <section id="game-stage" class="game-container state-idle" role="button" tabindex="0" aria-label="Game Area"> | |
| <div id="stage-content" class="fade-in"> | |
| <div class="icon-large">⚡</div> | |
| <div class="message-title">Reaction Test</div> | |
| <div class="message-subtitle">Click anywhere to start</div> | |
| </div> | |
| </section> | |
| <!-- Main Action Button --> | |
| <div class="controls"> | |
| <button id="reset-btn" class="btn btn-primary hidden">Nochmal</button> | |
| </div> | |
| <!-- Statistics Grid --> | |
| <section class="stats-grid"> | |
| <div class="stat-card"> | |
| <span id="last-score" class="stat-value">--</span> | |
| <span class="stat-label">Last Time (ms)</span> | |
| </div> | |
| <div class="stat-card"> | |
| <span id="best-score" class="stat-value">--</span> | |
| <span class="stat-label">Best Time (ms)</span> | |
| </div> | |
| <div class="stat-card"> | |
| <span id="avg-score" class="stat-value">--</span> | |
| <span class="stat-label">Average (ms)</span> | |
| </div> | |
| </section> | |
| <!-- History Section --> | |
| <section class="history-container"> | |
| <div class="history-header"> | |
| <h3>Recent Attempts</h3> | |
| <button id="clear-history" class="btn-secondary" style="padding: 0.25rem 0.75rem; font-size: 0.8rem; border-radius: 6px;">Clear</button> | |
| </div> | |
| <ul id="history-list" class="history-list"> | |
| <li class="history-item" style="justify-content: center; color: var(--text-muted);">No attempts yet</li> | |
| </ul> | |
| </section> | |
| </main> | |
| <footer> | |
| <p>Train your brain. Improve your focus. Try NOCHMAL.</p> | |
| </footer> | |
| <script> | |
| /** | |
| * NOCHMAL - Reaction Time Trainer Logic | |
| * Handles game states: idle, waiting, ready (go), result | |
| */ | |
| // --- DOM Elements --- | |
| const gameStage = document.getElementById('game-stage'); | |
| const stageContent = document.getElementById('stage-content'); | |
| const resetBtn = document.getElementById('reset-btn'); | |
| const lastScoreEl = document.getElementById('last-score'); | |
| const bestScoreEl = document.getElementById('best-score'); | |
| const avgScoreEl = document.getElementById('avg-score'); | |
| const historyList = document.getElementById('history-list'); | |
| const clearHistoryBtn = document.getElementById('clear-history'); | |
| // --- State Variables --- | |
| let gameState = 'idle'; // idle, waiting, ready, result | |
| let startTime = 0; | |
| let timeoutId = null; | |
| let scores = []; | |
| let bestScore = Infinity; | |
| // --- Event Listeners --- | |
| // Main interaction area click | |
| gameStage.addEventListener('mousedown', handleInteraction); | |
| // Touch support for mobile | |
| gameStage.addEventListener('touchstart', (e) => { | |
| e.preventDefault(); // Prevent double firing on some devices | |
| handleInteraction(); | |
| }); | |
| // Keyboard support (Space/Enter) | |
| gameStage.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| handleInteraction(); | |
| } | |
| }); | |
| // Reset button click | |
| resetBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); // Prevent bubbling to gameStage | |
| resetGame(); | |
| }); | |
| // Clear history | |
| clearHistoryBtn.addEventListener('click', () => { | |
| scores = []; | |
| bestScore = Infinity; | |
| updateStatsUI(); | |
| renderHistory(); | |
| }); | |
| // --- Core Functions --- | |
| function handleInteraction() { | |
| switch (gameState) { | |
| case 'idle': | |
| case 'result': | |
| prepareGame(); | |
| break; | |
| case 'waiting': | |
| triggerTooEarly(); | |
| break; | |
| case 'ready': | |
| triggerSuccess(); | |
| break; | |
| } | |
| } | |
| function prepareGame() { | |
| gameState = 'waiting'; | |
| setStageVisuals('waiting', '⏳', 'Wait for Green...', 'Do not click yet.'); | |
| resetBtn.classList.add('hidden'); | |
| // Random delay between 2 and 5 seconds | |
| const delay = Math.floor(Math.random() * 3000) + 2000; | |
| timeoutId = setTimeout(() => { | |
| triggerGo(); | |
| }, delay); | |
| } | |
| function triggerGo() { | |
| gameState = 'ready'; | |
| startTime = performance.now(); | |
| setStageVisuals('go', '⚡', 'CLICK NOW!', 'Fast!'); | |
| } | |
| function triggerTooEarly() { | |
| clearTimeout(timeoutId); | |
| gameState = 'result'; | |
| setStageVisuals('result', '⚠️', 'Too Early!', 'Click to try NOCHMAL.'); | |
| resetBtn.classList.remove('hidden'); | |
| } | |
| function triggerSuccess() { | |
| const endTime = performance.now(); | |
| const reactionTime = Math.round(endTime - startTime); | |
| gameState = 'result'; | |
| // Update Logic | |
| scores.push(reactionTime); | |
| if (reactionTime < bestScore) bestScore = reactionTime; | |
| // Update UI | |
| updateStatsUI(); | |
| renderHistory(); | |
| let message = reactionTime < 250 ? 'Godlike!' : | |
| reactionTime < 350 ? 'Great Job!' : | |
| reactionTime < 500 ? 'Good.' : 'Keep practicing.'; | |
| setStageVisuals('result', '⏱️', `${reactionTime} ms`, message); | |
| resetBtn.classList.remove('hidden'); | |
| } | |
| function resetGame() { | |
| gameState = 'idle'; | |
| setStageVisuals('idle', '⚡', 'Reaction Test', 'Click anywhere to start'); | |
| resetBtn.classList.add('hidden'); | |
| } | |
| // --- UI Helper Functions --- | |
| function setStageVisuals(stateClass, icon, title, subtitle) { | |
| // Remove all state classes | |
| gameStage.classList.remove('state-idle', 'state-waiting', 'state-go', 'state-result'); | |
| // Add current state class | |
| gameStage.classList.add(`state-${stateClass}`); | |
| // Update content with a small fade animation | |
| stageContent.classList.remove('fade-in'); | |
| void stageContent.offsetWidth; // Trigger reflow | |
| stageContent.classList.add('fade-in'); | |
| stageContent.innerHTML = ` | |
| <div class="icon-large">${icon}</div> | |
| <div class="message-title">${title}</div> | |
| <div class="message-subtitle">${subtitle}</div> | |
| `; | |
| } | |
| function updateStatsUI() { | |
| if (scores.length === 0) { | |
| lastScoreEl.textContent = '--'; | |
| bestScoreEl.textContent = '--'; | |
| avgScoreEl.textContent = '--'; | |
| return; | |
| } | |
| const last = scores[scores.length - 1]; | |
| const sum = scores.reduce((a, b) => a + b, 0); | |
| const avg = Math.round(sum / scores.length); | |
| lastScoreEl.textContent = last; | |
| bestScoreEl.textContent = bestScore; | |
| avgScoreEl.textContent = avg; | |
| // Color code the last score | |
| lastScoreEl.style.color = last < 300 ? 'var(--success-color)' : | |
| last < 500 ? 'var(--primary-color)' : 'var(--text-main)'; | |
| } | |
| function renderHistory() { | |
| if (scores.length === 0) { | |
| historyList.innerHTML = '<li class="history-item" style="justify-content: center; color: var(--text-muted);">No attempts yet</li>'; | |
| return; | |
| } | |
| // Show last 5 attempts reversed | |
| const recentScores = scores.slice(-5).reverse(); | |
| historyList.innerHTML = recentScores.map((score, index) => { | |
| // Determine rank styling | |
| let rankColor = 'var(--text-muted)'; | |
| if (score < 250) rankColor = 'var(--success-color)'; | |
| else if (score < 350) rankColor = 'var(--primary-color)'; | |
| return ` | |
| <li class="history-item fade-in"> | |
| <span class="history-rank">Attempt #${scores.length - index}</span> | |
| <span class="history-time" style="color: ${rankColor}">${score} ms</span> | |
| </li> | |
| `; | |
| }).join(''); | |
| } | |
| // Initialize UI | |
| resetGame(); | |
| </script> | |
| </body> | |
| </html> |