From 77d5f9b19686a53ea77ffcca1877a3005c2c96ab Mon Sep 17 00:00:00 2001 From: "nate@necloud.us" Date: Thu, 3 Jul 2025 09:42:40 -0400 Subject: [PATCH] module change --- data/results.json | 38 +++ {public => data}/users.json | 10 +- data/wordlists.json | 13 + public/index.html | 160 ++++++------ public/js/admin.js | 228 +++++++++++++++++ public/js/auth.js | 58 +++++ public/js/game.js | 145 +++++++++++ public/js/main.js | 22 ++ public/js/themes.js | 14 + public/js/ui.js | 61 +++++ public/results.json | 11 - public/script.js | 494 ------------------------------------ public/styles.css | 132 +++++----- public/wordlists.json | 3 - server.js | 195 ++++++++++---- 15 files changed, 886 insertions(+), 698 deletions(-) create mode 100644 data/results.json rename {public => data}/users.json (80%) create mode 100644 data/wordlists.json create mode 100644 public/js/admin.js create mode 100644 public/js/auth.js create mode 100644 public/js/game.js create mode 100644 public/js/main.js create mode 100644 public/js/themes.js create mode 100644 public/js/ui.js delete mode 100644 public/results.json delete mode 100644 public/script.js delete mode 100644 public/wordlists.json diff --git a/data/results.json b/data/results.json new file mode 100644 index 0000000..39d572a --- /dev/null +++ b/data/results.json @@ -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 + } + ] + } +} \ No newline at end of file diff --git a/public/users.json b/data/users.json similarity index 80% rename from public/users.json rename to data/users.json index 4f2a0a2..ff8b016 100644 --- a/public/users.json +++ b/data/users.json @@ -4,14 +4,14 @@ "hash": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", "role": "admin" }, - { - "username": "student1", - "hash": "8d969eef6ecad3c29a3a629280e686cf", - "role": "student" - }, { "username": "nate", "hash": "5a2a558c78d3717db731600c4f354fa1d9c84b556f108091a891f444f1bdec40", "role": "student" + }, + { + "username": "student1", + "hash": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", + "role": "student" } ] \ No newline at end of file diff --git a/data/wordlists.json b/data/wordlists.json new file mode 100644 index 0000000..0604ecb --- /dev/null +++ b/data/wordlists.json @@ -0,0 +1,13 @@ +{ + "student1": [], + "admin1": [ + "boy", + "bot", + "snot" + ], + "nate": [ + "not", + "bot", + "snot" + ] +} \ No newline at end of file diff --git a/public/index.html b/public/index.html index 6ec8ed8..744bf0c 100644 --- a/public/index.html +++ b/public/index.html @@ -2,15 +2,26 @@ + Spelling App with Roles - - - - - + + + + + + + + + + + +

Spelling Practice

+ + +

User Login

@@ -22,102 +33,105 @@

- + + + + + + - - - - - -
- - + + \ No newline at end of file diff --git a/public/js/admin.js b/public/js/admin.js new file mode 100644 index 0000000..beb2204 --- /dev/null +++ b/public/js/admin.js @@ -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 = ``; + users.forEach(user => { + dropdown.innerHTML += ``; + }); +} + +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 => `
  • ${word}
  • `).join(''); +} + +function populateWordUserDropdown() { + const selects = document.querySelectorAll(".word-user-dropdown"); + selects.forEach(select => { + select.innerHTML = ''; + 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 = "
  • No words found for this user.
  • "; + 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 = `
  • โŒ Error loading words
  • `; + } +} + +// ๐Ÿ“Š 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 = '
  • No results available yet.
  • '; + 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}
    `; + }); + + li.innerHTML = ` + ${username} โ€” Score: ${result.score}, Completed: ${result.completed ? 'โœ…' : 'โŒ'}
    + Answers:
    ${answersHTML} + `; + list.appendChild(li); + } + } catch (err) { + console.error("๐Ÿ”ด Failed to load results:", err); + list.innerHTML = '
  • Error loading results.
  • '; + } +} + +// ๐Ÿงญ 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 = ''; + 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); + } +} \ No newline at end of file diff --git a/public/js/auth.js b/public/js/auth.js new file mode 100644 index 0000000..5cd3d95 --- /dev/null +++ b/public/js/auth.js @@ -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 = ''; +} \ No newline at end of file diff --git a/public/js/game.js b/public/js/game.js new file mode 100644 index 0000000..5f86faa --- /dev/null +++ b/public/js/game.js @@ -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 `
  • ${word} โ€” ${attempt || '(no answer)'}
  • `; + }).join(''); + + summary.innerHTML = ` +

    Results

    +

    You got ${correct} out of ${words.length} correct (${Math.round((correct / words.length) * 100)}%)

    + + ${missed.length ? `` : ''} + `; + + 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 +} \ No newline at end of file diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..1f64cbc --- /dev/null +++ b/public/js/main.js @@ -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); + } +}); \ No newline at end of file diff --git a/public/js/themes.js b/public/js/themes.js new file mode 100644 index 0000000..65254d4 --- /dev/null +++ b/public/js/themes.js @@ -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); +} \ No newline at end of file diff --git a/public/js/ui.js b/public/js/ui.js new file mode 100644 index 0000000..09669e8 --- /dev/null +++ b/public/js/ui.js @@ -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 +} \ No newline at end of file diff --git a/public/results.json b/public/results.json deleted file mode 100644 index c029b8d..0000000 --- a/public/results.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "student1": { - "score": 8, - "completed": true, - "answers": [ - { "word": "apple", "correct": true }, - { "word": "banana", "correct": false }, - { "word": "grape", "correct": true } - ] - } -} \ No newline at end of file diff --git a/public/script.js b/public/script.js deleted file mode 100644 index 1c6faf0..0000000 --- a/public/script.js +++ /dev/null @@ -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 = `` - - users.forEach(user => { - dropdown.innerHTML += `` - }) -} - -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 => `
  • ${word}
  • `).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 `
  • ${word} โ€” ${attempt || '(no answer)'}
  • ` - }).join('') - - summary.innerHTML = ` -

    Results

    -

    You got ${correct} out of ${words.length} correct (${Math.round((correct / words.length) * 100)}%)

    - - ${missed.length ? `` : ''} - ` - 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 = '' - 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 = "
  • No words found for this user.
  • " - return - } - - words.forEach(word => { - const li = document.createElement("li") - li.textContent = word - list.appendChild(li) - }) - } catch (err) { - list.innerHTML = `
  • โŒ Error loading words: ${err.message}
  • ` - 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 = '
  • No results available yet.
  • ' - 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}
    ` - }) - - li.innerHTML = ` - ${username} โ€” Score: ${result.score}, Completed: ${result.completed ? 'โœ…' : 'โŒ'}
    - Answers:
    ${answersHTML} - ` - list.appendChild(li) - } - } catch (err) { - console.error("๐Ÿ”ด Failed to load results:", err) - list.innerHTML = '
  • Error loading results.
  • ' - } -} -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') - } - }) -} \ No newline at end of file diff --git a/public/styles.css b/public/styles.css index 8bebded..f06f32e 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,3 +1,4 @@ +/* ๐Ÿ“‹ Global Defaults */ body { font-family: sans-serif; text-align: center; @@ -5,45 +6,12 @@ body { margin: 0; } -/* ๐ŸŽจ Theme Styles */ -body.playground { - background: #fef9e7; - color: #333; - font-family: 'Comic Sans MS', cursive, sans-serif; +ul { + list-style: none; + padding: 0; } - -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; -} - -/* ๐Ÿ”˜ 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); +li { + margin: 0.25rem 0; } input, textarea, select { @@ -54,7 +22,57 @@ input, textarea, select { 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 { display: none; } @@ -68,43 +86,20 @@ input, textarea, select { color: #2196f3; 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 { padding: 0.5em; text-align: center; font-weight: bold; color: white; } - .banner.admin { - background-color: #b22222; /* Firebrick red */ + background-color: #b22222; } - .banner.student { - background-color: #1e90ff; /* Dodger blue */ + background-color: #1e90ff; } -.hidden { - display: none; -} +/* ๐Ÿ”” Toast Messages */ .toast { position: fixed; bottom: 20px; @@ -118,11 +113,12 @@ body.nature button { transition: opacity 0.4s ease; z-index: 999; } - .toast.show { opacity: 1; pointer-events: auto; } + +/* ๐Ÿงญ Admin Tabs */ #adminTabs { margin: 1em 0; } @@ -138,12 +134,10 @@ body.nature button { cursor: pointer; transition: background-color 0.2s, color 0.2s; } - #adminTabs button:hover { background-color: #e0f2ff; color: #000; } - #adminTabs button.active { background-color: #ffffff; color: #000; @@ -155,4 +149,4 @@ body.nature button { } .adminTab:not(.hidden) { display: block; -} \ No newline at end of file +} diff --git a/public/wordlists.json b/public/wordlists.json deleted file mode 100644 index 75dee33..0000000 --- a/public/wordlists.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "student1": ["apple", "banana", "cherry"] -} diff --git a/server.js b/server.js index d0f9fd2..987aac1 100644 --- a/server.js +++ b/server.js @@ -1,57 +1,166 @@ -const express = require('express') -const fs = require('fs') -const app = express() -const PORT = 3000 +const express = require('express'); +const fs = require('fs'); +const path = require('path'); +const app = express(); +const PORT = 3000; -app.use(express.static('public')) -app.use(express.json()) +// ๐Ÿ“ Static assets +app.use(express.static('public')); +app.use(express.json()); -app.post('/addUser', (req, res) => { - const { username, hash, role } = req.body - const users = JSON.parse(fs.readFileSync('public/users.json', 'utf-8')) +// ๐Ÿ—‚ Ensure data directory and files exist +const DATA_DIR = path.join(__dirname, 'data'); +if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR); - if (users.find(u => u.username === username)) { - return res.status(409).send('User already exists.') +const files = { + 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 }) - fs.writeFileSync('public/users.json', JSON.stringify(users, null, 2)) - res.send('User added successfully.') -}) + const users = readJsonSafe(path.join(DATA_DIR, files.users), []); + const user = users.find(u => u.username === username && u.hash === hash); + + 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) => { - const { username } = req.body - const users = JSON.parse(fs.readFileSync('public/users.json', 'utf-8')) - const updated = users.filter(u => u.username !== username) + const { username } = req.body; + if (typeof username !== 'string') return res.status(400).send("Invalid 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) { - 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)) - res.send(`User "${username}" deleted.`) -}) + fs.writeFileSync(usersPath, JSON.stringify(updated, null, 2)); + res.send(`โœ… User "${username}" deleted`); +}); + +// ๐Ÿ“š Get word list app.get('/getWordList', (req, res) => { - const username = req.query.user - if (!username) 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 username = req.query.user; + if (!username || typeof username !== 'string') { + return res.status(400).send("Username required"); } - const data = JSON.parse(fs.readFileSync('public/wordlists.json', 'utf-8')) - data[username] = words - 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) -}) + const wordlists = readJsonSafe(path.join(DATA_DIR, files.wordlists)); + res.json(wordlists[username] || []); +}); -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); +}); \ No newline at end of file