bunch of fixes
This commit is contained in:
1 parent
462c7908a1
commit
266a064330
14 files changed
+1211
-868
No files matched your search
+297
-193
@@ -1,200 +1,304 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Spelling App with Roles</title>
|
||||
<link href="styles.css" rel="stylesheet"/>
|
||||
<!-- 🔧 Script Load Order -->
|
||||
<script defer="" src="https://cdnjs.cloudflare.com/ajax/libs/jsSHA/2.4.2/sha.js"></script>
|
||||
<script defer="" src="js/auth.js"></script>
|
||||
<script defer="" src="js/ui.js"></script>
|
||||
<script defer="" src="js/themes.js"></script>
|
||||
<script src="js/game.js"></script>
|
||||
<script defer="" src="js/admin.js"></script>
|
||||
<script defer="" src="js/main.js"></script>
|
||||
<script defer="" src="js/typing.js"></script>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>✨ Modern Spelling Practice</title>
|
||||
<link href="styles.css" rel="stylesheet"/>
|
||||
<!-- Scripts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsSHA/2.4.2/sha.js"></script>
|
||||
<script src="js/admin.js"></script>
|
||||
<script src="js/auth.js"></script>
|
||||
<script src="js/ui.js"></script>
|
||||
<script src="js/themes.js"></script>
|
||||
<script src="js/game.js"></script>
|
||||
<script src="js/typing.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Spelling Practice</h1>
|
||||
<!-- 🏳️ Role Banner -->
|
||||
<div class="banner hidden" id="roleBanner"></div>
|
||||
<!-- 👤 Login Panel -->
|
||||
<div id="loginPanel">
|
||||
<h2>User Login</h2>
|
||||
<form onsubmit="event.preventDefault(); verifyLogin();">
|
||||
<input id="username" placeholder="Username" required="" type="text"/>
|
||||
<input id="password" placeholder="Password" required="" type="password"/>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<p id="loginMessage"></p>
|
||||
</div>
|
||||
<!-- 🔀 Role-Based Navigation -->
|
||||
<div class="hidden" id="nav">
|
||||
<p><strong id="userInfo"></strong></p>
|
||||
<button class="hidden" id="adminBtn" onclick="showAdmin()">Admin Panel</button>
|
||||
<button class="hidden" id="studentBtn" onclick="showStudent()">Student Panel</button>
|
||||
<button id="logoutBtn" onclick="logoutUser()">Log Out</button>
|
||||
</div>
|
||||
<!-- 👨🏫 Admin Panel -->
|
||||
<div class="hidden" id="adminPanel">
|
||||
<h2>👨🏫 Admin Panel</h2>
|
||||
<!-- 🔘 Tabs -->
|
||||
<div id="adminTabs">
|
||||
<button class="active" onclick="switchTab('tabUsers')">👤 Manage Users</button>
|
||||
<button onclick="switchTab('tabWords')">📝 Word Lists</button>
|
||||
<button onclick="switchTab('tabAnalytics')">📊 Analytics</button>
|
||||
<button onclick="switchTab('tabResults')">📊 Results</button>
|
||||
<button onclick="switchTab('tabReports')">📘 Reports</button>
|
||||
<button onclick="switchTab('tabSessions')">📆 Sessions</button>
|
||||
<button onclick="switchTab('tabBadges')">🎖 Badges</button>
|
||||
<button onclick="switchTab('tabTheme')">🎨 Theme</button>
|
||||
<button onclick="switchTab('tabTyping')">⌨️ Typing Practice</button>
|
||||
</div>
|
||||
<!-- 👥 Users Tab -->
|
||||
<div class="adminTab" id="tabUsers">
|
||||
<h3>Add New User</h3>
|
||||
<form onsubmit="event.preventDefault(); addNewUser();">
|
||||
<input id="newUsername" placeholder="Username" required="" type="text"/>
|
||||
<input id="newPassword" placeholder="Password" required="" type="password"/>
|
||||
<select id="newRole">
|
||||
<option value="student">Student</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<button type="submit">Add User</button>
|
||||
</form>
|
||||
<h3>Delete Users</h3>
|
||||
<select id="userDropdown" onchange="selectUserAction(this.value)">
|
||||
<option value="">-- Select a user --</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 📝 Word List Tab -->
|
||||
<div class="adminTab hidden" id="tabWords">
|
||||
<h3>Edit Word List for:</h3>
|
||||
<select class="word-user-dropdown" id="wordUserSelect" onchange="loadWordsForSelectedUser()">
|
||||
<option value="">-- Select a user --</option>
|
||||
</select>
|
||||
<h2>Enter Spelling Words</h2>
|
||||
<textarea id="wordInput" placeholder="Enter one word per line..."></textarea><br/>
|
||||
<button onclick="saveWords()">Save Word List</button>
|
||||
<button onclick="clearWords()">Clear Saved Words</button>
|
||||
<h3>Upload a Word File</h3>
|
||||
<input accept=".txt,.csv" onchange="handleFileUpload(event)" type="file"/>
|
||||
<p><strong>Current Word List:</strong></p>
|
||||
<ul id="wordListDisplay"></ul>
|
||||
<h3>📋 View Word List by User:</h3>
|
||||
<select class="word-user-dropdown" id="viewUserSelect" onchange="displayWordListForSelectedUser()">
|
||||
<option value="">-- Select a user --</option>
|
||||
</select>
|
||||
<ul id="userWordListDisplay"></ul>
|
||||
</div>
|
||||
<!-- 📊 Results Tab -->
|
||||
<div class="adminTab hidden" id="tabResults">
|
||||
<h3>🧾 Student Results Overview</h3>
|
||||
<button onclick="loadStudentResults()">Refresh Results</button>
|
||||
<ul id="resultsList"></ul>
|
||||
</div>
|
||||
<!-- 📊 Analytics Tab -->
|
||||
<div class="adminTab hidden" id="tabAnalytics">
|
||||
<h3>Smart Analytics Overview</h3>
|
||||
<ul id="analyticsSummary">
|
||||
<li><strong>Average Score:</strong> –</li>
|
||||
<li><strong>Total Completed Sessions:</strong> –</li>
|
||||
<li><strong>Most Missed Word:</strong> –</li>
|
||||
</ul>
|
||||
<h4>Per-Student Breakdown</h4>
|
||||
<ul id="studentBreakdown"></ul>
|
||||
<button onclick="refreshAnalytics()">🔄 Refresh</button>
|
||||
</div>
|
||||
<!-- 🎖 Badges -->
|
||||
<div class="adminTab hidden" id="tabBadges">
|
||||
<h3>🎖 Badge Viewer</h3>
|
||||
<ul id="badgeUserList"></ul>
|
||||
<div class="hidden" id="badgeDetails">
|
||||
<h4 id="badgeUserTitle"></h4>
|
||||
<ul id="badgeUserBadges"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 🎨 Theme Tab -->
|
||||
<div class="adminTab hidden" id="tabTheme">
|
||||
<h3>🎨 Select Theme</h3>
|
||||
<select id="themeSelect" onchange="changeTheme(this.value)">
|
||||
<option value="playground">Playground</option>
|
||||
<option value="chalkboard">Chalkboard</option>
|
||||
<option value="galaxy">Galaxy</option>
|
||||
<option value="nature">Nature</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ⌨️ Typing Practice Tab -->
|
||||
<div class="adminTab hidden" id="tabTyping">
|
||||
<h3>⌨️ Typing Challenge Preview</h3>
|
||||
<label for="typingUserSelect">Select Student:</label>
|
||||
<select class="word-user-dropdown" id="typingUserSelect" onchange="loadTypingWords(this.value)">
|
||||
<option value="">-- Select a user --</option>
|
||||
</select>
|
||||
<button onclick="startTypingTest()">Start Typing Test</button>
|
||||
<div class="hidden" id="typingTestArea">
|
||||
<p><strong>Practice Sentence:</strong></p>
|
||||
<p class="typing-sentence" id="typingPrompt"></p>
|
||||
<input autocomplete="off" id="typingInput" placeholder="Type the spelling word here..." type="text"/>
|
||||
<button onclick="submitTyping()">Submit</button>
|
||||
<p id="typingFeedback"></p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 🧾 Student Report Viewer -->
|
||||
<div class="adminTab hidden" id="tabReports">
|
||||
<h3>📘 Student Progress Report</h3>
|
||||
<label for="reportUser">Select Student:</label>
|
||||
<select id="reportUser" onchange="loadStudentReport(this.value)">
|
||||
<option value="">-- Select --</option>
|
||||
</select>
|
||||
<div class="hidden" id="reportContent">
|
||||
<h4 id="reportTitle"></h4>
|
||||
<p><strong>Last Score:</strong> <span id="reportScore"></span></p>
|
||||
<p><strong>Words Attempted:</strong></p>
|
||||
<ul id="reportWords"></ul>
|
||||
<p><strong>Badges Earned:</strong></p>
|
||||
<ul id="reportBadges"></ul>
|
||||
<button onclick="window.print()">🖨️ Print Report</button>
|
||||
<button onclick="copyReportToClipboard()">📋 Copy to Clipboard</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="adminTab hidden" id="tabSessions">
|
||||
<h3>📘 Archived Sessions</h3>
|
||||
<label for="sessionUserSelect">Select Student:</label>
|
||||
<select id="sessionUserSelect" onchange="loadUserSessions(this.value)">
|
||||
<option value="">-- Select --</option>
|
||||
</select>
|
||||
<ul id="userSessionList"></ul>
|
||||
</div>
|
||||
<!-- 👩🎓 Student Panel -->
|
||||
<div class="hidden" id="studentPanel">
|
||||
<h2>Student Dashboard</h2>
|
||||
<div class="student-controls">
|
||||
<button id="startGameBtn" onclick="startGame()">Start Spelling Game</button>
|
||||
<button id="startTypingBtn" onclick="startTypingPractice()">Start Typing Practice</button>
|
||||
</div>
|
||||
<!-- Game Section -->
|
||||
<div class="hidden" id="gameSection">
|
||||
<h3>🎮 Spelling Game</h3>
|
||||
<div class="word-box" id="wordBox"></div>
|
||||
<div class="hidden" id="inputSection">
|
||||
<input id="userInput" placeholder="Type the word you saw..." type="text"/>
|
||||
<button onclick="submitAnswer()">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Typing Practice Section -->
|
||||
<div class="hidden" id="typingSection">
|
||||
<h3>Typing Practice</h3>
|
||||
<p id="typingPrompt">Type the word...</p>
|
||||
<input id="typingInput" type="text"/>
|
||||
<button onclick="submitTyping()">Submit</button>
|
||||
<p id="typingFeedback"></p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
<div class="container">
|
||||
<h1>✨ Spelling Practice</h1>
|
||||
|
||||
<!-- Role Banner -->
|
||||
<div class="banner hidden" id="roleBanner"></div>
|
||||
|
||||
<!-- Login Panel -->
|
||||
<div id="loginPanel" class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="text-center">Welcome Back</h2>
|
||||
<p class="text-center" style="color: var(--text-secondary);">Sign in to continue your spelling practice</p>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); verifyLogin();">
|
||||
<input id="username" placeholder="Username" required type="text"/>
|
||||
<input id="password" placeholder="Password" required type="password"/>
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
<p id="loginMessage" style="margin-top: 1rem; text-align: center;"></p>
|
||||
</div>
|
||||
<!-- Navigation -->
|
||||
<div id="nav" class="hidden">
|
||||
<p><strong id="userInfo"></strong></p>
|
||||
<div class="flex gap-4 justify-center" style="margin-top: 1rem;">
|
||||
<button class="hidden" id="adminBtn" onclick="showAdmin()">👨💼 Admin Panel</button>
|
||||
<button class="hidden" id="studentBtn" onclick="showStudent()">🎓 Student Panel</button>
|
||||
<button id="logoutBtn" onclick="logoutUser()" class="btn-secondary">Sign Out</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Admin Panel -->
|
||||
<div class="hidden" id="adminPanel">
|
||||
<div class="card-header">
|
||||
<h2>👨💼 Admin Dashboard</h2>
|
||||
<p style="color: var(--text-secondary);">Manage users, word lists, and view analytics</p>
|
||||
</div>
|
||||
|
||||
<!-- Admin Tabs -->
|
||||
<div id="adminTabs">
|
||||
<button class="active" onclick="switchTab('tabUsers')">👤 Users</button>
|
||||
<button onclick="switchTab('tabWords')">📝 Word Lists</button>
|
||||
<button onclick="switchTab('tabAnalytics')">📊 Analytics</button>
|
||||
<button onclick="switchTab('tabResults')">� Results</button>
|
||||
<button onclick="switchTab('tabReports')">📘 Reports</button>
|
||||
<button onclick="switchTab('tabSessions')">� Sessions</button>
|
||||
<button onclick="switchTab('tabBadges')">� Badges</button>
|
||||
<button onclick="switchTab('tabTheme')">🎨 Themes</button>
|
||||
</div>
|
||||
<!-- Users Tab -->
|
||||
<div id="tabUsers" class="adminTab">
|
||||
<h3>👤 User Management</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 1.5rem;">
|
||||
<div class="card">
|
||||
<h4>Add New User</h4>
|
||||
<form onsubmit="event.preventDefault(); addNewUser();" style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<input id="newUsername" placeholder="Username" required type="text"/>
|
||||
<input id="newPassword" placeholder="Password" required type="password"/>
|
||||
<select id="newRole">
|
||||
<option value="student">🎓 Student</option>
|
||||
<option value="admin">👨💼 Admin</option>
|
||||
</select>
|
||||
<button type="submit" class="btn-success">➕ Add User</button>
|
||||
</form>
|
||||
<p id="userAddMessage" style="margin-top: 1rem;"></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h4>Remove User</h4>
|
||||
<select id="userDropdown" onchange="selectUserAction(this.value)" style="width: 100%;">
|
||||
<option value="">-- Select a user to remove --</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Word Lists Tab -->
|
||||
<div class="adminTab hidden" id="tabWords">
|
||||
<h3>📝 Word List Management</h3>
|
||||
<div style="display: grid; gap: 2rem;">
|
||||
<div class="card">
|
||||
<h4>Select Student</h4>
|
||||
<select class="word-user-dropdown" id="wordUserSelect" onchange="loadWordsForSelectedUser()" style="width: 100%;">
|
||||
<option value="">-- Select a student --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h4>Manage Word List</h4>
|
||||
<textarea id="wordInput" placeholder="Enter one word per line..."></textarea>
|
||||
<div style="display: flex; gap: 1rem; margin-top: 1rem;">
|
||||
<button onclick="saveWords()" class="btn-success">💾 Save Words</button>
|
||||
<button onclick="clearWords()" class="btn-error">🗑️ Clear All</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1.5rem;">
|
||||
<h5>Upload Word File</h5>
|
||||
<input accept=".txt,.csv" onchange="handleFileUpload(event)" type="file" style="margin: 0.5rem 0;"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h4>Current Word List</h4>
|
||||
<ul id="wordListDisplay" style="max-height: 200px; overflow-y: auto;"></ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h4>View by Student</h4>
|
||||
<select class="word-user-dropdown" id="viewUserSelect" onchange="displayWordListForSelectedUser()" style="width: 100%;">
|
||||
<option value="">-- Select a student --</option>
|
||||
</select>
|
||||
<ul id="userWordListDisplay" style="max-height: 200px; overflow-y: auto; margin-top: 1rem;"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Results Tab -->
|
||||
<div class="adminTab hidden" id="tabResults">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📋 Student Results Overview</h3>
|
||||
<button onclick="loadStudentResults()" class="btn-primary">🔄 Refresh Results</button>
|
||||
</div>
|
||||
<ul id="resultsList" style="max-height: 400px; overflow-y: auto;"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Analytics Tab -->
|
||||
<div class="adminTab hidden" id="tabAnalytics">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>📊 Performance Analytics</h3>
|
||||
<button onclick="refreshAnalytics()" class="btn-primary">🔄 Refresh Analytics</button>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||
<div>
|
||||
<h4>Summary Statistics</h4>
|
||||
<ul id="analyticsSummary" style="background: var(--bg-secondary); border-radius: var(--radius); padding: 1rem;">
|
||||
<li><strong>Average Score:</strong> –</li>
|
||||
<li><strong>Total Sessions:</strong> –</li>
|
||||
<li><strong>Most Missed Word:</strong> –</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Student Breakdown</h4>
|
||||
<ul id="studentBreakdown" style="max-height: 300px; overflow-y: auto;"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Badges Tab -->
|
||||
<div class="adminTab hidden" id="tabBadges">
|
||||
<div class="card">
|
||||
<h3>� Student Badges</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 2rem;">
|
||||
<div>
|
||||
<h4>Select Student</h4>
|
||||
<ul id="badgeUserList"></ul>
|
||||
</div>
|
||||
<div class="hidden" id="badgeDetails">
|
||||
<h4 id="badgeUserTitle"></h4>
|
||||
<ul id="badgeUserBadges"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Theme Tab -->
|
||||
<div class="adminTab hidden" id="tabTheme">
|
||||
<div class="card">
|
||||
<h3>🎨 Application Themes</h3>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1.5rem;">Choose a visual theme for the application</p>
|
||||
<select id="themeSelect" onchange="changeTheme(this.value)" style="width: 100%; max-width: 300px;">
|
||||
<option value="default">✨ Modern (Default)</option>
|
||||
<option value="playground">🎪 Playground</option>
|
||||
<option value="chalkboard">🎓 Chalkboard</option>
|
||||
<option value="galaxy">🌌 Galaxy</option>
|
||||
<option value="nature">🌿 Nature</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Reports Tab -->
|
||||
<div class="adminTab hidden" id="tabReports">
|
||||
<div class="card">
|
||||
<h3>📘 Student Progress Reports</h3>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<label for="reportUser">Select Student:</label>
|
||||
<select id="reportUser" onchange="loadStudentReport(this.value)" style="width: 100%; max-width: 300px;">
|
||||
<option value="">-- Select a student --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="hidden" id="reportContent">
|
||||
<div class="card" style="background: var(--bg-secondary);">
|
||||
<h4 id="reportTitle"></h4>
|
||||
<p><strong>Latest Score:</strong> <span id="reportScore"></span></p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 1.5rem;">
|
||||
<div>
|
||||
<h5>Words Attempted</h5>
|
||||
<ul id="reportWords" style="max-height: 200px; overflow-y: auto;"></ul>
|
||||
</div>
|
||||
<div>
|
||||
<h5>Badges Earned</h5>
|
||||
<ul id="reportBadges"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 2rem; display: flex; gap: 1rem;">
|
||||
<button onclick="window.print()" class="btn-secondary">🖨️ Print Report</button>
|
||||
<button onclick="copyReportToClipboard()" class="btn-primary">📋 Copy to Clipboard</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sessions Tab -->
|
||||
<div class="adminTab hidden" id="tabSessions">
|
||||
<div class="card">
|
||||
<h3>� Session History</h3>
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<label for="sessionUserSelect">Select Student:</label>
|
||||
<select id="sessionUserSelect" onchange="loadUserSessions(this.value)" style="width: 100%; max-width: 300px;">
|
||||
<option value="">-- Select a student --</option>
|
||||
</select>
|
||||
</div>
|
||||
<ul id="userSessionList" style="max-height: 400px; overflow-y: auto;"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Student Panel -->
|
||||
<div class="hidden" id="studentPanel">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>🎓 Student Dashboard</h2>
|
||||
<p style="color: var(--text-secondary);">Choose an activity to start practicing</p>
|
||||
</div>
|
||||
|
||||
<div class="student-controls">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; max-width: 600px; margin: 0 auto;">
|
||||
<button id="startGameBtn" onclick="startGame()" class="btn-success">
|
||||
🎮 Spelling Game
|
||||
</button>
|
||||
<button id="startTypingBtn" onclick="startTypingPractice()" class="btn-warning">
|
||||
⌨️ Typing Practice
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Section -->
|
||||
<div class="hidden" id="gameSection">
|
||||
<h3>🎮 Spelling Challenge</h3>
|
||||
<div class="word-box" id="wordBox"></div>
|
||||
<div class="hidden" id="inputSection" style="text-align: center;">
|
||||
<input id="userInput" placeholder="Type the word you saw..." type="text" style="max-width: 400px;"/>
|
||||
<button onclick="submitAnswer()" class="btn-primary">Submit Answer</button>
|
||||
</div>
|
||||
<!-- Game Summary -->
|
||||
<div class="hidden" id="summary" class="card"></div>
|
||||
<!-- Badge Display -->
|
||||
<div class="hidden" id="badgeDisplay" class="card">
|
||||
<h3>🏅 Congratulations!</h3>
|
||||
<p>You've earned new badges:</p>
|
||||
<ul id="badgeList"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Typing Practice Section -->
|
||||
<div class="hidden" id="typingSection">
|
||||
<h3>⌨️ Typing Practice</h3>
|
||||
<div id="typingPrompt">Get ready to type...</div>
|
||||
<div style="text-align: center; margin: 2rem 0;">
|
||||
<input id="typingInput" type="text" style="max-width: 500px;"/>
|
||||
<button onclick="submitTyping()" class="btn-primary">Submit</button>
|
||||
</div>
|
||||
<p id="typingFeedback" style="text-align: center; font-weight: 600;"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast notification -->
|
||||
<div id="toast" class="toast"></div>
|
||||
</div> <script>
|
||||
// Initialize the application when DOM is loaded
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||
if (user) {
|
||||
@@ -206,5 +310,5 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<div id="spellingSection"><h3>Spelling Game</h3><p id="spellingPrompt">Word will appear here...</p></div></body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,245 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Spelling App with Roles</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
|
||||
<!-- 🔧 Script Load Order -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsSHA/2.4.2/sha.js" defer></script>
|
||||
<script src="js/auth.js" defer></script>
|
||||
<script src="js/ui.js" defer></script>
|
||||
<script src="js/themes.js" defer></script>
|
||||
<script src="js/game.js"></script>
|
||||
<script src="js/admin.js" defer></script>
|
||||
<script src="js/main.js" defer></script>
|
||||
<script src="js/typing.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Spelling Practice</h1>
|
||||
|
||||
<!-- 🏳️ Role Banner -->
|
||||
<div id="roleBanner" class="banner hidden"></div>
|
||||
|
||||
<!-- 👤 Login Panel -->
|
||||
<div id="loginPanel">
|
||||
<h2>User Login</h2>
|
||||
<form onsubmit="event.preventDefault(); verifyLogin();">
|
||||
<input type="text" id="username" placeholder="Username" required />
|
||||
<input type="password" id="password" placeholder="Password" required />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<p id="loginMessage"></p>
|
||||
</div>
|
||||
|
||||
<!-- 🔀 Role-Based Navigation -->
|
||||
<div id="nav" class="hidden">
|
||||
<p><strong id="userInfo"></strong></p>
|
||||
<button onclick="showAdmin()" id="adminBtn" class="hidden">Admin Panel</button>
|
||||
<button onclick="showStudent()" id="studentBtn" class="hidden">Student Panel</button>
|
||||
<button onclick="logoutUser()" id="logoutBtn">Log Out</button>
|
||||
</div>
|
||||
|
||||
<!-- 👨🏫 Admin Panel -->
|
||||
<div id="adminPanel" class="hidden">
|
||||
<h2>👨🏫 Admin Panel</h2>
|
||||
|
||||
<!-- 🔘 Tabs -->
|
||||
<div id="adminTabs">
|
||||
<button onclick="switchTab('tabUsers')" class="active">👤 Manage Users</button>
|
||||
<button onclick="switchTab('tabWords')">📝 Word Lists</button>
|
||||
<button onclick="switchTab('tabAnalytics')">📊 Analytics</button>
|
||||
<button onclick="switchTab('tabResults')">📊 Results</button>
|
||||
<button onclick="switchTab('tabReports')">📘 Reports</button>
|
||||
<button onclick="switchTab('tabSessions')">📆 Sessions</button>
|
||||
<button onclick="switchTab('tabBadges')">🎖 Badges</button>
|
||||
<button onclick="switchTab('tabTheme')">🎨 Theme</button>
|
||||
<button onclick="switchTab('tabTyping')">⌨️ Typing Practice</button>
|
||||
</div>
|
||||
|
||||
<!-- 👥 Users Tab -->
|
||||
<div id="tabUsers" class="adminTab">
|
||||
<h3>Add New User</h3>
|
||||
<form onsubmit="event.preventDefault(); addNewUser();">
|
||||
<input type="text" id="newUsername" placeholder="Username" required />
|
||||
<input type="password" id="newPassword" placeholder="Password" required />
|
||||
<select id="newRole">
|
||||
<option value="student">Student</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<button type="submit">Add User</button>
|
||||
</form>
|
||||
|
||||
<h3>Delete Users</h3>
|
||||
<select id="userDropdown" onchange="selectUserAction(this.value)">
|
||||
<option value="">-- Select a user --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 📝 Word List Tab -->
|
||||
<div id="tabWords" class="adminTab hidden">
|
||||
<h3>Edit Word List for:</h3>
|
||||
<select id="wordUserSelect" class="word-user-dropdown" onchange="loadWordsForSelectedUser()">
|
||||
<option value="">-- Select a user --</option>
|
||||
</select>
|
||||
|
||||
<h2>Enter Spelling Words</h2>
|
||||
<textarea id="wordInput" placeholder="Enter one word per line..."></textarea><br/>
|
||||
<button onclick="saveWords()">Save Word List</button>
|
||||
<button onclick="clearWords()">Clear Saved Words</button>
|
||||
|
||||
<h3>Upload a Word File</h3>
|
||||
<input type="file" accept=".txt,.csv" onchange="handleFileUpload(event)" />
|
||||
|
||||
<p><strong>Current Word List:</strong></p>
|
||||
<ul id="wordListDisplay"></ul>
|
||||
|
||||
<h3>📋 View Word List by User:</h3>
|
||||
<select id="viewUserSelect" class="word-user-dropdown" onchange="displayWordListForSelectedUser()">
|
||||
<option value="">-- Select a user --</option>
|
||||
</select>
|
||||
<ul id="userWordListDisplay"></ul>
|
||||
</div>
|
||||
|
||||
<!-- 📊 Results Tab -->
|
||||
<div id="tabResults" class="adminTab hidden">
|
||||
<h3>🧾 Student Results Overview</h3>
|
||||
<button onclick="loadStudentResults()">Refresh Results</button>
|
||||
<ul id="resultsList"></ul>
|
||||
</div>
|
||||
|
||||
<!-- 📊 Analytics Tab -->
|
||||
<div id="tabAnalytics" class="adminTab hidden">
|
||||
<h3>Smart Analytics Overview</h3>
|
||||
<ul id="analyticsSummary">
|
||||
<li><strong>Average Score:</strong> –</li>
|
||||
<li><strong>Total Completed Sessions:</strong> –</li>
|
||||
<li><strong>Most Missed Word:</strong> –</li>
|
||||
</ul>
|
||||
|
||||
<h4>Per-Student Breakdown</h4>
|
||||
<ul id="studentBreakdown"></ul>
|
||||
|
||||
<button onclick="refreshAnalytics()">🔄 Refresh</button>
|
||||
</div>
|
||||
|
||||
<!-- 🎖 Badges -->
|
||||
<div id="tabBadges" class="adminTab hidden">
|
||||
<h3>🎖 Badge Viewer</h3>
|
||||
<ul id="badgeUserList"></ul>
|
||||
<div id="badgeDetails" class="hidden">
|
||||
<h4 id="badgeUserTitle"></h4>
|
||||
<ul id="badgeUserBadges"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 🎨 Theme Tab -->
|
||||
<div id="tabTheme" class="adminTab hidden">
|
||||
<h3>🎨 Select Theme</h3>
|
||||
<select id="themeSelect" onchange="changeTheme(this.value)">
|
||||
<option value="playground">Playground</option>
|
||||
<option value="chalkboard">Chalkboard</option>
|
||||
<option value="galaxy">Galaxy</option>
|
||||
<option value="nature">Nature</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ⌨️ Typing Practice Tab -->
|
||||
<div id="tabTyping" class="adminTab hidden">
|
||||
<h3>⌨️ Typing Challenge Preview</h3>
|
||||
|
||||
<label for="typingUserSelect">Select Student:</label>
|
||||
<select id="typingUserSelect" class="word-user-dropdown" onchange="loadTypingWords(this.value)">
|
||||
<option value="">-- Select a user --</option>
|
||||
</select>
|
||||
|
||||
<button onclick="startTypingTest()">Start Typing Test</button>
|
||||
|
||||
<div id="typingTestArea" class="hidden">
|
||||
<p><strong>Practice Sentence:</strong></p>
|
||||
<p id="typingPrompt" class="typing-sentence"></p>
|
||||
<input type="text" id="typingInput" placeholder="Type the spelling word here..." autocomplete="off" />
|
||||
<button onclick="submitTyping()">Submit</button>
|
||||
<p id="typingFeedback"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 🧾 Student Report Viewer -->
|
||||
<div id="tabReports" class="adminTab hidden">
|
||||
<h3>📘 Student Progress Report</h3>
|
||||
<label for="reportUser">Select Student:</label>
|
||||
<select id="reportUser" onchange="loadStudentReport(this.value)">
|
||||
<option value="">-- Select --</option>
|
||||
</select>
|
||||
|
||||
<div id="reportContent" class="hidden">
|
||||
<h4 id="reportTitle"></h4>
|
||||
<p><strong>Last Score:</strong> <span id="reportScore"></span></p>
|
||||
<p><strong>Words Attempted:</strong></p>
|
||||
<ul id="reportWords"></ul>
|
||||
<p><strong>Badges Earned:</strong></p>
|
||||
<ul id="reportBadges"></ul>
|
||||
|
||||
<button onclick="window.print()">🖨️ Print Report</button>
|
||||
<button onclick="copyReportToClipboard()">📋 Copy to Clipboard</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tabSessions" class="adminTab hidden">
|
||||
<h3>📘 Archived Sessions</h3>
|
||||
<label for="sessionUserSelect">Select Student:</label>
|
||||
<select id="sessionUserSelect" onchange="loadUserSessions(this.value)">
|
||||
<option value="">-- Select --</option>
|
||||
</select>
|
||||
|
||||
<ul id="userSessionList"></ul>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 👩🎓 Student Panel -->
|
||||
<div id="studentPanel" class="hidden">
|
||||
<h2>Student Dashboard</h2>
|
||||
|
||||
<div class="student-controls">
|
||||
<button id="startGameBtn" onclick="startGame()">Start Spelling Game</button>
|
||||
<button id="startTypingBtn" onclick="startTypingPractice()">Start Typing Practice</button>
|
||||
</div>
|
||||
|
||||
<!-- Game Section -->
|
||||
<div id="gameSection" class="hidden">
|
||||
<h3>🎮 Spelling Game</h3>
|
||||
<div id="wordBox" class="word-box"></div>
|
||||
<div id="inputSection" class="hidden">
|
||||
<input type="text" id="userInput" placeholder="Type the word you saw..." />
|
||||
<button onclick="submitAnswer()">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Typing Practice Section -->
|
||||
|
||||
<div id="typingSection" class="hidden">
|
||||
<h3>Typing Practice</h3>
|
||||
<p id="typingPrompt">Type the word...</p>
|
||||
<input type="text" id="typingInput" />
|
||||
<button onclick="submitTyping()">Submit</button>
|
||||
<p id="typingFeedback"></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||
if (user) {
|
||||
if (user.role === 'admin') {
|
||||
showAdmin();
|
||||
} else {
|
||||
showStudent();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+229
-86
@@ -1,3 +1,84 @@
|
||||
// Global variables for admin functionality
|
||||
let allWords = [];
|
||||
let adminUsers = [];
|
||||
|
||||
// 🧭 Admin tab switcher (make it globally accessible)
|
||||
window.switchTab = function switchTab(targetId) {
|
||||
console.log('🔄 Switching to tab:', targetId);
|
||||
|
||||
// Hide all tabs
|
||||
document.querySelectorAll('.adminTab').forEach(tab => {
|
||||
tab.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Remove active class from all buttons
|
||||
document.querySelectorAll('#adminTabs button').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show the selected tab
|
||||
const targetTab = document.getElementById(targetId);
|
||||
if (targetTab) {
|
||||
targetTab.classList.remove('hidden');
|
||||
console.log('✅ Showing tab:', targetId);
|
||||
} else {
|
||||
console.error('❌ Tab not found:', targetId);
|
||||
}
|
||||
|
||||
// Highlight the active button
|
||||
document.querySelectorAll('#adminTabs button').forEach(btn => {
|
||||
const onclick = btn.getAttribute('onclick');
|
||||
if (onclick && onclick.includes(targetId)) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 🔁 Trigger tab-specific logic
|
||||
switch(targetId) {
|
||||
case 'tabSessions':
|
||||
if (typeof populateSessionUserDropdown === 'function') {
|
||||
populateSessionUserDropdown();
|
||||
}
|
||||
const sessionList = document.getElementById("userSessionList");
|
||||
if (sessionList) sessionList.innerHTML = "";
|
||||
break;
|
||||
case 'tabReports':
|
||||
if (typeof loadStudentNamesForReport === 'function') {
|
||||
loadStudentNamesForReport();
|
||||
}
|
||||
break;
|
||||
case 'tabBadges':
|
||||
if (typeof loadBadgeViewer === 'function') {
|
||||
loadBadgeViewer();
|
||||
}
|
||||
break;
|
||||
case 'tabAnalytics':
|
||||
if (typeof refreshAnalytics === 'function') {
|
||||
refreshAnalytics();
|
||||
}
|
||||
break;
|
||||
case 'tabResults':
|
||||
if (typeof loadStudentResults === 'function') {
|
||||
loadStudentResults();
|
||||
}
|
||||
break;
|
||||
case 'tabWords':
|
||||
loadUserDropdowns();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Load users from server
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const res = await fetch('/getUsers');
|
||||
adminUsers = await res.json();
|
||||
} catch (err) {
|
||||
console.error("Failed to load users:", err);
|
||||
adminUsers = [];
|
||||
}
|
||||
}
|
||||
|
||||
// ➕ Add a new user
|
||||
async function addNewUser() {
|
||||
const username = document.getElementById("newUsername").value.trim();
|
||||
@@ -26,8 +107,9 @@ async function addNewUser() {
|
||||
document.getElementById("newPassword").value = '';
|
||||
if (hashPreview) hashPreview.textContent = '[auto]';
|
||||
|
||||
await loadUsers();
|
||||
displayUserDropdown();
|
||||
await loadUsers();
|
||||
displayUserDropdown();
|
||||
populateWordUserDropdown();
|
||||
} catch (e) {
|
||||
msg.textContent = "❌ Failed to add user.";
|
||||
console.error("Add user error:", e);
|
||||
@@ -38,7 +120,7 @@ async function addNewUser() {
|
||||
function displayUserDropdown() {
|
||||
const dropdown = document.getElementById("userDropdown");
|
||||
dropdown.innerHTML = `<option value="">-- Select a user --</option>`;
|
||||
users.forEach(user => {
|
||||
adminUsers.forEach(user => {
|
||||
dropdown.innerHTML += `<option value="${user.username}">${user.username} (${user.role})</option>`;
|
||||
});
|
||||
}
|
||||
@@ -104,8 +186,9 @@ async function loadWordsForSelectedUser() {
|
||||
const selectedUser = document.getElementById("wordUserSelect").value;
|
||||
if (!selectedUser) return;
|
||||
|
||||
const res = await fetch(`/getWordList?user=${encodeURIComponent(selectedUser)}`);
|
||||
allWords = await res.json();
|
||||
const res = await fetch(`/getWordList?username=${encodeURIComponent(selectedUser)}`);
|
||||
const data = await res.json();
|
||||
allWords = data.words || [];
|
||||
displayWordList();
|
||||
}
|
||||
|
||||
@@ -118,7 +201,7 @@ function populateWordUserDropdown() {
|
||||
const selects = document.querySelectorAll(".word-user-dropdown");
|
||||
selects.forEach(select => {
|
||||
select.innerHTML = '<option value="">-- Select a user --</option>';
|
||||
users.forEach(user => {
|
||||
adminUsers.forEach(user => {
|
||||
const option = document.createElement("option");
|
||||
option.value = user.username;
|
||||
option.textContent = user.username;
|
||||
@@ -136,8 +219,9 @@ async function displayWordListForSelectedUser() {
|
||||
if (!username) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/getWordList?user=${username}`);
|
||||
const words = await res.json();
|
||||
const res = await fetch(`/getWordList?username=${username}`);
|
||||
const data = await res.json();
|
||||
const words = data.words || [];
|
||||
if (!words.length) {
|
||||
list.innerHTML = "<li><em>No words found for this user.</em></li>";
|
||||
return;
|
||||
@@ -154,7 +238,7 @@ async function displayWordListForSelectedUser() {
|
||||
}
|
||||
|
||||
// 📊 Student result viewer
|
||||
async function loadStudentResults() {
|
||||
window.loadStudentResults = async function() {
|
||||
const list = document.getElementById("resultsList");
|
||||
list.innerHTML = '';
|
||||
|
||||
@@ -167,19 +251,38 @@ async function loadStudentResults() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [username, result] of Object.entries(results)) {
|
||||
const li = document.createElement("li");
|
||||
let answersHTML = '';
|
||||
result.answers.forEach(entry => {
|
||||
const status = entry.correct ? '✅' : '❌';
|
||||
answersHTML += `${status} ${entry.word}<br>`;
|
||||
});
|
||||
for (const [username, userResults] of Object.entries(results)) {
|
||||
// Handle array of results per user
|
||||
if (Array.isArray(userResults) && userResults.length > 0) {
|
||||
userResults.forEach((result, index) => {
|
||||
const li = document.createElement("li");
|
||||
let answersHTML = '';
|
||||
|
||||
// Check if result and answers exist and are valid
|
||||
if (result && result.answers && Array.isArray(result.answers)) {
|
||||
result.answers.forEach(entry => {
|
||||
const status = entry.correct ? '✅' : '❌';
|
||||
answersHTML += `${status} ${entry.word}<br>`;
|
||||
});
|
||||
} else {
|
||||
answersHTML = '<em>No answer data available</em>';
|
||||
}
|
||||
|
||||
li.innerHTML = `
|
||||
<strong>${username}</strong> — Score: ${result.score}, Completed: ${result.completed ? '✅' : '❌'}<br>
|
||||
<em>Answers:</em><br>${answersHTML}
|
||||
`;
|
||||
list.appendChild(li);
|
||||
const timestamp = result.timestamp ? new Date(result.timestamp).toLocaleString() : 'Unknown time';
|
||||
li.innerHTML = `
|
||||
<strong>${username}</strong> (Session ${index + 1}) — Score: ${result?.score || 'N/A'}, Completed: ${result?.completed ? '✅' : '❌'}<br>
|
||||
<em>Date:</em> ${timestamp}<br>
|
||||
<em>Answers:</em><br>${answersHTML}
|
||||
<hr style="margin: 1rem 0; opacity: 0.3;">
|
||||
`;
|
||||
list.appendChild(li);
|
||||
});
|
||||
} else {
|
||||
// Fallback for single result format
|
||||
const li = document.createElement("li");
|
||||
li.innerHTML = `<strong>${username}</strong> — No valid results found`;
|
||||
list.appendChild(li);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("🔴 Failed to load results:", err);
|
||||
@@ -187,40 +290,6 @@ async function loadStudentResults() {
|
||||
}
|
||||
}
|
||||
|
||||
// 🧭 Admin tab switcher
|
||||
function switchTab(targetId) {
|
||||
document.querySelectorAll('.adminTab').forEach(tab =>
|
||||
tab.classList.add('hidden')
|
||||
);
|
||||
document.querySelectorAll('#adminTabs button').forEach(btn =>
|
||||
btn.classList.remove('active')
|
||||
);
|
||||
|
||||
// Show the selected tab
|
||||
document.getElementById(targetId).classList.remove('hidden');
|
||||
|
||||
// Highlight the active button
|
||||
document.querySelectorAll('#adminTabs button').forEach(btn => {
|
||||
if (btn.getAttribute('onclick')?.includes(targetId)) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 🔁 Trigger tab-specific logic
|
||||
if (targetId === 'tabSessions') {
|
||||
loadSessionHistory();
|
||||
}
|
||||
if (targetId === 'tabReports') {
|
||||
loadStudentNamesForReport();
|
||||
}
|
||||
if (targetId === 'tabBadges') {
|
||||
loadBadgeViewer();
|
||||
}
|
||||
if (targetId === 'tabSessions') {
|
||||
populateSessionUserDropdown();
|
||||
document.getElementById("userSessionList").innerHTML = "";
|
||||
}
|
||||
}
|
||||
async function loadUserDropdowns() {
|
||||
try {
|
||||
const res = await fetch('/getUsers');
|
||||
@@ -243,6 +312,8 @@ async function loadUserDropdowns() {
|
||||
drop.appendChild(opt);
|
||||
});
|
||||
});
|
||||
|
||||
console.log('✅ User dropdowns loaded');
|
||||
} catch (err) {
|
||||
console.error("⚠️ Failed to load user list:", err);
|
||||
}
|
||||
@@ -352,44 +423,73 @@ async function loadStudentNamesForReport() {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadStudentReport(username) {
|
||||
window.loadStudentReport = async function(username) {
|
||||
if (!username) return;
|
||||
|
||||
const res1 = await fetch('/getResults');
|
||||
const results = await res1.json();
|
||||
const res2 = await fetch('/getBadges');
|
||||
const badges = await res2.json();
|
||||
try {
|
||||
const res1 = await fetch('/getResults');
|
||||
const results = await res1.json();
|
||||
const res2 = await fetch('/getBadges');
|
||||
const badges = await res2.json();
|
||||
|
||||
const report = results[username];
|
||||
const badgeList = badges[username] || [];
|
||||
const userResults = results[username];
|
||||
const badgeList = badges[username] || [];
|
||||
|
||||
document.getElementById("reportTitle").textContent = `Report for ${username}`;
|
||||
document.getElementById("reportScore").textContent = `${report.score}/${report.answers.length} correct`;
|
||||
|
||||
const wordList = document.getElementById("reportWords");
|
||||
wordList.innerHTML = "";
|
||||
report.answers.forEach(({ word, correct }) => {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = `${word} — ${correct ? '✅' : '❌'}`;
|
||||
wordList.appendChild(li);
|
||||
});
|
||||
// Handle case where userResults might be an array or undefined
|
||||
let latestReport = null;
|
||||
if (Array.isArray(userResults) && userResults.length > 0) {
|
||||
// Get the most recent result
|
||||
latestReport = userResults[userResults.length - 1];
|
||||
} else if (userResults && typeof userResults === 'object') {
|
||||
// Handle single result object
|
||||
latestReport = userResults;
|
||||
}
|
||||
|
||||
const badgeContainer = document.getElementById("reportBadges");
|
||||
badgeContainer.innerHTML = "";
|
||||
if (badgeList.length) {
|
||||
badgeList.forEach(b => {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = `🏅 ${b}`;
|
||||
badgeContainer.appendChild(li);
|
||||
});
|
||||
} else {
|
||||
badgeContainer.innerHTML = "<li><em>No badges earned yet.</em></li>";
|
||||
if (!latestReport) {
|
||||
document.getElementById("reportContent").classList.add("hidden");
|
||||
alert(`No results found for ${username}`);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("reportTitle").textContent = `Report for ${username}`;
|
||||
|
||||
// Safe access to score and answers
|
||||
const score = latestReport.score || 0;
|
||||
const answers = latestReport.answers || [];
|
||||
document.getElementById("reportScore").textContent = `${score}/${answers.length} correct`;
|
||||
|
||||
const wordList = document.getElementById("reportWords");
|
||||
wordList.innerHTML = "";
|
||||
if (answers.length > 0) {
|
||||
answers.forEach(({ word, correct }) => {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = `${word} — ${correct ? '✅' : '❌'}`;
|
||||
wordList.appendChild(li);
|
||||
});
|
||||
} else {
|
||||
wordList.innerHTML = "<li><em>No word data available.</em></li>";
|
||||
}
|
||||
|
||||
const badgeContainer = document.getElementById("reportBadges");
|
||||
badgeContainer.innerHTML = "";
|
||||
if (badgeList.length) {
|
||||
badgeList.forEach(b => {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = `🏅 ${b}`;
|
||||
badgeContainer.appendChild(li);
|
||||
});
|
||||
} else {
|
||||
badgeContainer.innerHTML = "<li><em>No badges earned yet.</em></li>";
|
||||
}
|
||||
|
||||
document.getElementById("reportContent").classList.remove("hidden");
|
||||
} catch (error) {
|
||||
console.error("Error loading student report:", error);
|
||||
alert("Error loading student report. Please try again.");
|
||||
}
|
||||
|
||||
document.getElementById("reportContent").classList.remove("hidden");
|
||||
}
|
||||
|
||||
function copyReportToClipboard() {
|
||||
window.copyReportToClipboard = function() {
|
||||
const name = document.getElementById("reportTitle").textContent;
|
||||
const score = document.getElementById("reportScore").textContent;
|
||||
const words = Array.from(document.querySelectorAll("#reportWords li")).map(li => li.textContent).join("\n");
|
||||
@@ -457,4 +557,47 @@ async function loadUserSessions(username) {
|
||||
sessionItem.innerHTML = html;
|
||||
list.appendChild(sessionItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// File upload handler for word lists
|
||||
function handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const content = e.target.result;
|
||||
const words = content.split(/\r?\n/).map(w => w.trim()).filter(Boolean);
|
||||
|
||||
document.getElementById('wordInput').value = words.join('\n');
|
||||
showToast(`📁 Loaded ${words.length} words from file`);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// Add event listeners for admin tabs as backup to onclick
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Setup tab click listeners
|
||||
setTimeout(() => {
|
||||
const tabButtons = document.querySelectorAll('#adminTabs button');
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const onclick = button.getAttribute('onclick');
|
||||
if (onclick) {
|
||||
const match = onclick.match(/switchTab\('(.+?)'\)/);
|
||||
if (match) {
|
||||
const tabId = match[1];
|
||||
console.log('Tab clicked:', tabId);
|
||||
if (typeof window.switchTab === 'function') {
|
||||
window.switchTab(tabId);
|
||||
} else {
|
||||
console.error('switchTab function not available');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
console.log('✅ Tab event listeners added to', tabButtons.length, 'buttons');
|
||||
}, 300);
|
||||
});
|
||||
+92
-19
@@ -1,8 +1,24 @@
|
||||
// 🔐 Password hashing using jsSHA
|
||||
function hashPassword(password) {
|
||||
const shaObj = new jsSHA("SHA-256", "TEXT");
|
||||
shaObj.update(password);
|
||||
return shaObj.getHash("HEX");
|
||||
try {
|
||||
if (typeof jsSHA === 'undefined') {
|
||||
console.error('jsSHA library not loaded');
|
||||
throw new Error('jsSHA library not available');
|
||||
}
|
||||
const shaObj = new jsSHA("SHA-256", "TEXT");
|
||||
shaObj.update(password);
|
||||
return shaObj.getHash("HEX");
|
||||
} catch (error) {
|
||||
console.error('Error hashing password:', error);
|
||||
// Fallback: simple hash (not secure, but for debugging)
|
||||
let hash = 0;
|
||||
for (let i = 0; i < password.length; i++) {
|
||||
const char = password.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
return Math.abs(hash).toString(16);
|
||||
}
|
||||
}
|
||||
|
||||
// 🌐 Load users from server
|
||||
@@ -12,28 +28,43 @@ let users = [];
|
||||
async function verifyLogin() {
|
||||
const uname = document.getElementById('username').value.trim();
|
||||
const pw = document.getElementById('password').value;
|
||||
const hash = hashPassword(pw);
|
||||
const msg = document.getElementById('loginMessage');
|
||||
|
||||
if (!uname || !pw) {
|
||||
msg.textContent = "Please enter both username and password.";
|
||||
msg.style.color = 'var(--error)';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Attempting login for:', uname);
|
||||
const hash = hashPassword(pw);
|
||||
console.log('Generated hash:', hash);
|
||||
|
||||
const res = await fetch('/verifyUser', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: uname, hash })
|
||||
});
|
||||
|
||||
const responseText = await res.text();
|
||||
console.log('Server response:', res.status, responseText);
|
||||
|
||||
if (!res.ok) {
|
||||
msg.textContent = "Invalid username or password.";
|
||||
msg.style.color = 'var(--error)';
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await res.json();
|
||||
const user = JSON.parse(responseText);
|
||||
console.log('Login successful for user:', user);
|
||||
localStorage.setItem('loggedInUser', JSON.stringify(user));
|
||||
msg.textContent = '';
|
||||
setupSession(user);
|
||||
} catch (err) {
|
||||
console.error("Login error:", err);
|
||||
msg.textContent = "Login failed. Please try again.";
|
||||
msg.style.color = 'var(--error)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,18 +72,60 @@ async function verifyLogin() {
|
||||
function logoutUser() {
|
||||
localStorage.removeItem('loggedInUser');
|
||||
|
||||
document.getElementById('adminPanel').classList.add('hidden');
|
||||
document.getElementById('studentPanel').classList.add('hidden');
|
||||
document.getElementById('nav').classList.add('hidden');
|
||||
document.getElementById('loginPanel').classList.remove('hidden');
|
||||
document.getElementById('username').value = '';
|
||||
document.getElementById('password').value = '';
|
||||
|
||||
const roleBanner = document.getElementById('roleBanner');
|
||||
roleBanner.classList.add('hidden');
|
||||
roleBanner.classList.remove('admin', 'student');
|
||||
roleBanner.textContent = '';
|
||||
|
||||
// Hide all panels
|
||||
document.getElementById('adminPanel')?.classList.add('hidden');
|
||||
document.getElementById('studentPanel')?.classList.add('hidden');
|
||||
document.getElementById('nav')?.classList.add('hidden');
|
||||
|
||||
// Show login panel
|
||||
const loginPanel = document.getElementById('loginPanel');
|
||||
loginPanel?.classList.remove('hidden');
|
||||
|
||||
// Clear login form
|
||||
const usernameField = document.getElementById('username');
|
||||
const passwordField = document.getElementById('password');
|
||||
const loginMessage = document.getElementById('loginMessage');
|
||||
if (loginMessage) loginMessage.textContent = '';
|
||||
}
|
||||
|
||||
if (usernameField) usernameField.value = '';
|
||||
if (passwordField) passwordField.value = '';
|
||||
if (loginMessage) {
|
||||
loginMessage.textContent = '';
|
||||
loginMessage.style.color = '';
|
||||
}
|
||||
|
||||
// Reset banner
|
||||
const roleBanner = document.getElementById('roleBanner');
|
||||
if (roleBanner) {
|
||||
roleBanner.classList.add('hidden');
|
||||
roleBanner.classList.remove('admin', 'student');
|
||||
roleBanner.textContent = '';
|
||||
}
|
||||
|
||||
// Focus on username field
|
||||
if (usernameField) {
|
||||
setTimeout(() => usernameField.focus(), 100);
|
||||
}
|
||||
|
||||
showToast('Logged out successfully');
|
||||
}
|
||||
|
||||
// Add Enter key support for login form
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const loginForm = document.querySelector('#loginPanel form');
|
||||
const usernameField = document.getElementById('username');
|
||||
const passwordField = document.getElementById('password');
|
||||
|
||||
if (loginForm && usernameField && passwordField) {
|
||||
// Focus username field on page load
|
||||
usernameField.focus();
|
||||
|
||||
// Add Enter key support
|
||||
[usernameField, passwordField].forEach(field => {
|
||||
field.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
verifyLogin();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
+42
-2
@@ -3,7 +3,46 @@ let words = [];
|
||||
let currentWord = '';
|
||||
let currentIndex = 0;
|
||||
let results = [];
|
||||
let typingWords = [];
|
||||
// 📝 Check user's answer and move to next
|
||||
window.submitAnswer = function() {
|
||||
const input = document.getElementById('userInput').value.trim();
|
||||
|
||||
// Record the attempt
|
||||
results.push({
|
||||
word: currentWord,
|
||||
attempt: input
|
||||
});
|
||||
|
||||
if (input.toLowerCase() === currentWord.toLowerCase()) {
|
||||
currentIndex++;
|
||||
document.getElementById('inputSection').classList.add('hidden');
|
||||
setTimeout(() => showWord(), 500); // Brief pause before next word
|
||||
} else {
|
||||
// Show feedback for incorrect answer
|
||||
const inputEl = document.getElementById('userInput');
|
||||
inputEl.style.borderColor = 'var(--error)';
|
||||
inputEl.style.backgroundColor = '#fef2f2';
|
||||
|
||||
setTimeout(() => {
|
||||
inputEl.style.borderColor = '';
|
||||
inputEl.style.backgroundColor = '';
|
||||
inputEl.focus();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Add Enter key support for game input
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const gameInput = document.getElementById('userInput');
|
||||
if (gameInput) {
|
||||
gameInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
submitAnswer();
|
||||
}
|
||||
});
|
||||
}
|
||||
});[];
|
||||
let currentSentence = '';
|
||||
|
||||
function showStudent() {
|
||||
console.log("🔊 Showing student panel");
|
||||
@@ -34,7 +73,7 @@ function showStudent() {
|
||||
}
|
||||
|
||||
// 🚀 Begin game session
|
||||
async function startGame() {
|
||||
window.startGame = async function() {
|
||||
console.log('🎮 Starting spelling game');
|
||||
|
||||
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||
@@ -62,6 +101,7 @@ async function startGame() {
|
||||
|
||||
// Start game
|
||||
words = [...data.words];
|
||||
typingWords = [...data.words]; // Initialize typing words too
|
||||
currentIndex = 0;
|
||||
results = []; // Reset results array
|
||||
showWord();
|
||||
|
||||
+46
-14
@@ -1,22 +1,54 @@
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// 🌐 Load users.json for login validation
|
||||
// await loadUsers();
|
||||
displayUserDropdown();
|
||||
populateWordUserDropdown();
|
||||
|
||||
// 🎨 Restore saved theme
|
||||
applySavedTheme();
|
||||
|
||||
// 🔁 Restore session if user was logged in
|
||||
const savedUser = localStorage.getItem("loggedInUser");
|
||||
if (savedUser) {
|
||||
setupSession(JSON.parse(savedUser));
|
||||
console.log('🚀 Application starting...');
|
||||
|
||||
// Load users for admin functionality
|
||||
try {
|
||||
await loadUsers();
|
||||
displayUserDropdown();
|
||||
populateWordUserDropdown();
|
||||
} catch (e) {
|
||||
console.warn('Could not load users (normal if not admin):', e);
|
||||
}
|
||||
|
||||
// 🧭 Tab switching for Admin Panel
|
||||
// Restore saved theme
|
||||
applySavedTheme();
|
||||
|
||||
// Restore session if user was logged in
|
||||
const savedUser = localStorage.getItem("loggedInUser");
|
||||
if (savedUser) {
|
||||
try {
|
||||
const user = JSON.parse(savedUser);
|
||||
console.log('👤 Restoring session for:', user.username);
|
||||
setupSession(user);
|
||||
} catch (e) {
|
||||
console.error('Error restoring session:', e);
|
||||
localStorage.removeItem("loggedInUser");
|
||||
}
|
||||
} else {
|
||||
console.log('👤 No saved session found');
|
||||
// Focus on login form
|
||||
const usernameField = document.getElementById('username');
|
||||
if (usernameField) {
|
||||
setTimeout(() => usernameField.focus(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Set default admin tab
|
||||
const defaultTab = document.querySelector('#adminTabs button.active');
|
||||
if (defaultTab) {
|
||||
const targetId = defaultTab.getAttribute("onclick")?.match(/'(.*?)'/)?.[1];
|
||||
if (targetId) switchTab(targetId);
|
||||
if (targetId) {
|
||||
console.log('🔧 Setting default admin tab:', targetId);
|
||||
// Use a small delay to ensure all scripts are loaded
|
||||
setTimeout(() => {
|
||||
if (typeof window.switchTab === 'function') {
|
||||
window.switchTab(targetId);
|
||||
} else {
|
||||
console.warn('switchTab function not yet available');
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Application initialized');
|
||||
});
|
||||
+64
-5
@@ -1,14 +1,73 @@
|
||||
// 🎨 Apply saved theme on load
|
||||
// Modern theme system
|
||||
function applySavedTheme() {
|
||||
const savedTheme = localStorage.getItem('theme') || 'playground';
|
||||
document.body.className = savedTheme;
|
||||
const savedTheme = localStorage.getItem('theme') || 'default';
|
||||
applyTheme(savedTheme);
|
||||
|
||||
const themeSelector = document.getElementById('themeSelect');
|
||||
if (themeSelector) themeSelector.value = savedTheme;
|
||||
}
|
||||
|
||||
// 🎨 Change theme dynamically
|
||||
function changeTheme(themeName) {
|
||||
document.body.className = themeName;
|
||||
applyTheme(themeName);
|
||||
localStorage.setItem('theme', themeName);
|
||||
showToast(`🎨 Theme changed to ${themeName}`);
|
||||
}
|
||||
|
||||
function applyTheme(themeName) {
|
||||
// Remove existing theme classes
|
||||
document.body.className = '';
|
||||
|
||||
// Apply theme-specific styles
|
||||
switch(themeName) {
|
||||
case 'playground':
|
||||
document.body.className = 'theme-playground';
|
||||
document.documentElement.style.setProperty('--primary', '#ff6b6b');
|
||||
document.documentElement.style.setProperty('--accent', '#4ecdc4');
|
||||
document.documentElement.style.setProperty('--bg-primary', '#fff5f5');
|
||||
document.documentElement.style.setProperty('--bg-secondary', '#ffebee');
|
||||
break;
|
||||
|
||||
case 'chalkboard':
|
||||
document.body.className = 'theme-chalkboard';
|
||||
document.documentElement.style.setProperty('--primary', '#4ade80');
|
||||
document.documentElement.style.setProperty('--accent', '#22d3ee');
|
||||
document.documentElement.style.setProperty('--bg-primary', '#1f2937');
|
||||
document.documentElement.style.setProperty('--bg-secondary', '#374151');
|
||||
document.documentElement.style.setProperty('--text-primary', '#f9fafb');
|
||||
document.documentElement.style.setProperty('--text-secondary', '#d1d5db');
|
||||
document.documentElement.style.setProperty('--border', '#4b5563');
|
||||
break;
|
||||
|
||||
case 'galaxy':
|
||||
document.body.className = 'theme-galaxy';
|
||||
document.documentElement.style.setProperty('--primary', '#8b5cf6');
|
||||
document.documentElement.style.setProperty('--accent', '#06b6d4');
|
||||
document.documentElement.style.setProperty('--bg-primary', '#0f0f23');
|
||||
document.documentElement.style.setProperty('--bg-secondary', '#1a1a3f');
|
||||
document.documentElement.style.setProperty('--text-primary', '#e0e7ff');
|
||||
document.documentElement.style.setProperty('--text-secondary', '#a5b4fc');
|
||||
document.documentElement.style.setProperty('--border', '#3730a3');
|
||||
break;
|
||||
|
||||
case 'nature':
|
||||
document.body.className = 'theme-nature';
|
||||
document.documentElement.style.setProperty('--primary', '#059669');
|
||||
document.documentElement.style.setProperty('--accent', '#84cc16');
|
||||
document.documentElement.style.setProperty('--bg-primary', '#f0fdf4');
|
||||
document.documentElement.style.setProperty('--bg-secondary', '#dcfce7');
|
||||
document.documentElement.style.setProperty('--text-primary', '#14532d');
|
||||
break;
|
||||
|
||||
default: // Modern default theme
|
||||
document.body.className = 'theme-default';
|
||||
// Reset to original CSS custom properties
|
||||
document.documentElement.style.setProperty('--primary', '#3b82f6');
|
||||
document.documentElement.style.setProperty('--accent', '#06b6d4');
|
||||
document.documentElement.style.setProperty('--bg-primary', '#ffffff');
|
||||
document.documentElement.style.setProperty('--bg-secondary', '#f8fafc');
|
||||
document.documentElement.style.setProperty('--text-primary', '#1e293b');
|
||||
document.documentElement.style.setProperty('--text-secondary', '#64748b');
|
||||
document.documentElement.style.setProperty('--border', '#e2e8f0');
|
||||
break;
|
||||
}
|
||||
}
|
||||
+4
-9
@@ -5,7 +5,8 @@ const typingState = {
|
||||
currentWord: ''
|
||||
};
|
||||
|
||||
async function startTypingPractice() {
|
||||
// Make functions globally accessible
|
||||
window.startTypingPractice = async function() {
|
||||
console.log('Starting typing practice');
|
||||
|
||||
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||
@@ -34,7 +35,7 @@ async function startTypingPractice() {
|
||||
}
|
||||
}
|
||||
|
||||
function showNextWord() {
|
||||
window.showNextWord = function() {
|
||||
const promptEl = document.getElementById('typingPrompt');
|
||||
const inputEl = document.getElementById('typingInput');
|
||||
if (!promptEl || !inputEl) return;
|
||||
@@ -58,14 +59,8 @@ function showNextWord() {
|
||||
inputEl.focus();
|
||||
}
|
||||
|
||||
|
||||
// Reset input
|
||||
inputEl.value = '';
|
||||
inputEl.disabled = false;
|
||||
inputEl.focus();
|
||||
|
||||
|
||||
function submitTyping() {
|
||||
window.submitTyping = function() {
|
||||
const input = document.getElementById('typingInput')?.value.trim() || '';
|
||||
const feedback = document.getElementById('typingFeedback');
|
||||
if (!feedback) return;
|
||||
|
||||
+51
-9
@@ -46,16 +46,58 @@ function showToast(message, duration = 3000) {
|
||||
toast.classList.remove("show");
|
||||
}, duration);
|
||||
}
|
||||
function showAdmin() {
|
||||
console.log("showAdmin fired");
|
||||
|
||||
const adminPanel = document.getElementById("adminPanel");
|
||||
const studentPanel = document.getElementById("studentPanel");
|
||||
function showStudent() {
|
||||
console.log("🎓 Showing student panel");
|
||||
|
||||
loadUserDropdowns();
|
||||
// Hide other panels
|
||||
document.getElementById('adminPanel')?.classList.add('hidden');
|
||||
document.getElementById('loginPanel')?.classList.add('hidden');
|
||||
|
||||
// Show student panel
|
||||
const studentPanel = document.getElementById('studentPanel');
|
||||
if (studentPanel) {
|
||||
studentPanel.classList.remove('hidden');
|
||||
|
||||
// Ensure buttons are visible and functional
|
||||
const gameBtn = document.getElementById('startGameBtn');
|
||||
const typingBtn = document.getElementById('startTypingBtn');
|
||||
|
||||
if (gameBtn && typingBtn) {
|
||||
gameBtn.classList.remove('hidden');
|
||||
typingBtn.classList.remove('hidden');
|
||||
console.log("✅ Student buttons are now visible");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adminPanel?.classList.remove("hidden");
|
||||
studentPanel?.classList.add("hidden");
|
||||
function showAdmin() {
|
||||
console.log("👨💼 Showing admin panel");
|
||||
|
||||
switchTab("tabUsers"); // optional default tab
|
||||
// Hide other panels
|
||||
document.getElementById('studentPanel')?.classList.add('hidden');
|
||||
document.getElementById('loginPanel')?.classList.add('hidden');
|
||||
|
||||
const adminPanel = document.getElementById("adminPanel");
|
||||
if (adminPanel) {
|
||||
adminPanel.classList.remove("hidden");
|
||||
|
||||
// Load user data for dropdowns
|
||||
loadUserDropdowns().then(() => {
|
||||
console.log("✅ User dropdowns loaded");
|
||||
}).catch(e => {
|
||||
console.warn("Could not load user dropdowns:", e);
|
||||
});
|
||||
|
||||
// Set default tab - use a slight delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
if (typeof window.switchTab === 'function') {
|
||||
window.switchTab("tabUsers");
|
||||
console.log("✅ Default tab set");
|
||||
} else {
|
||||
console.error("switchTab function not available");
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
console.error("Admin panel not found");
|
||||
}
|
||||
}
|
||||
+340
-262
@@ -1,319 +1,397 @@
|
||||
/* 📋 Global Defaults */
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
/* Modern Spelling App Styles */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
/* Reset and base styles */
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
margin: 0.25rem 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Modern color palette */
|
||||
--primary: #3b82f6;
|
||||
--primary-dark: #2563eb;
|
||||
--secondary: #f1f5f9;
|
||||
--accent: #06b6d4;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8fafc;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--radius: 0.75rem;
|
||||
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: linear-gradient(135deg, var(--bg-secondary) 0%, #ffffff 100%);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
/* Container and layout */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
input, textarea, select {
|
||||
font-size: 1rem;
|
||||
padding: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0.5rem;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 5px;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
/* 🎨 Theme Styles */
|
||||
body.playground {
|
||||
background: #fef9e7;
|
||||
color: #333;
|
||||
font-family: 'Comic Sans MS', cursive, sans-serif;
|
||||
}
|
||||
body.chalkboard {
|
||||
background: #222;
|
||||
color: #f0f0f0;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
body.galaxy {
|
||||
background: radial-gradient(circle at center, #0d0d2b, #1a1a3f);
|
||||
color: #b3e5fc;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
}
|
||||
body.nature {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
font-family: 'Georgia', serif;
|
||||
input:focus, textarea:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
|
||||
}
|
||||
|
||||
/* 🔘 Buttons */
|
||||
textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
background-color: #4caf50;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
margin: 0.25rem;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 1rem;
|
||||
margin: 0.4rem;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
display: inline-block;
|
||||
transition: var(--transition);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: scale(1.05);
|
||||
background: var(--primary-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
/* 🌈 Theme-specific Button Colors */
|
||||
body.chalkboard button {
|
||||
background-color: #555;
|
||||
}
|
||||
body.galaxy button {
|
||||
background-color: #6200ea;
|
||||
}
|
||||
body.nature button {
|
||||
background-color: #81c784;
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 🔳 Panels and Banners */
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Button variants */
|
||||
.btn-secondary {
|
||||
background: var(--secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--success);
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: var(--warning);
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
.btn-error {
|
||||
background: var(--error);
|
||||
}
|
||||
|
||||
.btn-error:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
/* Cards and panels */
|
||||
.card {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem;
|
||||
margin: 1rem 0;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Layout utilities */
|
||||
.hidden {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
#wordBox {
|
||||
font-size: 2em;
|
||||
margin: 20px;
|
||||
min-height: 60px;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.word-box {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2rem;
|
||||
color: #2196f3;
|
||||
text-shadow: 1px 1px #ccc;
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.banner {
|
||||
padding: 0.5em;
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
.banner.admin {
|
||||
background-color: #b22222;
|
||||
}
|
||||
.banner.student {
|
||||
background-color: #1e90ff;
|
||||
}
|
||||
|
||||
/* 🔔 Toast Messages */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
padding: 0.75em 1.25em;
|
||||
border-radius: 5px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.4s ease;
|
||||
z-index: 999;
|
||||
}
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
/* Specific component styles */
|
||||
#loginPanel {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
background: var(--bg-primary);
|
||||
padding: 2rem;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
/* 🧭 Admin Tabs */
|
||||
#adminTabs {
|
||||
margin: 1em 0;
|
||||
#loginPanel form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
#adminTabs button {
|
||||
margin-right: 8px;
|
||||
padding: 10px 16px;
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #ccc;
|
||||
border-bottom: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
|
||||
/* Banner */
|
||||
.banner {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: white;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.banner.admin {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
}
|
||||
|
||||
.banner.student {
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
#nav {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* Admin tabs */
|
||||
#adminTabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
#adminTabs button {
|
||||
flex: 1;
|
||||
min-width: fit-content;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border: none;
|
||||
border-radius: calc(var(--radius) - 0.25rem);
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 500;
|
||||
transition: var(--transition);
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
#adminTabs button:hover {
|
||||
background-color: #e0f2ff;
|
||||
color: #000;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#adminTabs button.active {
|
||||
background-color: #ffffff;
|
||||
color: #000;
|
||||
border-bottom: 2px solid #fff;
|
||||
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
|
||||
background: var(--primary) !important;
|
||||
color: white !important;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.adminTab {
|
||||
display: none;
|
||||
}
|
||||
.adminTab:not(.hidden) {
|
||||
display: block;
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.typing-sentence {
|
||||
font-size: 1.5em;
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ddd;
|
||||
min-height: 50px;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
.adminTab.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Remove these style classes if they exist */
|
||||
.typing-section
|
||||
.typing-container
|
||||
.typing-prompt
|
||||
.input-container
|
||||
#typingInput
|
||||
#typingFeedback
|
||||
|
||||
/* Fix for start buttons visibility */
|
||||
#startGameBtn, #startTypingBtn {
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
padding: 12px 24px;
|
||||
font-size: 1.1em;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#startGameBtn:hover, #startTypingBtn:hover {
|
||||
transform: scale(1.05);
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
/* Override any hidden class for these buttons */
|
||||
#startGameBtn.hidden, #startTypingBtn.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Button container for better spacing */
|
||||
.button-container {
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
/* Game elements */
|
||||
#wordBox, .word-box, #typingPrompt {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
padding: 3rem 2rem;
|
||||
margin: 2rem 0;
|
||||
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--secondary) 100%);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.student-controls {
|
||||
display: block !important;
|
||||
margin: 20px auto;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
visibility: visible !important;
|
||||
text-align: center;
|
||||
margin: 2rem 0;
|
||||
padding: 2rem;
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.student-btn {
|
||||
display: inline-block !important;
|
||||
visibility: visible !important;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
margin: 10px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
opacity: 1 !important;
|
||||
/* Lists */
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* Override any hiding classes for these specific buttons */
|
||||
#startGameBtn:not(.hidden),
|
||||
#startTypingBtn:not(.hidden) {
|
||||
display: inline-block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
li {
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0.5rem 0;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: calc(var(--radius) - 0.25rem);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Remove this if it exists as it might conflict */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
/* Toast notifications */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
background: var(--text-primary);
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: var(--radius);
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
transition: var(--transition);
|
||||
z-index: 1000;
|
||||
box-shadow: var(--shadow-lg);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Add this to ensure student panel shows properly */
|
||||
#studentPanel:not(.hidden) {
|
||||
display: block !important;
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 📝 Typing Practice Styles */
|
||||
.typing-section {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#typingPrompt {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
min-height: 60px;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
#typingInput {
|
||||
width: 300px;
|
||||
padding: 10px;
|
||||
font-size: 18px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.typing-word {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
min-height: 50px;
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.typing-input-container {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* Fix student panel buttons */
|
||||
#startGameBtn, #startTypingBtn {
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
padding: 12px 24px;
|
||||
font-size: 1.1em;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Ensure student panel visibility */
|
||||
#studentPanel:not(.hidden) {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Remove any conflicting hidden styles */
|
||||
#startGameBtn:not(.hidden),
|
||||
#startTypingBtn:not(.hidden) {
|
||||
display: inline-block !important;
|
||||
visibility: visible !important;
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
#adminTabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#adminTabs button {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.card, .adminTab, #loginPanel {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#wordBox, .word-box, #typingPrompt {
|
||||
font-size: 2rem;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user