bunch of fixes
This commit is contained in:
1 parent
462c7908a1
commit
266a064330
14 files changed
+1169
-826
No files matched your search
@@ -0,0 +1,17 @@
|
|||||||
|
// Debug script to check password hashes
|
||||||
|
const { createHash } = require('crypto');
|
||||||
|
|
||||||
|
function hashPassword(password) {
|
||||||
|
return createHash('sha256').update(password).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Testing password hashes:');
|
||||||
|
console.log('password -> ', hashPassword('password'));
|
||||||
|
console.log('123456 -> ', hashPassword('123456'));
|
||||||
|
console.log('test -> ', hashPassword('test'));
|
||||||
|
|
||||||
|
// Known hashes from users.json:
|
||||||
|
console.log('\nExpected hashes from users.json:');
|
||||||
|
console.log('admin1 (password): 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8');
|
||||||
|
console.log('student1 (123456): 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92');
|
||||||
|
console.log('nate: 5a2a558c78d3717db731600c4f354fa1d9c84b556f108091a891f444f1bdec40');
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const resultsPath = path.join(__dirname, 'data', 'results.json');
|
|
||||||
const existingData = JSON.parse(fs.readFileSync(resultsPath));
|
|
||||||
|
|
||||||
for (const username in existingData) {
|
|
||||||
const singleResult = existingData[username];
|
|
||||||
existingData[username] = [{
|
|
||||||
...singleResult,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(resultsPath, JSON.stringify(existingData, null, 2));
|
|
||||||
console.log("✅ Migration complete. Results are now session arrays.");
|
|
||||||
+15
-7
@@ -1,19 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "spelling",
|
"name": "modern-spelling-practice",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
|
"description": "A modern, multi-user spelling practice application with admin controls, customizable themes, and analytics",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"start": "node server.js",
|
||||||
"start": "node server.js"
|
"dev": "node server.js",
|
||||||
|
"test": "echo \"No tests specified\" && exit 0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.necloud.us/nmemmert/Spelling.git"
|
"url": "https://git.necloud.us/nmemmert/Spelling.git"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [
|
||||||
"author": "",
|
"spelling",
|
||||||
|
"education",
|
||||||
|
"practice",
|
||||||
|
"nodejs",
|
||||||
|
"express",
|
||||||
|
"learning"
|
||||||
|
],
|
||||||
|
"author": "NateEmmert",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.1.0"
|
"express": "^5.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+297
-193
@@ -1,200 +1,304 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
<title>Spelling App with Roles</title>
|
<title>✨ Modern Spelling Practice</title>
|
||||||
<link href="styles.css" rel="stylesheet"/>
|
<link href="styles.css" rel="stylesheet"/>
|
||||||
<!-- 🔧 Script Load Order -->
|
<!-- Scripts -->
|
||||||
<script defer="" src="https://cdnjs.cloudflare.com/ajax/libs/jsSHA/2.4.2/sha.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsSHA/2.4.2/sha.js"></script>
|
||||||
<script defer="" src="js/auth.js"></script>
|
<script src="js/admin.js"></script>
|
||||||
<script defer="" src="js/ui.js"></script>
|
<script src="js/auth.js"></script>
|
||||||
<script defer="" src="js/themes.js"></script>
|
<script src="js/ui.js"></script>
|
||||||
<script src="js/game.js"></script>
|
<script src="js/themes.js"></script>
|
||||||
<script defer="" src="js/admin.js"></script>
|
<script src="js/game.js"></script>
|
||||||
<script defer="" src="js/main.js"></script>
|
<script src="js/typing.js"></script>
|
||||||
<script defer="" src="js/typing.js"></script>
|
<script src="js/main.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Spelling Practice</h1>
|
<div class="container">
|
||||||
<!-- 🏳️ Role Banner -->
|
<h1>✨ Spelling Practice</h1>
|
||||||
<div class="banner hidden" id="roleBanner"></div>
|
|
||||||
<!-- 👤 Login Panel -->
|
<!-- Role Banner -->
|
||||||
<div id="loginPanel">
|
<div class="banner hidden" id="roleBanner"></div>
|
||||||
<h2>User Login</h2>
|
|
||||||
<form onsubmit="event.preventDefault(); verifyLogin();">
|
<!-- Login Panel -->
|
||||||
<input id="username" placeholder="Username" required="" type="text"/>
|
<div id="loginPanel" class="card">
|
||||||
<input id="password" placeholder="Password" required="" type="password"/>
|
<div class="card-header">
|
||||||
<button type="submit">Login</button>
|
<h2 class="text-center">Welcome Back</h2>
|
||||||
</form>
|
<p class="text-center" style="color: var(--text-secondary);">Sign in to continue your spelling practice</p>
|
||||||
<p id="loginMessage"></p>
|
</div>
|
||||||
</div>
|
<form onsubmit="event.preventDefault(); verifyLogin();">
|
||||||
<!-- 🔀 Role-Based Navigation -->
|
<input id="username" placeholder="Username" required type="text"/>
|
||||||
<div class="hidden" id="nav">
|
<input id="password" placeholder="Password" required type="password"/>
|
||||||
<p><strong id="userInfo"></strong></p>
|
<button type="submit">Sign In</button>
|
||||||
<button class="hidden" id="adminBtn" onclick="showAdmin()">Admin Panel</button>
|
</form>
|
||||||
<button class="hidden" id="studentBtn" onclick="showStudent()">Student Panel</button>
|
<p id="loginMessage" style="margin-top: 1rem; text-align: center;"></p>
|
||||||
<button id="logoutBtn" onclick="logoutUser()">Log Out</button>
|
</div>
|
||||||
</div>
|
<!-- Navigation -->
|
||||||
<!-- 👨🏫 Admin Panel -->
|
<div id="nav" class="hidden">
|
||||||
<div class="hidden" id="adminPanel">
|
<p><strong id="userInfo"></strong></p>
|
||||||
<h2>👨🏫 Admin Panel</h2>
|
<div class="flex gap-4 justify-center" style="margin-top: 1rem;">
|
||||||
<!-- 🔘 Tabs -->
|
<button class="hidden" id="adminBtn" onclick="showAdmin()">👨💼 Admin Panel</button>
|
||||||
<div id="adminTabs">
|
<button class="hidden" id="studentBtn" onclick="showStudent()">🎓 Student Panel</button>
|
||||||
<button class="active" onclick="switchTab('tabUsers')">👤 Manage Users</button>
|
<button id="logoutBtn" onclick="logoutUser()" class="btn-secondary">Sign Out</button>
|
||||||
<button onclick="switchTab('tabWords')">📝 Word Lists</button>
|
</div>
|
||||||
<button onclick="switchTab('tabAnalytics')">📊 Analytics</button>
|
</div>
|
||||||
<button onclick="switchTab('tabResults')">📊 Results</button>
|
<!-- Admin Panel -->
|
||||||
<button onclick="switchTab('tabReports')">📘 Reports</button>
|
<div class="hidden" id="adminPanel">
|
||||||
<button onclick="switchTab('tabSessions')">📆 Sessions</button>
|
<div class="card-header">
|
||||||
<button onclick="switchTab('tabBadges')">🎖 Badges</button>
|
<h2>👨💼 Admin Dashboard</h2>
|
||||||
<button onclick="switchTab('tabTheme')">🎨 Theme</button>
|
<p style="color: var(--text-secondary);">Manage users, word lists, and view analytics</p>
|
||||||
<button onclick="switchTab('tabTyping')">⌨️ Typing Practice</button>
|
</div>
|
||||||
</div>
|
|
||||||
<!-- 👥 Users Tab -->
|
<!-- Admin Tabs -->
|
||||||
<div class="adminTab" id="tabUsers">
|
<div id="adminTabs">
|
||||||
<h3>Add New User</h3>
|
<button class="active" onclick="switchTab('tabUsers')">👤 Users</button>
|
||||||
<form onsubmit="event.preventDefault(); addNewUser();">
|
<button onclick="switchTab('tabWords')">📝 Word Lists</button>
|
||||||
<input id="newUsername" placeholder="Username" required="" type="text"/>
|
<button onclick="switchTab('tabAnalytics')">📊 Analytics</button>
|
||||||
<input id="newPassword" placeholder="Password" required="" type="password"/>
|
<button onclick="switchTab('tabResults')">� Results</button>
|
||||||
<select id="newRole">
|
<button onclick="switchTab('tabReports')">📘 Reports</button>
|
||||||
<option value="student">Student</option>
|
<button onclick="switchTab('tabSessions')">� Sessions</button>
|
||||||
<option value="admin">Admin</option>
|
<button onclick="switchTab('tabBadges')">� Badges</button>
|
||||||
</select>
|
<button onclick="switchTab('tabTheme')">🎨 Themes</button>
|
||||||
<button type="submit">Add User</button>
|
</div>
|
||||||
</form>
|
<!-- Users Tab -->
|
||||||
<h3>Delete Users</h3>
|
<div id="tabUsers" class="adminTab">
|
||||||
<select id="userDropdown" onchange="selectUserAction(this.value)">
|
<h3>👤 User Management</h3>
|
||||||
<option value="">-- Select a user --</option>
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 1.5rem;">
|
||||||
</select>
|
<div class="card">
|
||||||
</div>
|
<h4>Add New User</h4>
|
||||||
<!-- 📝 Word List Tab -->
|
<form onsubmit="event.preventDefault(); addNewUser();" style="display: flex; flex-direction: column; gap: 1rem;">
|
||||||
<div class="adminTab hidden" id="tabWords">
|
<input id="newUsername" placeholder="Username" required type="text"/>
|
||||||
<h3>Edit Word List for:</h3>
|
<input id="newPassword" placeholder="Password" required type="password"/>
|
||||||
<select class="word-user-dropdown" id="wordUserSelect" onchange="loadWordsForSelectedUser()">
|
<select id="newRole">
|
||||||
<option value="">-- Select a user --</option>
|
<option value="student">🎓 Student</option>
|
||||||
</select>
|
<option value="admin">👨💼 Admin</option>
|
||||||
<h2>Enter Spelling Words</h2>
|
</select>
|
||||||
<textarea id="wordInput" placeholder="Enter one word per line..."></textarea><br/>
|
<button type="submit" class="btn-success">➕ Add User</button>
|
||||||
<button onclick="saveWords()">Save Word List</button>
|
</form>
|
||||||
<button onclick="clearWords()">Clear Saved Words</button>
|
<p id="userAddMessage" style="margin-top: 1rem;"></p>
|
||||||
<h3>Upload a Word File</h3>
|
</div>
|
||||||
<input accept=".txt,.csv" onchange="handleFileUpload(event)" type="file"/>
|
<div class="card">
|
||||||
<p><strong>Current Word List:</strong></p>
|
<h4>Remove User</h4>
|
||||||
<ul id="wordListDisplay"></ul>
|
<select id="userDropdown" onchange="selectUserAction(this.value)" style="width: 100%;">
|
||||||
<h3>📋 View Word List by User:</h3>
|
<option value="">-- Select a user to remove --</option>
|
||||||
<select class="word-user-dropdown" id="viewUserSelect" onchange="displayWordListForSelectedUser()">
|
</select>
|
||||||
<option value="">-- Select a user --</option>
|
</div>
|
||||||
</select>
|
</div>
|
||||||
<ul id="userWordListDisplay"></ul>
|
</div>
|
||||||
</div>
|
<!-- Word Lists Tab -->
|
||||||
<!-- 📊 Results Tab -->
|
<div class="adminTab hidden" id="tabWords">
|
||||||
<div class="adminTab hidden" id="tabResults">
|
<h3>📝 Word List Management</h3>
|
||||||
<h3>🧾 Student Results Overview</h3>
|
<div style="display: grid; gap: 2rem;">
|
||||||
<button onclick="loadStudentResults()">Refresh Results</button>
|
<div class="card">
|
||||||
<ul id="resultsList"></ul>
|
<h4>Select Student</h4>
|
||||||
</div>
|
<select class="word-user-dropdown" id="wordUserSelect" onchange="loadWordsForSelectedUser()" style="width: 100%;">
|
||||||
<!-- 📊 Analytics Tab -->
|
<option value="">-- Select a student --</option>
|
||||||
<div class="adminTab hidden" id="tabAnalytics">
|
</select>
|
||||||
<h3>Smart Analytics Overview</h3>
|
</div>
|
||||||
<ul id="analyticsSummary">
|
|
||||||
<li><strong>Average Score:</strong> –</li>
|
<div class="card">
|
||||||
<li><strong>Total Completed Sessions:</strong> –</li>
|
<h4>Manage Word List</h4>
|
||||||
<li><strong>Most Missed Word:</strong> –</li>
|
<textarea id="wordInput" placeholder="Enter one word per line..."></textarea>
|
||||||
</ul>
|
<div style="display: flex; gap: 1rem; margin-top: 1rem;">
|
||||||
<h4>Per-Student Breakdown</h4>
|
<button onclick="saveWords()" class="btn-success">💾 Save Words</button>
|
||||||
<ul id="studentBreakdown"></ul>
|
<button onclick="clearWords()" class="btn-error">🗑️ Clear All</button>
|
||||||
<button onclick="refreshAnalytics()">🔄 Refresh</button>
|
</div>
|
||||||
</div>
|
|
||||||
<!-- 🎖 Badges -->
|
<div style="margin-top: 1.5rem;">
|
||||||
<div class="adminTab hidden" id="tabBadges">
|
<h5>Upload Word File</h5>
|
||||||
<h3>🎖 Badge Viewer</h3>
|
<input accept=".txt,.csv" onchange="handleFileUpload(event)" type="file" style="margin: 0.5rem 0;"/>
|
||||||
<ul id="badgeUserList"></ul>
|
</div>
|
||||||
<div class="hidden" id="badgeDetails">
|
</div>
|
||||||
<h4 id="badgeUserTitle"></h4>
|
|
||||||
<ul id="badgeUserBadges"></ul>
|
<div class="card">
|
||||||
</div>
|
<h4>Current Word List</h4>
|
||||||
</div>
|
<ul id="wordListDisplay" style="max-height: 200px; overflow-y: auto;"></ul>
|
||||||
<!-- 🎨 Theme Tab -->
|
</div>
|
||||||
<div class="adminTab hidden" id="tabTheme">
|
|
||||||
<h3>🎨 Select Theme</h3>
|
<div class="card">
|
||||||
<select id="themeSelect" onchange="changeTheme(this.value)">
|
<h4>View by Student</h4>
|
||||||
<option value="playground">Playground</option>
|
<select class="word-user-dropdown" id="viewUserSelect" onchange="displayWordListForSelectedUser()" style="width: 100%;">
|
||||||
<option value="chalkboard">Chalkboard</option>
|
<option value="">-- Select a student --</option>
|
||||||
<option value="galaxy">Galaxy</option>
|
</select>
|
||||||
<option value="nature">Nature</option>
|
<ul id="userWordListDisplay" style="max-height: 200px; overflow-y: auto; margin-top: 1rem;"></ul>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- ⌨️ Typing Practice Tab -->
|
<!-- Results Tab -->
|
||||||
<div class="adminTab hidden" id="tabTyping">
|
<div class="adminTab hidden" id="tabResults">
|
||||||
<h3>⌨️ Typing Challenge Preview</h3>
|
<div class="card">
|
||||||
<label for="typingUserSelect">Select Student:</label>
|
<div class="card-header">
|
||||||
<select class="word-user-dropdown" id="typingUserSelect" onchange="loadTypingWords(this.value)">
|
<h3>📋 Student Results Overview</h3>
|
||||||
<option value="">-- Select a user --</option>
|
<button onclick="loadStudentResults()" class="btn-primary">🔄 Refresh Results</button>
|
||||||
</select>
|
</div>
|
||||||
<button onclick="startTypingTest()">Start Typing Test</button>
|
<ul id="resultsList" style="max-height: 400px; overflow-y: auto;"></ul>
|
||||||
<div class="hidden" id="typingTestArea">
|
</div>
|
||||||
<p><strong>Practice Sentence:</strong></p>
|
</div>
|
||||||
<p class="typing-sentence" id="typingPrompt"></p>
|
|
||||||
<input autocomplete="off" id="typingInput" placeholder="Type the spelling word here..." type="text"/>
|
<!-- Analytics Tab -->
|
||||||
<button onclick="submitTyping()">Submit</button>
|
<div class="adminTab hidden" id="tabAnalytics">
|
||||||
<p id="typingFeedback"></p>
|
<div class="card">
|
||||||
</div>
|
<div class="card-header">
|
||||||
</div>
|
<h3>📊 Performance Analytics</h3>
|
||||||
<!-- 🧾 Student Report Viewer -->
|
<button onclick="refreshAnalytics()" class="btn-primary">🔄 Refresh Analytics</button>
|
||||||
<div class="adminTab hidden" id="tabReports">
|
</div>
|
||||||
<h3>📘 Student Progress Report</h3>
|
|
||||||
<label for="reportUser">Select Student:</label>
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||||
<select id="reportUser" onchange="loadStudentReport(this.value)">
|
<div>
|
||||||
<option value="">-- Select --</option>
|
<h4>Summary Statistics</h4>
|
||||||
</select>
|
<ul id="analyticsSummary" style="background: var(--bg-secondary); border-radius: var(--radius); padding: 1rem;">
|
||||||
<div class="hidden" id="reportContent">
|
<li><strong>Average Score:</strong> –</li>
|
||||||
<h4 id="reportTitle"></h4>
|
<li><strong>Total Sessions:</strong> –</li>
|
||||||
<p><strong>Last Score:</strong> <span id="reportScore"></span></p>
|
<li><strong>Most Missed Word:</strong> –</li>
|
||||||
<p><strong>Words Attempted:</strong></p>
|
</ul>
|
||||||
<ul id="reportWords"></ul>
|
</div>
|
||||||
<p><strong>Badges Earned:</strong></p>
|
|
||||||
<ul id="reportBadges"></ul>
|
<div>
|
||||||
<button onclick="window.print()">🖨️ Print Report</button>
|
<h4>Student Breakdown</h4>
|
||||||
<button onclick="copyReportToClipboard()">📋 Copy to Clipboard</button>
|
<ul id="studentBreakdown" style="max-height: 300px; overflow-y: auto;"></ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="adminTab hidden" id="tabSessions">
|
</div>
|
||||||
<h3>📘 Archived Sessions</h3>
|
</div>
|
||||||
<label for="sessionUserSelect">Select Student:</label>
|
|
||||||
<select id="sessionUserSelect" onchange="loadUserSessions(this.value)">
|
<!-- Badges Tab -->
|
||||||
<option value="">-- Select --</option>
|
<div class="adminTab hidden" id="tabBadges">
|
||||||
</select>
|
<div class="card">
|
||||||
<ul id="userSessionList"></ul>
|
<h3>� Student Badges</h3>
|
||||||
</div>
|
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 2rem;">
|
||||||
<!-- 👩🎓 Student Panel -->
|
<div>
|
||||||
<div class="hidden" id="studentPanel">
|
<h4>Select Student</h4>
|
||||||
<h2>Student Dashboard</h2>
|
<ul id="badgeUserList"></ul>
|
||||||
<div class="student-controls">
|
</div>
|
||||||
<button id="startGameBtn" onclick="startGame()">Start Spelling Game</button>
|
<div class="hidden" id="badgeDetails">
|
||||||
<button id="startTypingBtn" onclick="startTypingPractice()">Start Typing Practice</button>
|
<h4 id="badgeUserTitle"></h4>
|
||||||
</div>
|
<ul id="badgeUserBadges"></ul>
|
||||||
<!-- Game Section -->
|
</div>
|
||||||
<div class="hidden" id="gameSection">
|
</div>
|
||||||
<h3>🎮 Spelling Game</h3>
|
</div>
|
||||||
<div class="word-box" id="wordBox"></div>
|
</div>
|
||||||
<div class="hidden" id="inputSection">
|
|
||||||
<input id="userInput" placeholder="Type the word you saw..." type="text"/>
|
<!-- Theme Tab -->
|
||||||
<button onclick="submitAnswer()">Submit</button>
|
<div class="adminTab hidden" id="tabTheme">
|
||||||
</div>
|
<div class="card">
|
||||||
</div>
|
<h3>🎨 Application Themes</h3>
|
||||||
<!-- Typing Practice Section -->
|
<p style="color: var(--text-secondary); margin-bottom: 1.5rem;">Choose a visual theme for the application</p>
|
||||||
<div class="hidden" id="typingSection">
|
<select id="themeSelect" onchange="changeTheme(this.value)" style="width: 100%; max-width: 300px;">
|
||||||
<h3>Typing Practice</h3>
|
<option value="default">✨ Modern (Default)</option>
|
||||||
<p id="typingPrompt">Type the word...</p>
|
<option value="playground">🎪 Playground</option>
|
||||||
<input id="typingInput" type="text"/>
|
<option value="chalkboard">🎓 Chalkboard</option>
|
||||||
<button onclick="submitTyping()">Submit</button>
|
<option value="galaxy">🌌 Galaxy</option>
|
||||||
<p id="typingFeedback"></p>
|
<option value="nature">🌿 Nature</option>
|
||||||
</div>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
</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', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||||
if (user) {
|
if (user) {
|
||||||
@@ -206,5 +310,5 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<div id="spellingSection"><h3>Spelling Game</h3><p id="spellingPrompt">Word will appear here...</p></div></body>
|
</body>
|
||||||
</html>
|
</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>
|
|
||||||
+191
-48
@@ -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
|
// ➕ Add a new user
|
||||||
async function addNewUser() {
|
async function addNewUser() {
|
||||||
const username = document.getElementById("newUsername").value.trim();
|
const username = document.getElementById("newUsername").value.trim();
|
||||||
@@ -28,6 +109,7 @@ async function addNewUser() {
|
|||||||
|
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
displayUserDropdown();
|
displayUserDropdown();
|
||||||
|
populateWordUserDropdown();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
msg.textContent = "❌ Failed to add user.";
|
msg.textContent = "❌ Failed to add user.";
|
||||||
console.error("Add user error:", e);
|
console.error("Add user error:", e);
|
||||||
@@ -38,7 +120,7 @@ async function addNewUser() {
|
|||||||
function displayUserDropdown() {
|
function displayUserDropdown() {
|
||||||
const dropdown = document.getElementById("userDropdown");
|
const dropdown = document.getElementById("userDropdown");
|
||||||
dropdown.innerHTML = `<option value="">-- Select a user --</option>`;
|
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>`;
|
dropdown.innerHTML += `<option value="${user.username}">${user.username} (${user.role})</option>`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -104,8 +186,9 @@ async function loadWordsForSelectedUser() {
|
|||||||
const selectedUser = document.getElementById("wordUserSelect").value;
|
const selectedUser = document.getElementById("wordUserSelect").value;
|
||||||
if (!selectedUser) return;
|
if (!selectedUser) return;
|
||||||
|
|
||||||
const res = await fetch(`/getWordList?user=${encodeURIComponent(selectedUser)}`);
|
const res = await fetch(`/getWordList?username=${encodeURIComponent(selectedUser)}`);
|
||||||
allWords = await res.json();
|
const data = await res.json();
|
||||||
|
allWords = data.words || [];
|
||||||
displayWordList();
|
displayWordList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +201,7 @@ function populateWordUserDropdown() {
|
|||||||
const selects = document.querySelectorAll(".word-user-dropdown");
|
const selects = document.querySelectorAll(".word-user-dropdown");
|
||||||
selects.forEach(select => {
|
selects.forEach(select => {
|
||||||
select.innerHTML = '<option value="">-- Select a user --</option>';
|
select.innerHTML = '<option value="">-- Select a user --</option>';
|
||||||
users.forEach(user => {
|
adminUsers.forEach(user => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = user.username;
|
option.value = user.username;
|
||||||
option.textContent = user.username;
|
option.textContent = user.username;
|
||||||
@@ -136,8 +219,9 @@ async function displayWordListForSelectedUser() {
|
|||||||
if (!username) return;
|
if (!username) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/getWordList?user=${username}`);
|
const res = await fetch(`/getWordList?username=${username}`);
|
||||||
const words = await res.json();
|
const data = await res.json();
|
||||||
|
const words = data.words || [];
|
||||||
if (!words.length) {
|
if (!words.length) {
|
||||||
list.innerHTML = "<li><em>No words found for this user.</em></li>";
|
list.innerHTML = "<li><em>No words found for this user.</em></li>";
|
||||||
return;
|
return;
|
||||||
@@ -154,7 +238,7 @@ async function displayWordListForSelectedUser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 📊 Student result viewer
|
// 📊 Student result viewer
|
||||||
async function loadStudentResults() {
|
window.loadStudentResults = async function() {
|
||||||
const list = document.getElementById("resultsList");
|
const list = document.getElementById("resultsList");
|
||||||
list.innerHTML = '';
|
list.innerHTML = '';
|
||||||
|
|
||||||
@@ -167,19 +251,38 @@ async function loadStudentResults() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [username, result] of Object.entries(results)) {
|
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");
|
const li = document.createElement("li");
|
||||||
let answersHTML = '';
|
let answersHTML = '';
|
||||||
|
|
||||||
|
// Check if result and answers exist and are valid
|
||||||
|
if (result && result.answers && Array.isArray(result.answers)) {
|
||||||
result.answers.forEach(entry => {
|
result.answers.forEach(entry => {
|
||||||
const status = entry.correct ? '✅' : '❌';
|
const status = entry.correct ? '✅' : '❌';
|
||||||
answersHTML += `${status} ${entry.word}<br>`;
|
answersHTML += `${status} ${entry.word}<br>`;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
answersHTML = '<em>No answer data available</em>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = result.timestamp ? new Date(result.timestamp).toLocaleString() : 'Unknown time';
|
||||||
li.innerHTML = `
|
li.innerHTML = `
|
||||||
<strong>${username}</strong> — Score: ${result.score}, Completed: ${result.completed ? '✅' : '❌'}<br>
|
<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}
|
<em>Answers:</em><br>${answersHTML}
|
||||||
|
<hr style="margin: 1rem 0; opacity: 0.3;">
|
||||||
`;
|
`;
|
||||||
list.appendChild(li);
|
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) {
|
} catch (err) {
|
||||||
console.error("🔴 Failed to load results:", 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() {
|
async function loadUserDropdowns() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/getUsers');
|
const res = await fetch('/getUsers');
|
||||||
@@ -243,6 +312,8 @@ async function loadUserDropdowns() {
|
|||||||
drop.appendChild(opt);
|
drop.appendChild(opt);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('✅ User dropdowns loaded');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("⚠️ Failed to load user list:", err);
|
console.error("⚠️ Failed to load user list:", err);
|
||||||
}
|
}
|
||||||
@@ -352,27 +423,52 @@ async function loadStudentNamesForReport() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStudentReport(username) {
|
window.loadStudentReport = async function(username) {
|
||||||
if (!username) return;
|
if (!username) return;
|
||||||
|
|
||||||
|
try {
|
||||||
const res1 = await fetch('/getResults');
|
const res1 = await fetch('/getResults');
|
||||||
const results = await res1.json();
|
const results = await res1.json();
|
||||||
const res2 = await fetch('/getBadges');
|
const res2 = await fetch('/getBadges');
|
||||||
const badges = await res2.json();
|
const badges = await res2.json();
|
||||||
|
|
||||||
const report = results[username];
|
const userResults = results[username];
|
||||||
const badgeList = badges[username] || [];
|
const badgeList = badges[username] || [];
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!latestReport) {
|
||||||
|
document.getElementById("reportContent").classList.add("hidden");
|
||||||
|
alert(`No results found for ${username}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("reportTitle").textContent = `Report for ${username}`;
|
document.getElementById("reportTitle").textContent = `Report for ${username}`;
|
||||||
document.getElementById("reportScore").textContent = `${report.score}/${report.answers.length} correct`;
|
|
||||||
|
// 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");
|
const wordList = document.getElementById("reportWords");
|
||||||
wordList.innerHTML = "";
|
wordList.innerHTML = "";
|
||||||
report.answers.forEach(({ word, correct }) => {
|
if (answers.length > 0) {
|
||||||
|
answers.forEach(({ word, correct }) => {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.textContent = `${word} — ${correct ? '✅' : '❌'}`;
|
li.textContent = `${word} — ${correct ? '✅' : '❌'}`;
|
||||||
wordList.appendChild(li);
|
wordList.appendChild(li);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
wordList.innerHTML = "<li><em>No word data available.</em></li>";
|
||||||
|
}
|
||||||
|
|
||||||
const badgeContainer = document.getElementById("reportBadges");
|
const badgeContainer = document.getElementById("reportBadges");
|
||||||
badgeContainer.innerHTML = "";
|
badgeContainer.innerHTML = "";
|
||||||
@@ -387,9 +483,13 @@ async function loadStudentReport(username) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("reportContent").classList.remove("hidden");
|
document.getElementById("reportContent").classList.remove("hidden");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading student report:", error);
|
||||||
|
alert("Error loading student report. Please try again.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyReportToClipboard() {
|
window.copyReportToClipboard = function() {
|
||||||
const name = document.getElementById("reportTitle").textContent;
|
const name = document.getElementById("reportTitle").textContent;
|
||||||
const score = document.getElementById("reportScore").textContent;
|
const score = document.getElementById("reportScore").textContent;
|
||||||
const words = Array.from(document.querySelectorAll("#reportWords li")).map(li => li.textContent).join("\n");
|
const words = Array.from(document.querySelectorAll("#reportWords li")).map(li => li.textContent).join("\n");
|
||||||
@@ -458,3 +558,46 @@ async function loadUserSessions(username) {
|
|||||||
list.appendChild(sessionItem);
|
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);
|
||||||
|
});
|
||||||
+83
-10
@@ -1,8 +1,24 @@
|
|||||||
// 🔐 Password hashing using jsSHA
|
// 🔐 Password hashing using jsSHA
|
||||||
function hashPassword(password) {
|
function hashPassword(password) {
|
||||||
|
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");
|
const shaObj = new jsSHA("SHA-256", "TEXT");
|
||||||
shaObj.update(password);
|
shaObj.update(password);
|
||||||
return shaObj.getHash("HEX");
|
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
|
// 🌐 Load users from server
|
||||||
@@ -12,28 +28,43 @@ let users = [];
|
|||||||
async function verifyLogin() {
|
async function verifyLogin() {
|
||||||
const uname = document.getElementById('username').value.trim();
|
const uname = document.getElementById('username').value.trim();
|
||||||
const pw = document.getElementById('password').value;
|
const pw = document.getElementById('password').value;
|
||||||
const hash = hashPassword(pw);
|
|
||||||
const msg = document.getElementById('loginMessage');
|
const msg = document.getElementById('loginMessage');
|
||||||
|
|
||||||
|
if (!uname || !pw) {
|
||||||
|
msg.textContent = "Please enter both username and password.";
|
||||||
|
msg.style.color = 'var(--error)';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Attempting login for:', uname);
|
||||||
|
const hash = hashPassword(pw);
|
||||||
|
console.log('Generated hash:', hash);
|
||||||
|
|
||||||
const res = await fetch('/verifyUser', {
|
const res = await fetch('/verifyUser', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username: uname, hash })
|
body: JSON.stringify({ username: uname, hash })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const responseText = await res.text();
|
||||||
|
console.log('Server response:', res.status, responseText);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
msg.textContent = "Invalid username or password.";
|
msg.textContent = "Invalid username or password.";
|
||||||
|
msg.style.color = 'var(--error)';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await res.json();
|
const user = JSON.parse(responseText);
|
||||||
|
console.log('Login successful for user:', user);
|
||||||
localStorage.setItem('loggedInUser', JSON.stringify(user));
|
localStorage.setItem('loggedInUser', JSON.stringify(user));
|
||||||
msg.textContent = '';
|
msg.textContent = '';
|
||||||
setupSession(user);
|
setupSession(user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Login error:", err);
|
console.error("Login error:", err);
|
||||||
msg.textContent = "Login failed. Please try again.";
|
msg.textContent = "Login failed. Please try again.";
|
||||||
|
msg.style.color = 'var(--error)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,18 +72,60 @@ async function verifyLogin() {
|
|||||||
function logoutUser() {
|
function logoutUser() {
|
||||||
localStorage.removeItem('loggedInUser');
|
localStorage.removeItem('loggedInUser');
|
||||||
|
|
||||||
document.getElementById('adminPanel').classList.add('hidden');
|
// Hide all panels
|
||||||
document.getElementById('studentPanel').classList.add('hidden');
|
document.getElementById('adminPanel')?.classList.add('hidden');
|
||||||
document.getElementById('nav').classList.add('hidden');
|
document.getElementById('studentPanel')?.classList.add('hidden');
|
||||||
document.getElementById('loginPanel').classList.remove('hidden');
|
document.getElementById('nav')?.classList.add('hidden');
|
||||||
document.getElementById('username').value = '';
|
|
||||||
document.getElementById('password').value = '';
|
|
||||||
|
|
||||||
|
// 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 (usernameField) usernameField.value = '';
|
||||||
|
if (passwordField) passwordField.value = '';
|
||||||
|
if (loginMessage) {
|
||||||
|
loginMessage.textContent = '';
|
||||||
|
loginMessage.style.color = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset banner
|
||||||
const roleBanner = document.getElementById('roleBanner');
|
const roleBanner = document.getElementById('roleBanner');
|
||||||
|
if (roleBanner) {
|
||||||
roleBanner.classList.add('hidden');
|
roleBanner.classList.add('hidden');
|
||||||
roleBanner.classList.remove('admin', 'student');
|
roleBanner.classList.remove('admin', 'student');
|
||||||
roleBanner.textContent = '';
|
roleBanner.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
const loginMessage = document.getElementById('loginMessage');
|
// Focus on username field
|
||||||
if (loginMessage) loginMessage.textContent = '';
|
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 currentWord = '';
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
let results = [];
|
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() {
|
function showStudent() {
|
||||||
console.log("🔊 Showing student panel");
|
console.log("🔊 Showing student panel");
|
||||||
@@ -34,7 +73,7 @@ function showStudent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🚀 Begin game session
|
// 🚀 Begin game session
|
||||||
async function startGame() {
|
window.startGame = async function() {
|
||||||
console.log('🎮 Starting spelling game');
|
console.log('🎮 Starting spelling game');
|
||||||
|
|
||||||
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||||
@@ -62,6 +101,7 @@ async function startGame() {
|
|||||||
|
|
||||||
// Start game
|
// Start game
|
||||||
words = [...data.words];
|
words = [...data.words];
|
||||||
|
typingWords = [...data.words]; // Initialize typing words too
|
||||||
currentIndex = 0;
|
currentIndex = 0;
|
||||||
results = []; // Reset results array
|
results = []; // Reset results array
|
||||||
showWord();
|
showWord();
|
||||||
|
|||||||
+44
-12
@@ -1,22 +1,54 @@
|
|||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
// 🌐 Load users.json for login validation
|
console.log('🚀 Application starting...');
|
||||||
// await loadUsers();
|
|
||||||
|
// Load users for admin functionality
|
||||||
|
try {
|
||||||
|
await loadUsers();
|
||||||
displayUserDropdown();
|
displayUserDropdown();
|
||||||
populateWordUserDropdown();
|
populateWordUserDropdown();
|
||||||
|
} catch (e) {
|
||||||
// 🎨 Restore saved theme
|
console.warn('Could not load users (normal if not admin):', e);
|
||||||
applySavedTheme();
|
|
||||||
|
|
||||||
// 🔁 Restore session if user was logged in
|
|
||||||
const savedUser = localStorage.getItem("loggedInUser");
|
|
||||||
if (savedUser) {
|
|
||||||
setupSession(JSON.parse(savedUser));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🧭 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');
|
const defaultTab = document.querySelector('#adminTabs button.active');
|
||||||
if (defaultTab) {
|
if (defaultTab) {
|
||||||
const targetId = defaultTab.getAttribute("onclick")?.match(/'(.*?)'/)?.[1];
|
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() {
|
function applySavedTheme() {
|
||||||
const savedTheme = localStorage.getItem('theme') || 'playground';
|
const savedTheme = localStorage.getItem('theme') || 'default';
|
||||||
document.body.className = savedTheme;
|
applyTheme(savedTheme);
|
||||||
|
|
||||||
const themeSelector = document.getElementById('themeSelect');
|
const themeSelector = document.getElementById('themeSelect');
|
||||||
if (themeSelector) themeSelector.value = savedTheme;
|
if (themeSelector) themeSelector.value = savedTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎨 Change theme dynamically
|
|
||||||
function changeTheme(themeName) {
|
function changeTheme(themeName) {
|
||||||
document.body.className = themeName;
|
applyTheme(themeName);
|
||||||
localStorage.setItem('theme', 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: ''
|
currentWord: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
async function startTypingPractice() {
|
// Make functions globally accessible
|
||||||
|
window.startTypingPractice = async function() {
|
||||||
console.log('Starting typing practice');
|
console.log('Starting typing practice');
|
||||||
|
|
||||||
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
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 promptEl = document.getElementById('typingPrompt');
|
||||||
const inputEl = document.getElementById('typingInput');
|
const inputEl = document.getElementById('typingInput');
|
||||||
if (!promptEl || !inputEl) return;
|
if (!promptEl || !inputEl) return;
|
||||||
@@ -59,13 +60,7 @@ function showNextWord() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Reset input
|
window.submitTyping = function() {
|
||||||
inputEl.value = '';
|
|
||||||
inputEl.disabled = false;
|
|
||||||
inputEl.focus();
|
|
||||||
|
|
||||||
|
|
||||||
function submitTyping() {
|
|
||||||
const input = document.getElementById('typingInput')?.value.trim() || '';
|
const input = document.getElementById('typingInput')?.value.trim() || '';
|
||||||
const feedback = document.getElementById('typingFeedback');
|
const feedback = document.getElementById('typingFeedback');
|
||||||
if (!feedback) return;
|
if (!feedback) return;
|
||||||
|
|||||||
+49
-7
@@ -46,16 +46,58 @@ function showToast(message, duration = 3000) {
|
|||||||
toast.classList.remove("show");
|
toast.classList.remove("show");
|
||||||
}, duration);
|
}, duration);
|
||||||
}
|
}
|
||||||
|
function showStudent() {
|
||||||
|
console.log("🎓 Showing student panel");
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showAdmin() {
|
function showAdmin() {
|
||||||
console.log("showAdmin fired");
|
console.log("👨💼 Showing admin panel");
|
||||||
|
|
||||||
|
// Hide other panels
|
||||||
|
document.getElementById('studentPanel')?.classList.add('hidden');
|
||||||
|
document.getElementById('loginPanel')?.classList.add('hidden');
|
||||||
|
|
||||||
const adminPanel = document.getElementById("adminPanel");
|
const adminPanel = document.getElementById("adminPanel");
|
||||||
const studentPanel = document.getElementById("studentPanel");
|
if (adminPanel) {
|
||||||
|
adminPanel.classList.remove("hidden");
|
||||||
|
|
||||||
loadUserDropdowns();
|
// Load user data for dropdowns
|
||||||
|
loadUserDropdowns().then(() => {
|
||||||
|
console.log("✅ User dropdowns loaded");
|
||||||
|
}).catch(e => {
|
||||||
|
console.warn("Could not load user dropdowns:", e);
|
||||||
|
});
|
||||||
|
|
||||||
adminPanel?.classList.remove("hidden");
|
// Set default tab - use a slight delay to ensure DOM is ready
|
||||||
studentPanel?.classList.add("hidden");
|
setTimeout(() => {
|
||||||
|
if (typeof window.switchTab === 'function') {
|
||||||
switchTab("tabUsers"); // optional default tab
|
window.switchTab("tabUsers");
|
||||||
|
console.log("✅ Default tab set");
|
||||||
|
} else {
|
||||||
|
console.error("switchTab function not available");
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
console.error("Admin panel not found");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+349
-271
@@ -1,319 +1,397 @@
|
|||||||
/* 📋 Global Defaults */
|
/* Modern Spelling App Styles */
|
||||||
body {
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
font-family: sans-serif;
|
|
||||||
text-align: center;
|
/* Reset and base styles */
|
||||||
padding: 2rem;
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
box-sizing: border-box;
|
||||||
li {
|
|
||||||
margin: 0.25rem 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input, textarea, select {
|
:root {
|
||||||
font-size: 1rem;
|
/* Modern color palette */
|
||||||
padding: 0.5rem;
|
--primary: #3b82f6;
|
||||||
margin: 0.5rem;
|
--primary-dark: #2563eb;
|
||||||
border: 2px solid #ccc;
|
--secondary: #f1f5f9;
|
||||||
border-radius: 5px;
|
--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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 🎨 Theme Styles */
|
body {
|
||||||
body.playground {
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
background: #fef9e7;
|
background: linear-gradient(135deg, var(--bg-secondary) 0%, #ffffff 100%);
|
||||||
color: #333;
|
color: var(--text-primary);
|
||||||
font-family: 'Comic Sans MS', cursive, sans-serif;
|
line-height: 1.6;
|
||||||
}
|
min-height: 100vh;
|
||||||
body.chalkboard {
|
padding: 2rem 1rem;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 🔘 Buttons */
|
/* Container and layout */
|
||||||
button {
|
.container {
|
||||||
background-color: #4caf50;
|
max-width: 1200px;
|
||||||
color: white;
|
margin: 0 auto;
|
||||||
border: none;
|
padding: 0 1rem;
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.6rem 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin: 0.4rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 🌈 Theme-specific Button Colors */
|
/* Typography */
|
||||||
body.chalkboard button {
|
h1 {
|
||||||
background-color: #555;
|
|
||||||
}
|
|
||||||
body.galaxy button {
|
|
||||||
background-color: #6200ea;
|
|
||||||
}
|
|
||||||
body.nature button {
|
|
||||||
background-color: #81c784;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🔳 Panels and Banners */
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#wordBox {
|
|
||||||
font-size: 2em;
|
|
||||||
margin: 20px;
|
|
||||||
min-height: 60px;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
.word-box {
|
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
margin-bottom: 2rem;
|
|
||||||
color: #2196f3;
|
|
||||||
text-shadow: 1px 1px #ccc;
|
|
||||||
}
|
|
||||||
.banner {
|
|
||||||
padding: 0.5em;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
margin-bottom: 2rem;
|
||||||
color: white;
|
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||||
}
|
-webkit-background-clip: text;
|
||||||
.banner.admin {
|
-webkit-text-fill-color: transparent;
|
||||||
background-color: #b22222;
|
background-clip: text;
|
||||||
}
|
|
||||||
.banner.student {
|
|
||||||
background-color: #1e90ff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 🔔 Toast Messages */
|
h2 {
|
||||||
.toast {
|
font-size: 1.875rem;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🧭 Admin Tabs */
|
|
||||||
#adminTabs {
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
#adminTabs button {
|
|
||||||
margin-right: 8px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-bottom: none;
|
|
||||||
border-radius: 6px 6px 0 0;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
margin-bottom: 1.5rem;
|
||||||
cursor: pointer;
|
color: var(--text-primary);
|
||||||
transition: background-color 0.2s, color 0.2s;
|
|
||||||
}
|
|
||||||
#adminTabs button:hover {
|
|
||||||
background-color: #e0f2ff;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
#adminTabs button.active {
|
|
||||||
background-color: #ffffff;
|
|
||||||
color: #000;
|
|
||||||
border-bottom: 2px solid #fff;
|
|
||||||
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.adminTab {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.adminTab:not(.hidden) {
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.typing-sentence {
|
h3 {
|
||||||
font-size: 1.5em;
|
font-size: 1.25rem;
|
||||||
margin: 20px 0;
|
font-weight: 600;
|
||||||
padding: 15px;
|
margin-bottom: 1rem;
|
||||||
background-color: #f5f5f5;
|
color: var(--text-primary);
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
min-height: 50px;
|
|
||||||
display: block !important;
|
|
||||||
visibility: visible !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove these style classes if they exist */
|
/* Form elements */
|
||||||
.typing-section
|
input, textarea, select {
|
||||||
.typing-container
|
width: 100%;
|
||||||
.typing-prompt
|
max-width: 320px;
|
||||||
.input-container
|
padding: 0.75rem 1rem;
|
||||||
#typingInput
|
margin: 0.5rem;
|
||||||
#typingFeedback
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/* Fix for start buttons visibility */
|
input:focus, textarea:focus, select:focus {
|
||||||
#startGameBtn, #startTypingBtn {
|
outline: none;
|
||||||
display: inline-block;
|
border-color: var(--primary);
|
||||||
margin: 10px;
|
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
|
||||||
padding: 12px 24px;
|
}
|
||||||
font-size: 1.1em;
|
|
||||||
background-color: #4CAF50;
|
textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
margin: 0.25rem;
|
||||||
|
background: var(--primary);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: var(--radius);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
#startGameBtn:hover, #startTypingBtn:hover {
|
button:hover {
|
||||||
transform: scale(1.05);
|
background: var(--primary-dark);
|
||||||
background-color: #45a049;
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override any hidden class for these buttons */
|
button:active {
|
||||||
#startGameBtn.hidden, #startTypingBtn.hidden {
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button container for better spacing */
|
.flex {
|
||||||
.button-container {
|
display: flex;
|
||||||
margin: 20px 0;
|
}
|
||||||
padding: 10px;
|
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-4 {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginPanel form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Banner */
|
||||||
|
.banner {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#adminTabs button:hover {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#adminTabs button.active {
|
||||||
|
background: var(--primary) !important;
|
||||||
|
color: white !important;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.adminTab {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.adminTab.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 {
|
.student-controls {
|
||||||
display: block !important;
|
|
||||||
margin: 20px auto;
|
|
||||||
padding: 15px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
visibility: visible !important;
|
margin: 2rem 0;
|
||||||
|
padding: 2rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.student-btn {
|
/* Lists */
|
||||||
display: inline-block !important;
|
ul {
|
||||||
visibility: visible !important;
|
list-style: none;
|
||||||
background-color: #4CAF50;
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast notifications */
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 2rem;
|
||||||
|
right: 2rem;
|
||||||
|
background: var(--text-primary);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 12px 24px;
|
padding: 1rem 1.5rem;
|
||||||
margin: 10px;
|
border-radius: var(--radius);
|
||||||
border: none;
|
opacity: 0;
|
||||||
border-radius: 5px;
|
transform: translateY(100%);
|
||||||
font-size: 16px;
|
transition: var(--transition);
|
||||||
cursor: pointer;
|
z-index: 1000;
|
||||||
opacity: 1 !important;
|
box-shadow: var(--shadow-lg);
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override any hiding classes for these specific buttons */
|
.toast.show {
|
||||||
#startGameBtn:not(.hidden),
|
opacity: 1;
|
||||||
#startTypingBtn:not(.hidden) {
|
transform: translateY(0);
|
||||||
display: inline-block !important;
|
|
||||||
visibility: visible !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove this if it exists as it might conflict */
|
/* Responsive design */
|
||||||
.hidden {
|
@media (max-width: 768px) {
|
||||||
display: none !important;
|
body {
|
||||||
}
|
padding: 1rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Add this to ensure student panel shows properly */
|
h1 {
|
||||||
#studentPanel:not(.hidden) {
|
font-size: 2rem;
|
||||||
display: block !important;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* 📝 Typing Practice Styles */
|
#adminTabs {
|
||||||
.typing-section {
|
flex-direction: column;
|
||||||
max-width: 800px;
|
}
|
||||||
margin: 20px auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#typingPrompt {
|
#adminTabs button {
|
||||||
font-size: 2em;
|
flex: none;
|
||||||
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 {
|
.card, .adminTab, #loginPanel {
|
||||||
width: 300px;
|
padding: 1rem;
|
||||||
padding: 10px;
|
}
|
||||||
font-size: 18px;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.typing-word {
|
#wordBox, .word-box, #typingPrompt {
|
||||||
font-size: 32px;
|
font-size: 2rem;
|
||||||
font-weight: bold;
|
padding: 2rem 1rem;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
@@ -54,12 +54,25 @@ function readJsonSafe(filePath, fallback = {}) {
|
|||||||
// 🔐 Verify login
|
// 🔐 Verify login
|
||||||
app.post('/verifyUser', (req, res) => {
|
app.post('/verifyUser', (req, res) => {
|
||||||
const { username, hash } = req.body;
|
const { username, hash } = req.body;
|
||||||
|
console.log('Login attempt - Username:', username, 'Hash:', hash);
|
||||||
|
|
||||||
if (typeof username !== 'string' || typeof hash !== 'string') {
|
if (typeof username !== 'string' || typeof hash !== 'string') {
|
||||||
|
console.log('Invalid credentials format');
|
||||||
return res.status(400).send("Invalid credentials format");
|
return res.status(400).send("Invalid credentials format");
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = readJsonSafe(path.join(DATA_DIR, files.users), []);
|
const users = readJsonSafe(path.join(DATA_DIR, files.users), []);
|
||||||
|
console.log('Available users:', users.map(u => u.username));
|
||||||
|
|
||||||
const user = users.find(u => u.username === username && u.hash === hash);
|
const user = users.find(u => u.username === username && u.hash === hash);
|
||||||
user ? res.json(user) : res.status(401).send("Invalid login");
|
|
||||||
|
if (user) {
|
||||||
|
console.log('Login successful for:', username);
|
||||||
|
res.json(user);
|
||||||
|
} else {
|
||||||
|
console.log('Login failed for:', username);
|
||||||
|
res.status(401).send("Invalid login");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ➕ Add user
|
// ➕ Add user
|
||||||
|
|||||||
Reference in new issue
Block a user