module change
This commit is contained in:
1 parent
ed1cafe761
commit
77d5f9b196
15 files changed
+886
-698
No files matched your search
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"student1": {
|
||||||
|
"score": 8,
|
||||||
|
"completed": true,
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"word": "apple",
|
||||||
|
"correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "banana",
|
||||||
|
"correct": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "grape",
|
||||||
|
"correct": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nate": {
|
||||||
|
"score": 1,
|
||||||
|
"completed": true,
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"word": "not",
|
||||||
|
"correct": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "bot",
|
||||||
|
"correct": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "snot",
|
||||||
|
"correct": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,14 @@
|
|||||||
"hash": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
|
"hash": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
|
||||||
"role": "admin"
|
"role": "admin"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"username": "student1",
|
|
||||||
"hash": "8d969eef6ecad3c29a3a629280e686cf",
|
|
||||||
"role": "student"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"username": "nate",
|
"username": "nate",
|
||||||
"hash": "5a2a558c78d3717db731600c4f354fa1d9c84b556f108091a891f444f1bdec40",
|
"hash": "5a2a558c78d3717db731600c4f354fa1d9c84b556f108091a891f444f1bdec40",
|
||||||
"role": "student"
|
"role": "student"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "student1",
|
||||||
|
"hash": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
|
||||||
|
"role": "student"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"student1": [],
|
||||||
|
"admin1": [
|
||||||
|
"boy",
|
||||||
|
"bot",
|
||||||
|
"snot"
|
||||||
|
],
|
||||||
|
"nate": [
|
||||||
|
"not",
|
||||||
|
"bot",
|
||||||
|
"snot"
|
||||||
|
]
|
||||||
|
}
|
||||||
+87
-73
@@ -2,15 +2,26 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Spelling App with Roles</title>
|
<title>Spelling App with Roles</title>
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css" />
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsSHA/2.4.2/sha.js"></script>
|
|
||||||
<script src="script.js" defer></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
|
<!-- 🔧 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/admin.js" defer></script>
|
||||||
|
<script src="js/game.js" defer></script>
|
||||||
|
<script src="js/main.js" defer></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
<h1>Spelling Practice</h1>
|
<h1>Spelling Practice</h1>
|
||||||
|
|
||||||
|
<!-- 🏳️ Role Banner -->
|
||||||
<div id="roleBanner" class="banner hidden"></div>
|
<div id="roleBanner" class="banner hidden"></div>
|
||||||
|
|
||||||
<!-- 👤 Login Panel -->
|
<!-- 👤 Login Panel -->
|
||||||
<div id="loginPanel">
|
<div id="loginPanel">
|
||||||
<h2>User Login</h2>
|
<h2>User Login</h2>
|
||||||
@@ -22,102 +33,105 @@
|
|||||||
<p id="loginMessage"></p>
|
<p id="loginMessage"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 🔀 Navigation -->
|
<!-- 🔀 Role-Based Navigation -->
|
||||||
<div id="nav" class="hidden">
|
<div id="nav" class="hidden">
|
||||||
<p><strong id="userInfo"></strong></p>
|
<p><strong id="userInfo"></strong></p>
|
||||||
<button onclick="showAdmin()" id="adminBtn" class="hidden">Admin Panel</button>
|
<button onclick="showAdmin()" id="adminBtn" class="hidden">Admin Panel</button>
|
||||||
<button onclick="showStudent()" id="studentBtn">Student Panel</button>
|
<button onclick="showStudent()" id="studentBtn" class="hidden">Student Panel</button>
|
||||||
<button onclick="logoutUser()">Log Out</button>
|
<button onclick="logoutUser()" id="logoutBtn">Log Out</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 👨🏫 Admin Panel -->
|
||||||
<div id="adminPanel" class="hidden">
|
<div id="adminPanel" class="hidden">
|
||||||
<h2>👨🏫 Admin Panel</h2>
|
<h2>👨🏫 Admin Panel</h2>
|
||||||
|
|
||||||
<!-- Tab Controls -->
|
<!-- 🔘 Tabs -->
|
||||||
<div id="adminTabs">
|
<div id="adminTabs">
|
||||||
<button onclick="switchTab('tabUsers')" class="active">👤 Manage Users</button>
|
<button onclick="switchTab('tabUsers')" class="active">👤 Manage Users</button>
|
||||||
<button onclick="switchTab('tabWords')">📝 Word Lists</button>
|
<button onclick="switchTab('tabWords')">📝 Word Lists</button>
|
||||||
<button onclick="switchTab('tabResults')">📊 Results</button>
|
<button onclick="switchTab('tabResults')">📊 Results</button>
|
||||||
<button onclick="switchTab('tabTheme')">🎨 Theme</button>
|
<button onclick="switchTab('tabTheme')">🎨 Theme</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Manage Users -->
|
<!-- 👥 Users Tab -->
|
||||||
<div id="tabUsers" class="adminTab">
|
<div id="tabUsers" class="adminTab">
|
||||||
<h3>Add New User</h3>
|
<h3>Add New User</h3>
|
||||||
<form onsubmit="event.preventDefault(); addNewUser();">
|
<form onsubmit="event.preventDefault(); addNewUser();">
|
||||||
<input type="text" id="newUsername" placeholder="Username" required />
|
<input type="text" id="newUsername" placeholder="Username" required />
|
||||||
<input type="password" id="newPassword" placeholder="Password" required />
|
<input type="password" id="newPassword" placeholder="Password" required />
|
||||||
<select id="newRole">
|
<select id="newRole">
|
||||||
<option value="student">Student</option>
|
<option value="student">Student</option>
|
||||||
<option value="admin">Admin</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>
|
</select>
|
||||||
<button type="submit">Add User</button>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
||||||
<h3>Delete Users</h3>
|
<!-- 📝 Word List Tab -->
|
||||||
<select id="userDropdown" onchange="selectUserAction(this.value)">
|
<div id="tabWords" class="adminTab hidden">
|
||||||
<option value="">-- Select a user --</option>
|
<h3>Edit Word List for:</h3>
|
||||||
</select>
|
<select id="wordUserSelect" class="word-user-dropdown" onchange="loadWordsForSelectedUser()">
|
||||||
</div>
|
<option value="">-- Select a user --</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<!-- Word Lists -->
|
<h2>Enter Spelling Words</h2>
|
||||||
<div id="tabWords" class="adminTab hidden">
|
<textarea id="wordInput" placeholder="Enter one word per line..."></textarea><br/>
|
||||||
<h3>Edit Word List for:</h3>
|
<button onclick="saveWords()">Save Word List</button>
|
||||||
<select id="wordUserSelect" class="word-user-dropdown" onchange="loadWordsForSelectedUser()">
|
<button onclick="clearWords()">Clear Saved Words</button>
|
||||||
<option value="">-- Select a user --</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<h2>Enter Spelling Words</h2>
|
<h3>Upload a Word File</h3>
|
||||||
<textarea id="wordInput" placeholder="Enter one word per line..."></textarea><br/>
|
<input type="file" accept=".txt,.csv" onchange="handleFileUpload(event)" />
|
||||||
<button onclick="saveWords()">Save Word List</button>
|
|
||||||
<button onclick="clearWords()">Clear Saved Words</button>
|
|
||||||
|
|
||||||
<h3>Or Upload a Word File</h3>
|
<p><strong>Current Word List:</strong></p>
|
||||||
<input type="file" accept=".txt,.csv" onchange="handleFileUpload(event)" />
|
<ul id="wordListDisplay"></ul>
|
||||||
<p><strong>Current Word List:</strong></p>
|
|
||||||
<ul id="wordListDisplay"></ul>
|
|
||||||
|
|
||||||
<h3>📋 View Word List by User:</h3>
|
<h3>📋 View Word List by User:</h3>
|
||||||
<select id="viewUserSelect" class="word-user-dropdown" onchange="displayWordListForSelectedUser()">
|
<select id="viewUserSelect" class="word-user-dropdown" onchange="displayWordListForSelectedUser()">
|
||||||
<option value="">-- Select a user --</option>
|
<option value="">-- Select a user --</option>
|
||||||
</select>
|
</select>
|
||||||
|
<ul id="userWordListDisplay"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul id="userWordListDisplay"></ul>
|
<!-- 📊 Results Tab -->
|
||||||
</div>
|
<div id="tabResults" class="adminTab hidden">
|
||||||
|
<h3>🧾 Student Results Overview</h3>
|
||||||
|
<button onclick="loadStudentResults()">Refresh Results</button>
|
||||||
|
<ul id="resultsList"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Student Results -->
|
<!-- 🎨 Theme Tab -->
|
||||||
<div id="tabResults" class="adminTab hidden">
|
<div id="tabTheme" class="adminTab hidden">
|
||||||
<h3>🧾 Student Results Overview</h3>
|
<h3>🎨 Select Theme</h3>
|
||||||
<button onclick="loadStudentResults()">Refresh Results</button>
|
<select id="themeSelect" onchange="changeTheme(this.value)">
|
||||||
<ul id="resultsList"></ul>
|
<option value="playground">Playground</option>
|
||||||
</div>
|
<option value="chalkboard">Chalkboard</option>
|
||||||
|
<option value="galaxy">Galaxy</option>
|
||||||
<!-- Theme Selection -->
|
<option value="nature">Nature</option>
|
||||||
<div id="tabTheme" class="adminTab hidden">
|
</select>
|
||||||
<h3>🎨 Select Theme</h3>
|
</div>
|
||||||
<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>
|
||||||
|
|
||||||
<!-- 👩🎓 Student Panel -->
|
<!-- 👩🎓 Student Panel -->
|
||||||
<div id="studentPanel" class="hidden">
|
<div id="studentPanel" class="hidden">
|
||||||
<button onclick="startGame()">Start Practice</button>
|
<div id="toast" class="toast">Welcome, student!</div>
|
||||||
|
|
||||||
|
<button id="startGameBtn" class="hidden" onclick="startGame()">Start Game</button>
|
||||||
|
|
||||||
<div id="wordBox" class="hidden">
|
<div id="wordBox" class="hidden">
|
||||||
<div id="word" class="word-box"></div>
|
<div id="word" class="word-box"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="inputSection" class="hidden">
|
<div id="inputSection" class="hidden">
|
||||||
<input type="text" id="userInput" placeholder="Spell the word" />
|
<input type="text" id="userInput" placeholder="Type your answer" autocomplete="off" />
|
||||||
<button onclick="submitAnswer()">Submit</button>
|
<button onclick="submitAnswer()">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="summary" class="hidden"></div>
|
<div id="summary" class="hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="toast" class="toast"></div>
|
</body>
|
||||||
</body>
|
</html>
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
// ➕ Add a new user
|
||||||
|
async function addNewUser() {
|
||||||
|
const username = document.getElementById("newUsername").value.trim();
|
||||||
|
const password = document.getElementById("newPassword").value;
|
||||||
|
const role = document.getElementById("newRole").value;
|
||||||
|
const msg = document.getElementById("userAddMessage");
|
||||||
|
const hashPreview = document.getElementById("hashPreview");
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
msg.textContent = "Please fill out all fields.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = hashPassword(password);
|
||||||
|
if (hashPreview) hashPreview.textContent = hash;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch('/addUser', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, hash, role })
|
||||||
|
});
|
||||||
|
msg.textContent = `✅ User "${username}" added to server.`;
|
||||||
|
|
||||||
|
document.getElementById("newUsername").value = '';
|
||||||
|
document.getElementById("newPassword").value = '';
|
||||||
|
if (hashPreview) hashPreview.textContent = '[auto]';
|
||||||
|
|
||||||
|
await loadUsers();
|
||||||
|
displayUserDropdown();
|
||||||
|
} catch (e) {
|
||||||
|
msg.textContent = "❌ Failed to add user.";
|
||||||
|
console.error("Add user error:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🗑 Populate and delete users
|
||||||
|
function displayUserDropdown() {
|
||||||
|
const dropdown = document.getElementById("userDropdown");
|
||||||
|
dropdown.innerHTML = `<option value="">-- Select a user --</option>`;
|
||||||
|
users.forEach(user => {
|
||||||
|
dropdown.innerHTML += `<option value="${user.username}">${user.username} (${user.role})</option>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectUserAction(username) {
|
||||||
|
if (!username) return;
|
||||||
|
|
||||||
|
if (!confirm(`Are you sure you want to delete user "${username}"?`)) {
|
||||||
|
document.getElementById("userDropdown").value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch("/deleteUser", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ username })
|
||||||
|
})
|
||||||
|
.then(res => res.text())
|
||||||
|
.then(msg => {
|
||||||
|
alert(msg);
|
||||||
|
loadUsers().then(displayUserDropdown);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📚 Save or clear word list for current user
|
||||||
|
async function saveWords() {
|
||||||
|
const selectedUser = document.getElementById('wordUserSelect')?.value;
|
||||||
|
if (!selectedUser) {
|
||||||
|
alert("⚠️ Please select a user before saving.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = document.getElementById('wordInput').value.trim();
|
||||||
|
allWords = text.split('\n').map(w => w.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
await fetch('/saveWordList', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username: selectedUser, words: allWords })
|
||||||
|
});
|
||||||
|
|
||||||
|
displayWordList();
|
||||||
|
alert(`✅ Word list saved to server for ${selectedUser}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearWords() {
|
||||||
|
const selectedUser = document.getElementById('wordUserSelect')?.value;
|
||||||
|
if (!selectedUser) return;
|
||||||
|
|
||||||
|
allWords = [];
|
||||||
|
await fetch('/saveWordList', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username: selectedUser, words: [] })
|
||||||
|
});
|
||||||
|
|
||||||
|
displayWordList();
|
||||||
|
alert(`🗑 Word list cleared for ${selectedUser}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🗂 Load and display word lists
|
||||||
|
async function loadWordsForSelectedUser() {
|
||||||
|
const selectedUser = document.getElementById("wordUserSelect").value;
|
||||||
|
if (!selectedUser) return;
|
||||||
|
|
||||||
|
const res = await fetch(`/getWordList?user=${encodeURIComponent(selectedUser)}`);
|
||||||
|
allWords = await res.json();
|
||||||
|
displayWordList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayWordList() {
|
||||||
|
const ul = document.getElementById('wordListDisplay');
|
||||||
|
ul.innerHTML = allWords.map(word => `<li>${word}</li>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateWordUserDropdown() {
|
||||||
|
const selects = document.querySelectorAll(".word-user-dropdown");
|
||||||
|
selects.forEach(select => {
|
||||||
|
select.innerHTML = '<option value="">-- Select a user --</option>';
|
||||||
|
users.forEach(user => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = user.username;
|
||||||
|
option.textContent = user.username;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function displayWordListForSelectedUser() {
|
||||||
|
const select = document.getElementById("viewUserSelect");
|
||||||
|
const username = select.value;
|
||||||
|
const list = document.getElementById("userWordListDisplay");
|
||||||
|
list.innerHTML = '';
|
||||||
|
|
||||||
|
if (!username) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/getWordList?user=${username}`);
|
||||||
|
const words = await res.json();
|
||||||
|
if (!words.length) {
|
||||||
|
list.innerHTML = "<li><em>No words found for this user.</em></li>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
words.forEach(word => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.textContent = word;
|
||||||
|
list.appendChild(li);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load word list for user:", username, err);
|
||||||
|
list.innerHTML = `<li style="color:red">❌ Error loading words</li>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📊 Student result viewer
|
||||||
|
async function loadStudentResults() {
|
||||||
|
const list = document.getElementById("resultsList");
|
||||||
|
list.innerHTML = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/getResults');
|
||||||
|
const results = await res.json();
|
||||||
|
|
||||||
|
if (!Object.keys(results).length) {
|
||||||
|
list.innerHTML = '<li><em>No results available yet.</em></li>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [username, result] of Object.entries(results)) {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
let answersHTML = '';
|
||||||
|
result.answers.forEach(entry => {
|
||||||
|
const status = entry.correct ? '✅' : '❌';
|
||||||
|
answersHTML += `${status} ${entry.word}<br>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
li.innerHTML = `
|
||||||
|
<strong>${username}</strong> — Score: ${result.score}, Completed: ${result.completed ? '✅' : '❌'}<br>
|
||||||
|
<em>Answers:</em><br>${answersHTML}
|
||||||
|
`;
|
||||||
|
list.appendChild(li);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("🔴 Failed to load results:", err);
|
||||||
|
list.innerHTML = '<li style="color:red">Error loading results.</li>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧭 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'));
|
||||||
|
|
||||||
|
document.getElementById(targetId).classList.remove('hidden');
|
||||||
|
const buttons = document.querySelectorAll('#adminTabs button');
|
||||||
|
buttons.forEach(btn => {
|
||||||
|
if (btn.getAttribute('onclick')?.includes(targetId)) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function loadUserDropdowns() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/getUsers');
|
||||||
|
const users = await res.json();
|
||||||
|
|
||||||
|
const dropdowns = [
|
||||||
|
document.getElementById('userDropdown'),
|
||||||
|
document.getElementById('wordUserSelect'),
|
||||||
|
document.getElementById('viewUserSelect')
|
||||||
|
];
|
||||||
|
|
||||||
|
dropdowns.forEach(drop => {
|
||||||
|
if (!drop) return;
|
||||||
|
|
||||||
|
drop.innerHTML = '<option value="">-- Select a user --</option>';
|
||||||
|
users.forEach(u => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = u.username;
|
||||||
|
opt.textContent = u.username;
|
||||||
|
drop.appendChild(opt);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("⚠️ Failed to load user list:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// 🔐 Password hashing using jsSHA
|
||||||
|
function hashPassword(password) {
|
||||||
|
const shaObj = new jsSHA("SHA-256", "TEXT");
|
||||||
|
shaObj.update(password);
|
||||||
|
return shaObj.getHash("HEX");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🌐 Load users from server
|
||||||
|
let users = [];
|
||||||
|
|
||||||
|
// 🔓 Verify user login
|
||||||
|
async function verifyLogin() {
|
||||||
|
const uname = document.getElementById('username').value.trim();
|
||||||
|
const pw = document.getElementById('password').value;
|
||||||
|
const hash = hashPassword(pw);
|
||||||
|
const msg = document.getElementById('loginMessage');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/verifyUser', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username: uname, hash })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
msg.textContent = "Invalid username or password.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await res.json();
|
||||||
|
localStorage.setItem('loggedInUser', JSON.stringify(user));
|
||||||
|
msg.textContent = '';
|
||||||
|
setupSession(user);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Login error:", err);
|
||||||
|
msg.textContent = "Login failed. Please try again.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🚪 Log out user
|
||||||
|
function logoutUser() {
|
||||||
|
localStorage.removeItem('loggedInUser');
|
||||||
|
|
||||||
|
document.getElementById('adminPanel').classList.add('hidden');
|
||||||
|
document.getElementById('studentPanel').classList.add('hidden');
|
||||||
|
document.getElementById('nav').classList.add('hidden');
|
||||||
|
document.getElementById('loginPanel').classList.remove('hidden');
|
||||||
|
document.getElementById('username').value = '';
|
||||||
|
document.getElementById('password').value = '';
|
||||||
|
|
||||||
|
const roleBanner = document.getElementById('roleBanner');
|
||||||
|
roleBanner.classList.add('hidden');
|
||||||
|
roleBanner.classList.remove('admin', 'student');
|
||||||
|
roleBanner.textContent = '';
|
||||||
|
|
||||||
|
const loginMessage = document.getElementById('loginMessage');
|
||||||
|
if (loginMessage) loginMessage.textContent = '';
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
// 🌍 Game state
|
||||||
|
let allWords = [], words = [], current = 0, results = [];
|
||||||
|
|
||||||
|
// 👩🎓 Show student interface
|
||||||
|
function showStudent() {
|
||||||
|
console.log("🔊 showStudent() executed");
|
||||||
|
|
||||||
|
const studentPanel = document.getElementById('studentPanel');
|
||||||
|
const adminPanel = document.getElementById('adminPanel');
|
||||||
|
studentPanel?.classList.remove('hidden');
|
||||||
|
adminPanel?.classList.add('hidden');
|
||||||
|
|
||||||
|
// Delay button reveal until DOM is fully settled
|
||||||
|
setTimeout(() => {
|
||||||
|
const btn = document.getElementById('startGameBtn');
|
||||||
|
if (btn) {
|
||||||
|
console.log("🎯 Button found in delayed reveal");
|
||||||
|
btn.classList.remove('hidden');
|
||||||
|
btn.style.display = 'inline-block';
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ Button still missing after timeout");
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🚀 Begin game session
|
||||||
|
async function startGame() {
|
||||||
|
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
const res = await fetch(`/getWordList?user=${encodeURIComponent(user.username)}`);
|
||||||
|
allWords = await res.json();
|
||||||
|
|
||||||
|
if (!allWords.length) {
|
||||||
|
alert("No words found. Please add words in Admin Panel.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
words = [...allWords];
|
||||||
|
current = 0;
|
||||||
|
results = [];
|
||||||
|
|
||||||
|
document.getElementById('summary')?.classList.add('hidden');
|
||||||
|
showWord();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔤 Show a word, then hide after timeout
|
||||||
|
function showWord() {
|
||||||
|
const wordBox = document.getElementById('wordBox');
|
||||||
|
const wordEl = document.getElementById('word');
|
||||||
|
|
||||||
|
if (!words[current]) {
|
||||||
|
endGame(); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wordEl.textContent = words[current];
|
||||||
|
wordBox.classList.remove('hidden');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
wordBox.classList.add('hidden');
|
||||||
|
document.getElementById('inputSection').classList.remove('hidden');
|
||||||
|
document.getElementById('userInput').value = '';
|
||||||
|
document.getElementById('userInput').focus();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📝 Check user’s answer and move to next
|
||||||
|
function submitAnswer() {
|
||||||
|
const input = document.getElementById('userInput').value.trim();
|
||||||
|
results.push({ word: words[current], attempt: input });
|
||||||
|
current++;
|
||||||
|
|
||||||
|
document.getElementById('inputSection').classList.add('hidden');
|
||||||
|
|
||||||
|
if (current < words.length) {
|
||||||
|
showWord();
|
||||||
|
} else {
|
||||||
|
showSummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📊 Show results summary
|
||||||
|
function showSummary() {
|
||||||
|
const summary = document.getElementById('summary');
|
||||||
|
let correct = 0;
|
||||||
|
let missed = [];
|
||||||
|
|
||||||
|
const report = results.map(({ word, attempt }) => {
|
||||||
|
const isCorrect = word.toLowerCase() === attempt.toLowerCase();
|
||||||
|
if (isCorrect) correct++;
|
||||||
|
else missed.push(word);
|
||||||
|
return `<li>${word} — <strong style="color:${isCorrect ? 'green' : 'red'}">${attempt || '(no answer)'}</strong></li>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
summary.innerHTML = `
|
||||||
|
<h2>Results</h2>
|
||||||
|
<p>You got ${correct} out of ${words.length} correct (${Math.round((correct / words.length) * 100)}%)</p>
|
||||||
|
<ul>${report}</ul>
|
||||||
|
${missed.length ? `<button onclick='retryMissed(${JSON.stringify(missed)})'>Retry Missed Words</button>` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
summary.classList.remove('hidden');
|
||||||
|
|
||||||
|
// 📨 Save results to server
|
||||||
|
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||||
|
if (user) {
|
||||||
|
console.log("📡 Sending results to server:", {
|
||||||
|
username: user.username,
|
||||||
|
results
|
||||||
|
});
|
||||||
|
|
||||||
|
const score = correct;
|
||||||
|
const completed = true;
|
||||||
|
|
||||||
|
fetch('/saveResults', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: user.username,
|
||||||
|
result: {
|
||||||
|
score,
|
||||||
|
completed,
|
||||||
|
answers: results.map(({ word, attempt }) => ({
|
||||||
|
word,
|
||||||
|
correct: word.toLowerCase() === attempt.toLowerCase()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔁 Retry only the missed words
|
||||||
|
function retryMissed(missedWords) {
|
||||||
|
words = missedWords;
|
||||||
|
current = 0;
|
||||||
|
results = [];
|
||||||
|
|
||||||
|
document.getElementById('summary')?.classList.add('hidden');
|
||||||
|
showWord();
|
||||||
|
}
|
||||||
|
|
||||||
|
// added remove later
|
||||||
|
console.log("showStudent fired");
|
||||||
|
// added remove later
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
// 🌐 Load users.json for login validation
|
||||||
|
// await loadUsers();
|
||||||
|
displayUserDropdown();
|
||||||
|
populateWordUserDropdown();
|
||||||
|
|
||||||
|
// 🎨 Restore saved theme
|
||||||
|
applySavedTheme();
|
||||||
|
|
||||||
|
// 🔁 Restore session if user was logged in
|
||||||
|
const savedUser = localStorage.getItem("loggedInUser");
|
||||||
|
if (savedUser) {
|
||||||
|
setupSession(JSON.parse(savedUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧭 Tab switching for Admin Panel
|
||||||
|
const defaultTab = document.querySelector('#adminTabs button.active');
|
||||||
|
if (defaultTab) {
|
||||||
|
const targetId = defaultTab.getAttribute("onclick")?.match(/'(.*?)'/)?.[1];
|
||||||
|
if (targetId) switchTab(targetId);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// 🎨 Apply saved theme on load
|
||||||
|
function applySavedTheme() {
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'playground';
|
||||||
|
document.body.className = savedTheme;
|
||||||
|
|
||||||
|
const themeSelector = document.getElementById('themeSelect');
|
||||||
|
if (themeSelector) themeSelector.value = savedTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎨 Change theme dynamically
|
||||||
|
function changeTheme(themeName) {
|
||||||
|
document.body.className = themeName;
|
||||||
|
localStorage.setItem('theme', themeName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// 🎭 Role-Based UI Setup
|
||||||
|
function setupSession(user) {
|
||||||
|
document.getElementById('loginPanel').classList.add('hidden');
|
||||||
|
document.getElementById('adminPanel').classList.add('hidden');
|
||||||
|
document.getElementById('studentPanel').classList.add('hidden');
|
||||||
|
document.getElementById('nav').classList.remove('hidden');
|
||||||
|
|
||||||
|
document.getElementById('userInfo').textContent = `${user.username} (${user.role})`;
|
||||||
|
|
||||||
|
// 🎨 Banner Setup
|
||||||
|
const roleBanner = document.getElementById('roleBanner');
|
||||||
|
roleBanner.classList.remove('hidden', 'admin', 'student');
|
||||||
|
roleBanner.classList.add(user.role);
|
||||||
|
roleBanner.textContent = user.role === "admin"
|
||||||
|
? "👨🏫 Admin Access Enabled"
|
||||||
|
: "🎓 Student Mode Active";
|
||||||
|
|
||||||
|
// 🎚 Role-Based Navigation
|
||||||
|
const studentBtn = document.getElementById('studentBtn');
|
||||||
|
const adminBtn = document.getElementById('adminBtn');
|
||||||
|
|
||||||
|
if (user.role === "admin") {
|
||||||
|
studentBtn.classList.remove('hidden');
|
||||||
|
adminBtn.classList.remove('hidden');
|
||||||
|
showAdmin();
|
||||||
|
} else if (user.role === "student") {
|
||||||
|
studentBtn.classList.add('hidden');
|
||||||
|
adminBtn.classList.add('hidden');
|
||||||
|
showStudent();
|
||||||
|
} else {
|
||||||
|
alert("Unknown role. Logging out for safety.");
|
||||||
|
logoutUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(`Welcome, ${user.role}!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔔 Toast Notification
|
||||||
|
function showToast(message, duration = 3000) {
|
||||||
|
const toast = document.getElementById("toast");
|
||||||
|
if (!toast) return;
|
||||||
|
|
||||||
|
toast.textContent = message;
|
||||||
|
toast.classList.add("show");
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove("show");
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
function showAdmin() {
|
||||||
|
console.log("showAdmin fired");
|
||||||
|
|
||||||
|
const adminPanel = document.getElementById("adminPanel");
|
||||||
|
const studentPanel = document.getElementById("studentPanel");
|
||||||
|
|
||||||
|
loadUserDropdowns();
|
||||||
|
|
||||||
|
adminPanel?.classList.remove("hidden");
|
||||||
|
studentPanel?.classList.add("hidden");
|
||||||
|
|
||||||
|
switchTab("tabUsers"); // optional default tab
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"student1": {
|
|
||||||
"score": 8,
|
|
||||||
"completed": true,
|
|
||||||
"answers": [
|
|
||||||
{ "word": "apple", "correct": true },
|
|
||||||
{ "word": "banana", "correct": false },
|
|
||||||
{ "word": "grape", "correct": true }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,494 +0,0 @@
|
|||||||
let users = []
|
|
||||||
|
|
||||||
// 🔁 Load users from server-side users.json
|
|
||||||
async function loadUsers() {
|
|
||||||
try {
|
|
||||||
const res = await fetch("users.json")
|
|
||||||
users = await res.json()
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to load users.json:", e)
|
|
||||||
users = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔐 Password hashing
|
|
||||||
function hashPassword(password) {
|
|
||||||
const shaObj = new jsSHA("SHA-256", "TEXT")
|
|
||||||
shaObj.update(password)
|
|
||||||
return shaObj.getHash("HEX")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 👤 Login
|
|
||||||
async function verifyLogin() {
|
|
||||||
const uname = document.getElementById('username').value.trim()
|
|
||||||
const pw = document.getElementById('password').value
|
|
||||||
const hash = hashPassword(pw)
|
|
||||||
const msg = document.getElementById('loginMessage')
|
|
||||||
|
|
||||||
const user = users.find(u => u.username === uname && u.hash === hash)
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
localStorage.setItem('loggedInUser', JSON.stringify(user))
|
|
||||||
msg.textContent = ''
|
|
||||||
setupSession(user)
|
|
||||||
await showAdmin()
|
|
||||||
} else {
|
|
||||||
msg.textContent = "Invalid username or password."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🧠 On load
|
|
||||||
window.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
await loadUsers()
|
|
||||||
displayUserDropdown()
|
|
||||||
|
|
||||||
const savedTheme = localStorage.getItem('theme') || 'playground'
|
|
||||||
document.body.className = savedTheme
|
|
||||||
const dropdown = document.getElementById('themeSelect')
|
|
||||||
if (dropdown) dropdown.value = savedTheme
|
|
||||||
|
|
||||||
const savedUser = localStorage.getItem('loggedInUser')
|
|
||||||
if (savedUser) setupSession(JSON.parse(savedUser))
|
|
||||||
})
|
|
||||||
|
|
||||||
// 🧑🏫 Admin setup
|
|
||||||
async function setupSession(user) {
|
|
||||||
// 🔒 Hide everything first for clean slate
|
|
||||||
document.getElementById('loginPanel').classList.add('hidden')
|
|
||||||
document.getElementById('adminPanel').classList.add('hidden')
|
|
||||||
document.getElementById('studentPanel').classList.add('hidden')
|
|
||||||
|
|
||||||
// 🔓 Show navigation and user info
|
|
||||||
document.getElementById('nav').classList.remove('hidden')
|
|
||||||
document.getElementById('userInfo').textContent = `${user.username} (${user.role})`
|
|
||||||
|
|
||||||
// 🎨 Role-colored banner setup
|
|
||||||
const roleBanner = document.getElementById('roleBanner')
|
|
||||||
roleBanner.classList.remove('hidden', 'admin', 'student')
|
|
||||||
roleBanner.classList.add(user.role)
|
|
||||||
roleBanner.textContent = user.role === "admin"
|
|
||||||
? "👨🏫 Admin Access Enabled"
|
|
||||||
: "🎓 Student Mode Active"
|
|
||||||
|
|
||||||
// 🙈 Hide/show Student Panel button appropriately
|
|
||||||
const studentBtn = document.getElementById('studentBtn')
|
|
||||||
if (user.role === "admin") {
|
|
||||||
studentBtn.classList.remove('hidden')
|
|
||||||
document.getElementById('adminBtn').classList.remove('hidden')
|
|
||||||
console.log("🛠 Logged in as Admin")
|
|
||||||
await showAdmin()
|
|
||||||
} else if (user.role === "student") {
|
|
||||||
studentBtn.classList.add('hidden')
|
|
||||||
document.getElementById('adminBtn').classList.add('hidden')
|
|
||||||
console.log("🎓 Logged in as Student")
|
|
||||||
showStudent()
|
|
||||||
} else {
|
|
||||||
console.warn("❓ Unknown role:", user.role)
|
|
||||||
logoutUser()
|
|
||||||
alert("Unknown role detected. Logging out for security.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 📣 Toast message
|
|
||||||
showToast(`Welcome, ${user.role}!`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function logoutUser() {
|
|
||||||
// Clear stored user session
|
|
||||||
localStorage.removeItem('loggedInUser')
|
|
||||||
|
|
||||||
// 🔄 Reset all visible panels
|
|
||||||
document.getElementById('adminPanel').classList.add('hidden')
|
|
||||||
document.getElementById('studentPanel').classList.add('hidden')
|
|
||||||
document.getElementById('nav').classList.add('hidden')
|
|
||||||
document.getElementById('loginPanel').classList.remove('hidden')
|
|
||||||
document.getElementById('username').value = ''
|
|
||||||
document.getElementById('password').value = ''
|
|
||||||
|
|
||||||
// 🧼 Reset role banner styling and content
|
|
||||||
const roleBanner = document.getElementById('roleBanner')
|
|
||||||
roleBanner.classList.add('hidden')
|
|
||||||
roleBanner.classList.remove('admin', 'student')
|
|
||||||
roleBanner.textContent = ''
|
|
||||||
|
|
||||||
// Optional: reset login message (if present)
|
|
||||||
const loginMessage = document.getElementById('loginMessage')
|
|
||||||
if (loginMessage) loginMessage.textContent = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
async function showAdmin() {
|
|
||||||
const user = JSON.parse(localStorage.getItem('loggedInUser'))
|
|
||||||
if (!user || user.role !== "admin") {
|
|
||||||
console.warn("⛔ Access denied: not an admin")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('adminPanel').classList.remove('hidden')
|
|
||||||
document.getElementById('studentPanel').classList.add('hidden')
|
|
||||||
|
|
||||||
try {
|
|
||||||
await loadWords()
|
|
||||||
displayWordList()
|
|
||||||
} catch (err) {
|
|
||||||
console.error("🚨 Failed to load words:", err)
|
|
||||||
// Optional: fallback or placeholder UI
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadUsers()
|
|
||||||
displayUserDropdown()
|
|
||||||
populateWordUserDropdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ➕ Add User
|
|
||||||
async function addNewUser() {
|
|
||||||
const username = document.getElementById("newUsername").value.trim()
|
|
||||||
const password = document.getElementById("newPassword").value
|
|
||||||
const role = document.getElementById("newRole").value
|
|
||||||
const msg = document.getElementById("userAddMessage")
|
|
||||||
const hashPreview = document.getElementById("hashPreview")
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
msg.textContent = "Please fill out all fields."
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const hash = hashPassword(password)
|
|
||||||
|
|
||||||
// Live hash preview (optional visual confirmation for admin)
|
|
||||||
if (hashPreview) {
|
|
||||||
hashPreview.textContent = hash
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await saveUserToServer(username, hash, role)
|
|
||||||
msg.textContent = `✅ User "${username}" added to server.`
|
|
||||||
|
|
||||||
// Clear fields after success
|
|
||||||
document.getElementById("newUsername").value = ''
|
|
||||||
document.getElementById("newPassword").value = ''
|
|
||||||
if (hashPreview) hashPreview.textContent = '[auto]'
|
|
||||||
|
|
||||||
await loadUsers()
|
|
||||||
displayUserDropdown()
|
|
||||||
} catch (e) {
|
|
||||||
msg.textContent = "❌ Failed to add user."
|
|
||||||
console.error("Add user error:", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveUserToServer(username, hash, role) {
|
|
||||||
const res = await fetch('/addUser', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username, hash, role })
|
|
||||||
})
|
|
||||||
|
|
||||||
const msg = await res.text()
|
|
||||||
alert(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🗑 User deletion
|
|
||||||
function displayUserDropdown() {
|
|
||||||
const dropdown = document.getElementById("userDropdown")
|
|
||||||
dropdown.innerHTML = `<option value="">-- Select a user --</option>`
|
|
||||||
|
|
||||||
users.forEach(user => {
|
|
||||||
dropdown.innerHTML += `<option value="${user.username}">${user.username} (${user.role})</option>`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectUserAction(username) {
|
|
||||||
if (!username) return
|
|
||||||
|
|
||||||
const confirmDelete = confirm(`Are you sure you want to delete user "${username}"?`)
|
|
||||||
if (!confirmDelete) {
|
|
||||||
document.getElementById("userDropdown").value = ""
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch("/deleteUser", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ username })
|
|
||||||
})
|
|
||||||
.then(res => res.text())
|
|
||||||
.then(msg => {
|
|
||||||
alert(msg)
|
|
||||||
loadUsers().then(displayUserDropdown)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🎨 Theme
|
|
||||||
function changeTheme(themeName) {
|
|
||||||
document.body.className = themeName
|
|
||||||
localStorage.setItem('theme', themeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 📚 Word list
|
|
||||||
|
|
||||||
|
|
||||||
function saveWords() {
|
|
||||||
const text = document.getElementById('wordInput').value.trim()
|
|
||||||
allWords = text.split('\n').map(w => w.trim()).filter(Boolean)
|
|
||||||
localStorage.setItem('spellingWords', JSON.stringify(allWords))
|
|
||||||
displayWordList()
|
|
||||||
alert("Word list saved!")
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearWords() {
|
|
||||||
localStorage.removeItem('spellingWords')
|
|
||||||
allWords = []
|
|
||||||
displayWordList()
|
|
||||||
alert("Saved words cleared.")
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayWordList() {
|
|
||||||
const ul = document.getElementById('wordListDisplay')
|
|
||||||
ul.innerHTML = allWords.map(word => `<li>${word}</li>`).join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFileUpload(event) {
|
|
||||||
const file = event.target.files[0]
|
|
||||||
if (!file) return
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = function(e) {
|
|
||||||
const text = e.target.result
|
|
||||||
const newWords = text.includes(",") ? text.split(",") : text.split("\n")
|
|
||||||
allWords = newWords.map(w => w.trim()).filter(Boolean)
|
|
||||||
localStorage.setItem('spellingWords', JSON.stringify(allWords))
|
|
||||||
displayWordList()
|
|
||||||
alert("Words uploaded and saved!")
|
|
||||||
}
|
|
||||||
reader.readAsText(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🎮 Spelling Game
|
|
||||||
let allWords = [], words = [], current = 0, results = []
|
|
||||||
|
|
||||||
function showStudent() {
|
|
||||||
document.getElementById('studentPanel').classList.remove('hidden')
|
|
||||||
document.getElementById('adminPanel').classList.add('hidden')
|
|
||||||
}
|
|
||||||
|
|
||||||
function startGame() {
|
|
||||||
loadWords()
|
|
||||||
if (allWords.length === 0) {
|
|
||||||
alert("No words found. Please add words in Admin Panel.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
words = [...allWords]
|
|
||||||
current = 0
|
|
||||||
results = []
|
|
||||||
document.getElementById('summary').classList.add('hidden')
|
|
||||||
showWord()
|
|
||||||
}
|
|
||||||
|
|
||||||
function showWord() {
|
|
||||||
const wordBox = document.getElementById('wordBox')
|
|
||||||
const wordEl = document.getElementById('word')
|
|
||||||
wordBox.classList.remove('hidden')
|
|
||||||
wordEl.textContent = words[current]
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
wordBox.classList.add('hidden')
|
|
||||||
document.getElementById('inputSection').classList.remove('hidden')
|
|
||||||
document.getElementById('userInput').value = ''
|
|
||||||
document.getElementById('userInput').focus()
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitAnswer() {
|
|
||||||
const input = document.getElementById('userInput').value.trim()
|
|
||||||
results.push({ word: words[current], attempt: input })
|
|
||||||
current++
|
|
||||||
document.getElementById('inputSection').classList.add('hidden')
|
|
||||||
|
|
||||||
if (current < words.length) {
|
|
||||||
showWord()
|
|
||||||
} else {
|
|
||||||
showSummary()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSummary() {
|
|
||||||
const summary = document.getElementById('summary')
|
|
||||||
let correct = 0
|
|
||||||
let missed = []
|
|
||||||
|
|
||||||
const report = results.map(({ word, attempt }) => {
|
|
||||||
const isCorrect = word.toLowerCase() === attempt.toLowerCase()
|
|
||||||
if (isCorrect) correct++
|
|
||||||
else missed.push(word)
|
|
||||||
return `<li>${word} — <strong style="color:${isCorrect ? 'green' : 'red'}">${attempt || '(no answer)'}</strong></li>`
|
|
||||||
}).join('')
|
|
||||||
|
|
||||||
summary.innerHTML = `
|
|
||||||
<h2>Results</h2>
|
|
||||||
<p>You got ${correct} out of ${words.length} correct (${Math.round((correct / words.length) * 100)}%)</p>
|
|
||||||
<ul>${report}</ul>
|
|
||||||
${missed.length ? `<button onclick="retryMissed(${JSON.stringify(missed)})">Retry Missed Words</button>` : ''}
|
|
||||||
`
|
|
||||||
summary.classList.remove('hidden')
|
|
||||||
}
|
|
||||||
|
|
||||||
function retryMissed(missedWords) {
|
|
||||||
words = missedWords
|
|
||||||
current = 0
|
|
||||||
results = []
|
|
||||||
document.getElementById('summary').classList.add('hidden')
|
|
||||||
showWord()
|
|
||||||
}
|
|
||||||
async function loadWords() {
|
|
||||||
const user = JSON.parse(localStorage.getItem('loggedInUser'))
|
|
||||||
if (!user) return allWords = []
|
|
||||||
|
|
||||||
const res = await fetch(`/getWordList?user=${encodeURIComponent(user.username)}`)
|
|
||||||
allWords = await res.json()
|
|
||||||
displayWordList()
|
|
||||||
}
|
|
||||||
async function saveWords() {
|
|
||||||
const user = JSON.parse(localStorage.getItem('loggedInUser'))
|
|
||||||
if (!user) return
|
|
||||||
|
|
||||||
const text = document.getElementById('wordInput').value.trim()
|
|
||||||
allWords = text.split('\n').map(w => w.trim()).filter(Boolean)
|
|
||||||
|
|
||||||
await fetch('/saveWordList', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username: user.username, words: allWords })
|
|
||||||
})
|
|
||||||
|
|
||||||
displayWordList()
|
|
||||||
alert("✅ Word list saved to server")
|
|
||||||
}
|
|
||||||
async function clearWords() {
|
|
||||||
const user = JSON.parse(localStorage.getItem('loggedInUser'))
|
|
||||||
if (!user) return
|
|
||||||
|
|
||||||
allWords = []
|
|
||||||
await fetch('/saveWordList', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ username: user.username, words: [] })
|
|
||||||
})
|
|
||||||
|
|
||||||
displayWordList()
|
|
||||||
alert("🗑 Word list cleared")
|
|
||||||
}
|
|
||||||
function populateWordUserDropdown() {
|
|
||||||
const userSelects = document.querySelectorAll(".word-user-dropdown")
|
|
||||||
|
|
||||||
if (!users || !users.length) {
|
|
||||||
console.warn("⚠️ No users to populate dropdowns.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userSelects.forEach(select => {
|
|
||||||
select.innerHTML = '<option value="">-- Select a user --</option>'
|
|
||||||
users.forEach(user => {
|
|
||||||
const option = document.createElement("option")
|
|
||||||
option.value = user.username
|
|
||||||
option.textContent = user.username
|
|
||||||
select.appendChild(option)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
async function loadWordsForSelectedUser() {
|
|
||||||
const selectedUser = document.getElementById("wordUserSelect").value
|
|
||||||
if (!selectedUser) return
|
|
||||||
|
|
||||||
const res = await fetch(`/getWordList?user=${encodeURIComponent(selectedUser)}`)
|
|
||||||
allWords = await res.json()
|
|
||||||
displayWordList()
|
|
||||||
}
|
|
||||||
function showToast(message, duration = 3000) {
|
|
||||||
const toast = document.getElementById("toast")
|
|
||||||
toast.textContent = message
|
|
||||||
toast.classList.add("show")
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.remove("show")
|
|
||||||
}, duration)
|
|
||||||
}
|
|
||||||
async function displayWordListForSelectedUser() {
|
|
||||||
const select = document.getElementById("viewUserSelect")
|
|
||||||
const username = select.value
|
|
||||||
const list = document.getElementById("userWordListDisplay")
|
|
||||||
list.innerHTML = ''
|
|
||||||
|
|
||||||
if (!username) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/getWordList?user=${username}`)
|
|
||||||
const words = await res.json()
|
|
||||||
|
|
||||||
if (!words.length) {
|
|
||||||
list.innerHTML = "<li><em>No words found for this user.</em></li>"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
words.forEach(word => {
|
|
||||||
const li = document.createElement("li")
|
|
||||||
li.textContent = word
|
|
||||||
list.appendChild(li)
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
list.innerHTML = `<li style="color:red">❌ Error loading words: ${err.message}</li>`
|
|
||||||
console.error("Failed to load word list for user:", username, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function loadStudentResults() {
|
|
||||||
const list = document.getElementById("resultsList")
|
|
||||||
list.innerHTML = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch('/getResults')
|
|
||||||
const results = await res.json()
|
|
||||||
|
|
||||||
if (!Object.keys(results).length) {
|
|
||||||
list.innerHTML = '<li><em>No results available yet.</em></li>'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [username, result] of Object.entries(results)) {
|
|
||||||
const li = document.createElement("li")
|
|
||||||
|
|
||||||
// Build detailed answer breakdown
|
|
||||||
let answersHTML = ''
|
|
||||||
result.answers.forEach(entry => {
|
|
||||||
const status = entry.correct ? '✅' : '❌'
|
|
||||||
answersHTML += `${status} ${entry.word}<br>`
|
|
||||||
})
|
|
||||||
|
|
||||||
li.innerHTML = `
|
|
||||||
<strong>${username}</strong> — Score: ${result.score}, Completed: ${result.completed ? '✅' : '❌'}<br>
|
|
||||||
<em>Answers:</em><br>${answersHTML}
|
|
||||||
`
|
|
||||||
list.appendChild(li)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("🔴 Failed to load results:", err)
|
|
||||||
list.innerHTML = '<li style="color:red">Error loading results.</li>'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function switchTab(targetId) {
|
|
||||||
// Hide all tabs
|
|
||||||
document.querySelectorAll('.adminTab').forEach(tab => tab.classList.add('hidden'))
|
|
||||||
|
|
||||||
// Remove active class from buttons
|
|
||||||
document.querySelectorAll('#adminTabs button').forEach(btn => btn.classList.remove('active'))
|
|
||||||
|
|
||||||
// Show the selected tab
|
|
||||||
document.getElementById(targetId).classList.remove('hidden')
|
|
||||||
|
|
||||||
// Highlight active tab button
|
|
||||||
const buttons = document.querySelectorAll('#adminTabs button')
|
|
||||||
buttons.forEach(btn => {
|
|
||||||
if (btn.getAttribute('onclick')?.includes(targetId)) {
|
|
||||||
btn.classList.add('active')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+63
-69
@@ -1,3 +1,4 @@
|
|||||||
|
/* 📋 Global Defaults */
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -5,45 +6,12 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 🎨 Theme Styles */
|
ul {
|
||||||
body.playground {
|
list-style: none;
|
||||||
background: #fef9e7;
|
padding: 0;
|
||||||
color: #333;
|
|
||||||
font-family: 'Comic Sans MS', cursive, sans-serif;
|
|
||||||
}
|
}
|
||||||
|
li {
|
||||||
body.chalkboard {
|
margin: 0.25rem 0;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🔘 UI Controls */
|
|
||||||
button {
|
|
||||||
background-color: #4caf50;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.6rem 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin: 0.4rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input, textarea, select {
|
input, textarea, select {
|
||||||
@@ -54,7 +22,57 @@ input, textarea, select {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 🔳 Panels */
|
/* 🎨 Theme Styles */
|
||||||
|
body.playground {
|
||||||
|
background: #fef9e7;
|
||||||
|
color: #333;
|
||||||
|
font-family: 'Comic Sans MS', cursive, sans-serif;
|
||||||
|
}
|
||||||
|
body.chalkboard {
|
||||||
|
background: #222;
|
||||||
|
color: #f0f0f0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
body.galaxy {
|
||||||
|
background: radial-gradient(circle at center, #0d0d2b, #1a1a3f);
|
||||||
|
color: #b3e5fc;
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
}
|
||||||
|
body.nature {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
font-family: 'Georgia', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🔘 Buttons */
|
||||||
|
button {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
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 */
|
||||||
|
body.chalkboard button {
|
||||||
|
background-color: #555;
|
||||||
|
}
|
||||||
|
body.galaxy button {
|
||||||
|
background-color: #6200ea;
|
||||||
|
}
|
||||||
|
body.nature button {
|
||||||
|
background-color: #81c784;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🔳 Panels and Banners */
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -68,43 +86,20 @@ input, textarea, select {
|
|||||||
color: #2196f3;
|
color: #2196f3;
|
||||||
text-shadow: 1px 1px #ccc;
|
text-shadow: 1px 1px #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
margin: 0.25rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🎭 Theme-specific button colors */
|
|
||||||
body.chalkboard button {
|
|
||||||
background-color: #555;
|
|
||||||
}
|
|
||||||
body.galaxy button {
|
|
||||||
background-color: #6200ea;
|
|
||||||
}
|
|
||||||
body.nature button {
|
|
||||||
background-color: #81c784;
|
|
||||||
}
|
|
||||||
.banner {
|
.banner {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner.admin {
|
.banner.admin {
|
||||||
background-color: #b22222; /* Firebrick red */
|
background-color: #b22222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner.student {
|
.banner.student {
|
||||||
background-color: #1e90ff; /* Dodger blue */
|
background-color: #1e90ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
/* 🔔 Toast Messages */
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
@@ -118,11 +113,12 @@ body.nature button {
|
|||||||
transition: opacity 0.4s ease;
|
transition: opacity 0.4s ease;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast.show {
|
.toast.show {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 🧭 Admin Tabs */
|
||||||
#adminTabs {
|
#adminTabs {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
@@ -138,12 +134,10 @@ body.nature button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s, color 0.2s;
|
transition: background-color 0.2s, color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#adminTabs button:hover {
|
#adminTabs button:hover {
|
||||||
background-color: #e0f2ff;
|
background-color: #e0f2ff;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
#adminTabs button.active {
|
#adminTabs button.active {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -155,4 +149,4 @@ body.nature button {
|
|||||||
}
|
}
|
||||||
.adminTab:not(.hidden) {
|
.adminTab:not(.hidden) {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"student1": ["apple", "banana", "cherry"]
|
|
||||||
}
|
|
||||||
@@ -1,57 +1,166 @@
|
|||||||
const express = require('express')
|
const express = require('express');
|
||||||
const fs = require('fs')
|
const fs = require('fs');
|
||||||
const app = express()
|
const path = require('path');
|
||||||
const PORT = 3000
|
const app = express();
|
||||||
|
const PORT = 3000;
|
||||||
|
|
||||||
app.use(express.static('public'))
|
// 📁 Static assets
|
||||||
app.use(express.json())
|
app.use(express.static('public'));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
app.post('/addUser', (req, res) => {
|
// 🗂 Ensure data directory and files exist
|
||||||
const { username, hash, role } = req.body
|
const DATA_DIR = path.join(__dirname, 'data');
|
||||||
const users = JSON.parse(fs.readFileSync('public/users.json', 'utf-8'))
|
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR);
|
||||||
|
|
||||||
if (users.find(u => u.username === username)) {
|
const files = {
|
||||||
return res.status(409).send('User already exists.')
|
users: 'users.json',
|
||||||
|
wordlists: 'wordlists.json',
|
||||||
|
results: 'results.json'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const file of Object.values(files)) {
|
||||||
|
const target = path.join(DATA_DIR, file);
|
||||||
|
if (!fs.existsSync(target)) {
|
||||||
|
const defaultData = file === 'users.json' ? '[]' : '{}';
|
||||||
|
fs.writeFileSync(target, defaultData);
|
||||||
|
console.log(`📄 Initialized ${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🛡 Safe JSON reader
|
||||||
|
function readJsonSafe(filePath, fallback = {}) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔐 Verify user credentials
|
||||||
|
app.post('/verifyUser', (req, res) => {
|
||||||
|
const { username, hash } = req.body;
|
||||||
|
if (typeof username !== 'string' || typeof hash !== 'string') {
|
||||||
|
return res.status(400).send("Invalid credentials format");
|
||||||
}
|
}
|
||||||
|
|
||||||
users.push({ username, hash, role })
|
const users = readJsonSafe(path.join(DATA_DIR, files.users), []);
|
||||||
fs.writeFileSync('public/users.json', JSON.stringify(users, null, 2))
|
const user = users.find(u => u.username === username && u.hash === hash);
|
||||||
res.send('User added successfully.')
|
|
||||||
})
|
if (user) {
|
||||||
|
res.json(user);
|
||||||
|
} else {
|
||||||
|
res.status(401).send("Invalid login");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ➕ Add new user
|
||||||
|
app.post('/addUser', (req, res) => {
|
||||||
|
const { username, hash, role } = req.body;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof username !== 'string' ||
|
||||||
|
typeof hash !== 'string' ||
|
||||||
|
typeof role !== 'string' ||
|
||||||
|
!['admin', 'student'].includes(role.trim().toLowerCase())
|
||||||
|
) {
|
||||||
|
return res.status(400).send("Invalid user data");
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = readJsonSafe(path.join(DATA_DIR, files.users), []);
|
||||||
|
if (users.find(u => u.username === username)) {
|
||||||
|
return res.status(409).send("User already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
users.push({ username: username.trim(), hash, role: role.trim() });
|
||||||
|
fs.writeFileSync(path.join(DATA_DIR, files.users), JSON.stringify(users, null, 2));
|
||||||
|
res.send(`✅ User "${username}" added`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ Delete user
|
||||||
app.post('/deleteUser', (req, res) => {
|
app.post('/deleteUser', (req, res) => {
|
||||||
const { username } = req.body
|
const { username } = req.body;
|
||||||
const users = JSON.parse(fs.readFileSync('public/users.json', 'utf-8'))
|
if (typeof username !== 'string') return res.status(400).send("Invalid username");
|
||||||
const updated = users.filter(u => u.username !== username)
|
|
||||||
|
const usersPath = path.join(DATA_DIR, files.users);
|
||||||
|
const users = readJsonSafe(usersPath, []);
|
||||||
|
const updated = users.filter(u => u.username !== username);
|
||||||
|
|
||||||
if (users.length === updated.length) {
|
if (users.length === updated.length) {
|
||||||
return res.status(404).send("User not found.")
|
return res.status(404).send("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync('public/users.json', JSON.stringify(updated, null, 2))
|
fs.writeFileSync(usersPath, JSON.stringify(updated, null, 2));
|
||||||
res.send(`User "${username}" deleted.`)
|
res.send(`✅ User "${username}" deleted`);
|
||||||
})
|
});
|
||||||
|
|
||||||
|
// 📚 Get word list
|
||||||
app.get('/getWordList', (req, res) => {
|
app.get('/getWordList', (req, res) => {
|
||||||
const username = req.query.user
|
const username = req.query.user;
|
||||||
if (!username) return res.status(400).send("Username required")
|
if (!username || typeof username !== 'string') {
|
||||||
|
return res.status(400).send("Username required");
|
||||||
const data = JSON.parse(fs.readFileSync('public/wordlists.json', 'utf-8'))
|
|
||||||
res.json(data[username] || [])
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post('/saveWordList', (req, res) => {
|
|
||||||
const { username, words } = req.body
|
|
||||||
if (!username || !Array.isArray(words)) {
|
|
||||||
return res.status(400).send("Username and words array required")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = JSON.parse(fs.readFileSync('public/wordlists.json', 'utf-8'))
|
const wordlists = readJsonSafe(path.join(DATA_DIR, files.wordlists));
|
||||||
data[username] = words
|
res.json(wordlists[username] || []);
|
||||||
fs.writeFileSync('public/wordlists.json', JSON.stringify(data, null, 2))
|
});
|
||||||
res.send(`✅ Word list saved for ${username}`)
|
|
||||||
})
|
|
||||||
app.get('/getResults', (req, res) => {
|
|
||||||
const results = JSON.parse(fs.readFileSync('public/results.json', 'utf-8'))
|
|
||||||
res.json(results)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`))
|
// 💾 Save word list
|
||||||
|
app.post('/saveWordList', (req, res) => {
|
||||||
|
const { username, words } = req.body;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof username !== 'string' ||
|
||||||
|
!Array.isArray(words) ||
|
||||||
|
words.length > 100 ||
|
||||||
|
words.some(w => typeof w !== 'string')
|
||||||
|
) {
|
||||||
|
return res.status(400).send("Invalid word list");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userList = readJsonSafe(path.join(DATA_DIR, files.users), []);
|
||||||
|
if (!userList.find(u => u.username === username)) {
|
||||||
|
return res.status(404).send("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const wordlistsPath = path.join(DATA_DIR, files.wordlists);
|
||||||
|
const wordlists = readJsonSafe(wordlistsPath);
|
||||||
|
wordlists[username] = words;
|
||||||
|
fs.writeFileSync(wordlistsPath, JSON.stringify(wordlists, null, 2));
|
||||||
|
res.send(`✅ Word list saved for ${username}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 📊 Get results
|
||||||
|
app.get('/getResults', (req, res) => {
|
||||||
|
const results = readJsonSafe(path.join(DATA_DIR, files.results));
|
||||||
|
res.json(results);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🚀 Launch server
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`✅ Server listening at http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
app.post('/saveResults', (req, res) => {
|
||||||
|
const { username, result } = req.body;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof username !== 'string' ||
|
||||||
|
typeof result !== 'object' ||
|
||||||
|
typeof result.score !== 'number' ||
|
||||||
|
typeof result.completed !== 'boolean' ||
|
||||||
|
!Array.isArray(result.answers)
|
||||||
|
) {
|
||||||
|
return res.status(400).send("Invalid result format");
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultsPath = path.join(DATA_DIR, files.results);
|
||||||
|
const allResults = readJsonSafe(resultsPath);
|
||||||
|
|
||||||
|
allResults[username] = result;
|
||||||
|
|
||||||
|
fs.writeFileSync(resultsPath, JSON.stringify(allResults, null, 2));
|
||||||
|
res.send(`✅ Results saved for ${username}`);
|
||||||
|
});
|
||||||
|
app.get('/getUsers', (req, res) => {
|
||||||
|
const users = readJsonSafe(path.join(DATA_DIR, files.users), []);
|
||||||
|
res.json(users);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user