diff --git a/debug-login.js b/debug-login.js new file mode 100644 index 0000000..0e5d8b7 --- /dev/null +++ b/debug-login.js @@ -0,0 +1,17 @@ +// Debug script to check password hashes +const { createHash } = require('crypto'); + +function hashPassword(password) { + return createHash('sha256').update(password).digest('hex'); +} + +console.log('Testing password hashes:'); +console.log('password -> ', hashPassword('password')); +console.log('123456 -> ', hashPassword('123456')); +console.log('test -> ', hashPassword('test')); + +// Known hashes from users.json: +console.log('\nExpected hashes from users.json:'); +console.log('admin1 (password): 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'); +console.log('student1 (123456): 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92'); +console.log('nate: 5a2a558c78d3717db731600c4f354fa1d9c84b556f108091a891f444f1bdec40'); diff --git a/migrateResults.js b/migrateResults.js deleted file mode 100644 index 62b082b..0000000 --- a/migrateResults.js +++ /dev/null @@ -1,16 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const resultsPath = path.join(__dirname, 'data', 'results.json'); -const existingData = JSON.parse(fs.readFileSync(resultsPath)); - -for (const username in existingData) { - const singleResult = existingData[username]; - existingData[username] = [{ - ...singleResult, - timestamp: new Date().toISOString() - }]; -} - -fs.writeFileSync(resultsPath, JSON.stringify(existingData, null, 2)); -console.log("โœ… Migration complete. Results are now session arrays."); \ No newline at end of file diff --git a/package.json b/package.json index 004722c..fb5199d 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,27 @@ { - "name": "spelling", - "version": "1.0.0", + "name": "modern-spelling-practice", + "version": "2.0.0", + "description": "A modern, multi-user spelling practice application with admin controls, customizable themes, and analytics", "main": "server.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node server.js" + "start": "node server.js", + "dev": "node server.js", + "test": "echo \"No tests specified\" && exit 0" }, "repository": { "type": "git", "url": "https://git.necloud.us/nmemmert/Spelling.git" }, - "keywords": [], - "author": "", + "keywords": [ + "spelling", + "education", + "practice", + "nodejs", + "express", + "learning" + ], + "author": "NateEmmert", "license": "ISC", - "description": "", "dependencies": { "express": "^5.1.0" } diff --git a/public/index.html b/public/index.html index 1bbb8d8..99d5622 100644 --- a/public/index.html +++ b/public/index.html @@ -1,200 +1,304 @@ - - - -Spelling App with Roles - - - - - - - - - - + + + โœจ Modern Spelling Practice + + + + + + + + + + -

Spelling Practice

- - - -
-

User Login

-
- - - -
-

-
- - - - - - - - - - - - -

Spelling Game

Word will appear here...

+ \ No newline at end of file diff --git a/public/index1.html b/public/index1.html deleted file mode 100644 index a483970..0000000 --- a/public/index1.html +++ /dev/null @@ -1,245 +0,0 @@ - - - - - - Spelling App with Roles - - - - - - - - - - - - - - -

Spelling Practice

- - - - - -
-

User Login

-
- - - -
-

-
- - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/js/admin.js b/public/js/admin.js index ab5aa35..a00247b 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -1,3 +1,84 @@ +// Global variables for admin functionality +let allWords = []; +let adminUsers = []; + +// ๐Ÿงญ Admin tab switcher (make it globally accessible) +window.switchTab = function switchTab(targetId) { + console.log('๐Ÿ”„ Switching to tab:', targetId); + + // Hide all tabs + document.querySelectorAll('.adminTab').forEach(tab => { + tab.classList.add('hidden'); + }); + + // Remove active class from all buttons + document.querySelectorAll('#adminTabs button').forEach(btn => { + btn.classList.remove('active'); + }); + + // Show the selected tab + const targetTab = document.getElementById(targetId); + if (targetTab) { + targetTab.classList.remove('hidden'); + console.log('โœ… Showing tab:', targetId); + } else { + console.error('โŒ Tab not found:', targetId); + } + + // Highlight the active button + document.querySelectorAll('#adminTabs button').forEach(btn => { + const onclick = btn.getAttribute('onclick'); + if (onclick && onclick.includes(targetId)) { + btn.classList.add('active'); + } + }); + + // ๐Ÿ” Trigger tab-specific logic + switch(targetId) { + case 'tabSessions': + if (typeof populateSessionUserDropdown === 'function') { + populateSessionUserDropdown(); + } + const sessionList = document.getElementById("userSessionList"); + if (sessionList) sessionList.innerHTML = ""; + break; + case 'tabReports': + if (typeof loadStudentNamesForReport === 'function') { + loadStudentNamesForReport(); + } + break; + case 'tabBadges': + if (typeof loadBadgeViewer === 'function') { + loadBadgeViewer(); + } + break; + case 'tabAnalytics': + if (typeof refreshAnalytics === 'function') { + refreshAnalytics(); + } + break; + case 'tabResults': + if (typeof loadStudentResults === 'function') { + loadStudentResults(); + } + break; + case 'tabWords': + loadUserDropdowns(); + break; + } +}; + +// Load users from server +async function loadUsers() { + try { + const res = await fetch('/getUsers'); + adminUsers = await res.json(); + } catch (err) { + console.error("Failed to load users:", err); + adminUsers = []; + } +} + // โž• Add a new user async function addNewUser() { const username = document.getElementById("newUsername").value.trim(); @@ -26,8 +107,9 @@ async function addNewUser() { document.getElementById("newPassword").value = ''; if (hashPreview) hashPreview.textContent = '[auto]'; - await loadUsers(); - displayUserDropdown(); + await loadUsers(); + displayUserDropdown(); + populateWordUserDropdown(); } catch (e) { msg.textContent = "โŒ Failed to add user."; console.error("Add user error:", e); @@ -38,7 +120,7 @@ async function addNewUser() { function displayUserDropdown() { const dropdown = document.getElementById("userDropdown"); dropdown.innerHTML = ``; - users.forEach(user => { + adminUsers.forEach(user => { dropdown.innerHTML += ``; }); } @@ -104,8 +186,9 @@ async function loadWordsForSelectedUser() { const selectedUser = document.getElementById("wordUserSelect").value; if (!selectedUser) return; - const res = await fetch(`/getWordList?user=${encodeURIComponent(selectedUser)}`); - allWords = await res.json(); + const res = await fetch(`/getWordList?username=${encodeURIComponent(selectedUser)}`); + const data = await res.json(); + allWords = data.words || []; displayWordList(); } @@ -118,7 +201,7 @@ function populateWordUserDropdown() { const selects = document.querySelectorAll(".word-user-dropdown"); selects.forEach(select => { select.innerHTML = ''; - users.forEach(user => { + adminUsers.forEach(user => { const option = document.createElement("option"); option.value = user.username; option.textContent = user.username; @@ -136,8 +219,9 @@ async function displayWordListForSelectedUser() { if (!username) return; try { - const res = await fetch(`/getWordList?user=${username}`); - const words = await res.json(); + const res = await fetch(`/getWordList?username=${username}`); + const data = await res.json(); + const words = data.words || []; if (!words.length) { list.innerHTML = "
  • No words found for this user.
  • "; return; @@ -154,7 +238,7 @@ async function displayWordListForSelectedUser() { } // ๐Ÿ“Š Student result viewer -async function loadStudentResults() { +window.loadStudentResults = async function() { const list = document.getElementById("resultsList"); list.innerHTML = ''; @@ -167,19 +251,38 @@ async function loadStudentResults() { return; } - for (const [username, result] of Object.entries(results)) { - const li = document.createElement("li"); - let answersHTML = ''; - result.answers.forEach(entry => { - const status = entry.correct ? 'โœ…' : 'โŒ'; - answersHTML += `${status} ${entry.word}
    `; - }); + for (const [username, userResults] of Object.entries(results)) { + // Handle array of results per user + if (Array.isArray(userResults) && userResults.length > 0) { + userResults.forEach((result, index) => { + const li = document.createElement("li"); + let answersHTML = ''; + + // Check if result and answers exist and are valid + if (result && result.answers && Array.isArray(result.answers)) { + result.answers.forEach(entry => { + const status = entry.correct ? 'โœ…' : 'โŒ'; + answersHTML += `${status} ${entry.word}
    `; + }); + } else { + answersHTML = 'No answer data available'; + } - li.innerHTML = ` - ${username} โ€” Score: ${result.score}, Completed: ${result.completed ? 'โœ…' : 'โŒ'}
    - Answers:
    ${answersHTML} - `; - list.appendChild(li); + const timestamp = result.timestamp ? new Date(result.timestamp).toLocaleString() : 'Unknown time'; + li.innerHTML = ` + ${username} (Session ${index + 1}) โ€” Score: ${result?.score || 'N/A'}, Completed: ${result?.completed ? 'โœ…' : 'โŒ'}
    + Date: ${timestamp}
    + Answers:
    ${answersHTML} +
    + `; + list.appendChild(li); + }); + } else { + // Fallback for single result format + const li = document.createElement("li"); + li.innerHTML = `${username} โ€” No valid results found`; + list.appendChild(li); + } } } catch (err) { console.error("๐Ÿ”ด Failed to load results:", err); @@ -187,40 +290,6 @@ async function loadStudentResults() { } } -// ๐Ÿงญ Admin tab switcher -function switchTab(targetId) { - document.querySelectorAll('.adminTab').forEach(tab => - tab.classList.add('hidden') - ); - document.querySelectorAll('#adminTabs button').forEach(btn => - btn.classList.remove('active') - ); - - // Show the selected tab - document.getElementById(targetId).classList.remove('hidden'); - - // Highlight the active button - document.querySelectorAll('#adminTabs button').forEach(btn => { - if (btn.getAttribute('onclick')?.includes(targetId)) { - btn.classList.add('active'); - } - }); - - // ๐Ÿ” Trigger tab-specific logic - if (targetId === 'tabSessions') { - loadSessionHistory(); - } - if (targetId === 'tabReports') { - loadStudentNamesForReport(); - } - if (targetId === 'tabBadges') { - loadBadgeViewer(); - } - if (targetId === 'tabSessions') { - populateSessionUserDropdown(); - document.getElementById("userSessionList").innerHTML = ""; -} -} async function loadUserDropdowns() { try { const res = await fetch('/getUsers'); @@ -243,6 +312,8 @@ async function loadUserDropdowns() { drop.appendChild(opt); }); }); + + console.log('โœ… User dropdowns loaded'); } catch (err) { console.error("โš ๏ธ Failed to load user list:", err); } @@ -352,44 +423,73 @@ async function loadStudentNamesForReport() { }); } -async function loadStudentReport(username) { +window.loadStudentReport = async function(username) { if (!username) return; - const res1 = await fetch('/getResults'); - const results = await res1.json(); - const res2 = await fetch('/getBadges'); - const badges = await res2.json(); + try { + const res1 = await fetch('/getResults'); + const results = await res1.json(); + const res2 = await fetch('/getBadges'); + const badges = await res2.json(); - const report = results[username]; - const badgeList = badges[username] || []; + const userResults = results[username]; + const badgeList = badges[username] || []; - document.getElementById("reportTitle").textContent = `Report for ${username}`; - document.getElementById("reportScore").textContent = `${report.score}/${report.answers.length} correct`; - - const wordList = document.getElementById("reportWords"); - wordList.innerHTML = ""; - report.answers.forEach(({ word, correct }) => { - const li = document.createElement("li"); - li.textContent = `${word} โ€” ${correct ? 'โœ…' : 'โŒ'}`; - wordList.appendChild(li); - }); + // Handle case where userResults might be an array or undefined + let latestReport = null; + if (Array.isArray(userResults) && userResults.length > 0) { + // Get the most recent result + latestReport = userResults[userResults.length - 1]; + } else if (userResults && typeof userResults === 'object') { + // Handle single result object + latestReport = userResults; + } - const badgeContainer = document.getElementById("reportBadges"); - badgeContainer.innerHTML = ""; - if (badgeList.length) { - badgeList.forEach(b => { - const li = document.createElement("li"); - li.textContent = `๐Ÿ… ${b}`; - badgeContainer.appendChild(li); - }); - } else { - badgeContainer.innerHTML = "
  • No badges earned yet.
  • "; + if (!latestReport) { + document.getElementById("reportContent").classList.add("hidden"); + alert(`No results found for ${username}`); + return; + } + + document.getElementById("reportTitle").textContent = `Report for ${username}`; + + // Safe access to score and answers + const score = latestReport.score || 0; + const answers = latestReport.answers || []; + document.getElementById("reportScore").textContent = `${score}/${answers.length} correct`; + + const wordList = document.getElementById("reportWords"); + wordList.innerHTML = ""; + if (answers.length > 0) { + answers.forEach(({ word, correct }) => { + const li = document.createElement("li"); + li.textContent = `${word} โ€” ${correct ? 'โœ…' : 'โŒ'}`; + wordList.appendChild(li); + }); + } else { + wordList.innerHTML = "
  • No word data available.
  • "; + } + + const badgeContainer = document.getElementById("reportBadges"); + badgeContainer.innerHTML = ""; + if (badgeList.length) { + badgeList.forEach(b => { + const li = document.createElement("li"); + li.textContent = `๐Ÿ… ${b}`; + badgeContainer.appendChild(li); + }); + } else { + badgeContainer.innerHTML = "
  • No badges earned yet.
  • "; + } + + document.getElementById("reportContent").classList.remove("hidden"); + } catch (error) { + console.error("Error loading student report:", error); + alert("Error loading student report. Please try again."); } - - document.getElementById("reportContent").classList.remove("hidden"); } -function copyReportToClipboard() { +window.copyReportToClipboard = function() { const name = document.getElementById("reportTitle").textContent; const score = document.getElementById("reportScore").textContent; const words = Array.from(document.querySelectorAll("#reportWords li")).map(li => li.textContent).join("\n"); @@ -457,4 +557,47 @@ async function loadUserSessions(username) { sessionItem.innerHTML = html; list.appendChild(sessionItem); }); -} \ No newline at end of file +} + +// File upload handler for word lists +function handleFileUpload(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = function(e) { + const content = e.target.result; + const words = content.split(/\r?\n/).map(w => w.trim()).filter(Boolean); + + document.getElementById('wordInput').value = words.join('\n'); + showToast(`๐Ÿ“ Loaded ${words.length} words from file`); + }; + reader.readAsText(file); +} + +// Add event listeners for admin tabs as backup to onclick +document.addEventListener('DOMContentLoaded', () => { + // Setup tab click listeners + setTimeout(() => { + const tabButtons = document.querySelectorAll('#adminTabs button'); + tabButtons.forEach(button => { + button.addEventListener('click', (e) => { + e.preventDefault(); + const onclick = button.getAttribute('onclick'); + if (onclick) { + const match = onclick.match(/switchTab\('(.+?)'\)/); + if (match) { + const tabId = match[1]; + console.log('Tab clicked:', tabId); + if (typeof window.switchTab === 'function') { + window.switchTab(tabId); + } else { + console.error('switchTab function not available'); + } + } + } + }); + }); + console.log('โœ… Tab event listeners added to', tabButtons.length, 'buttons'); + }, 300); +}); \ No newline at end of file diff --git a/public/js/auth.js b/public/js/auth.js index 5cd3d95..3cebf6c 100644 --- a/public/js/auth.js +++ b/public/js/auth.js @@ -1,8 +1,24 @@ // ๐Ÿ” Password hashing using jsSHA function hashPassword(password) { - const shaObj = new jsSHA("SHA-256", "TEXT"); - shaObj.update(password); - return shaObj.getHash("HEX"); + try { + if (typeof jsSHA === 'undefined') { + console.error('jsSHA library not loaded'); + throw new Error('jsSHA library not available'); + } + const shaObj = new jsSHA("SHA-256", "TEXT"); + shaObj.update(password); + return shaObj.getHash("HEX"); + } catch (error) { + console.error('Error hashing password:', error); + // Fallback: simple hash (not secure, but for debugging) + let hash = 0; + for (let i = 0; i < password.length; i++) { + const char = password.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + } + return Math.abs(hash).toString(16); + } } // ๐ŸŒ Load users from server @@ -12,28 +28,43 @@ let users = []; async function verifyLogin() { const uname = document.getElementById('username').value.trim(); const pw = document.getElementById('password').value; - const hash = hashPassword(pw); const msg = document.getElementById('loginMessage'); + if (!uname || !pw) { + msg.textContent = "Please enter both username and password."; + msg.style.color = 'var(--error)'; + return; + } + try { + console.log('Attempting login for:', uname); + const hash = hashPassword(pw); + console.log('Generated hash:', hash); + const res = await fetch('/verifyUser', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: uname, hash }) }); + const responseText = await res.text(); + console.log('Server response:', res.status, responseText); + if (!res.ok) { msg.textContent = "Invalid username or password."; + msg.style.color = 'var(--error)'; return; } - const user = await res.json(); + const user = JSON.parse(responseText); + console.log('Login successful for user:', user); localStorage.setItem('loggedInUser', JSON.stringify(user)); msg.textContent = ''; setupSession(user); } catch (err) { console.error("Login error:", err); msg.textContent = "Login failed. Please try again."; + msg.style.color = 'var(--error)'; } } @@ -41,18 +72,60 @@ async function verifyLogin() { function logoutUser() { localStorage.removeItem('loggedInUser'); - document.getElementById('adminPanel').classList.add('hidden'); - document.getElementById('studentPanel').classList.add('hidden'); - document.getElementById('nav').classList.add('hidden'); - document.getElementById('loginPanel').classList.remove('hidden'); - document.getElementById('username').value = ''; - document.getElementById('password').value = ''; - - const roleBanner = document.getElementById('roleBanner'); - roleBanner.classList.add('hidden'); - roleBanner.classList.remove('admin', 'student'); - roleBanner.textContent = ''; - + // Hide all panels + document.getElementById('adminPanel')?.classList.add('hidden'); + document.getElementById('studentPanel')?.classList.add('hidden'); + document.getElementById('nav')?.classList.add('hidden'); + + // Show login panel + const loginPanel = document.getElementById('loginPanel'); + loginPanel?.classList.remove('hidden'); + + // Clear login form + const usernameField = document.getElementById('username'); + const passwordField = document.getElementById('password'); const loginMessage = document.getElementById('loginMessage'); - if (loginMessage) loginMessage.textContent = ''; -} \ No newline at end of file + + if (usernameField) usernameField.value = ''; + if (passwordField) passwordField.value = ''; + if (loginMessage) { + loginMessage.textContent = ''; + loginMessage.style.color = ''; + } + + // Reset banner + const roleBanner = document.getElementById('roleBanner'); + if (roleBanner) { + roleBanner.classList.add('hidden'); + roleBanner.classList.remove('admin', 'student'); + roleBanner.textContent = ''; + } + + // Focus on username field + if (usernameField) { + setTimeout(() => usernameField.focus(), 100); + } + + showToast('Logged out successfully'); +} + +// Add Enter key support for login form +document.addEventListener('DOMContentLoaded', () => { + const loginForm = document.querySelector('#loginPanel form'); + const usernameField = document.getElementById('username'); + const passwordField = document.getElementById('password'); + + if (loginForm && usernameField && passwordField) { + // Focus username field on page load + usernameField.focus(); + + // Add Enter key support + [usernameField, passwordField].forEach(field => { + field.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + verifyLogin(); + } + }); + }); + } +}); \ No newline at end of file diff --git a/public/js/game.js b/public/js/game.js index 8efd86f..fb12ed9 100644 --- a/public/js/game.js +++ b/public/js/game.js @@ -3,7 +3,46 @@ let words = []; let currentWord = ''; let currentIndex = 0; let results = []; -let typingWords = []; +// ๐Ÿ“ Check user's answer and move to next +window.submitAnswer = function() { + const input = document.getElementById('userInput').value.trim(); + + // Record the attempt + results.push({ + word: currentWord, + attempt: input + }); + + if (input.toLowerCase() === currentWord.toLowerCase()) { + currentIndex++; + document.getElementById('inputSection').classList.add('hidden'); + setTimeout(() => showWord(), 500); // Brief pause before next word + } else { + // Show feedback for incorrect answer + const inputEl = document.getElementById('userInput'); + inputEl.style.borderColor = 'var(--error)'; + inputEl.style.backgroundColor = '#fef2f2'; + + setTimeout(() => { + inputEl.style.borderColor = ''; + inputEl.style.backgroundColor = ''; + inputEl.focus(); + }, 1000); + } +} + +// Add Enter key support for game input +document.addEventListener('DOMContentLoaded', () => { + const gameInput = document.getElementById('userInput'); + if (gameInput) { + gameInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + submitAnswer(); + } + }); + } +});[]; +let currentSentence = ''; function showStudent() { console.log("๐Ÿ”Š Showing student panel"); @@ -34,7 +73,7 @@ function showStudent() { } // ๐Ÿš€ Begin game session -async function startGame() { +window.startGame = async function() { console.log('๐ŸŽฎ Starting spelling game'); const user = JSON.parse(localStorage.getItem('loggedInUser')); @@ -62,6 +101,7 @@ async function startGame() { // Start game words = [...data.words]; + typingWords = [...data.words]; // Initialize typing words too currentIndex = 0; results = []; // Reset results array showWord(); diff --git a/public/js/main.js b/public/js/main.js index 1f64cbc..46f6447 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -1,22 +1,54 @@ document.addEventListener("DOMContentLoaded", async () => { - // ๐ŸŒ Load users.json for login validation - // await loadUsers(); - displayUserDropdown(); - populateWordUserDropdown(); - - // ๐ŸŽจ Restore saved theme - applySavedTheme(); - - // ๐Ÿ” Restore session if user was logged in - const savedUser = localStorage.getItem("loggedInUser"); - if (savedUser) { - setupSession(JSON.parse(savedUser)); + console.log('๐Ÿš€ Application starting...'); + + // Load users for admin functionality + try { + await loadUsers(); + displayUserDropdown(); + populateWordUserDropdown(); + } catch (e) { + console.warn('Could not load users (normal if not admin):', e); } - // ๐Ÿงญ Tab switching for Admin Panel + // Restore saved theme + applySavedTheme(); + + // Restore session if user was logged in + const savedUser = localStorage.getItem("loggedInUser"); + if (savedUser) { + try { + const user = JSON.parse(savedUser); + console.log('๐Ÿ‘ค Restoring session for:', user.username); + setupSession(user); + } catch (e) { + console.error('Error restoring session:', e); + localStorage.removeItem("loggedInUser"); + } + } else { + console.log('๐Ÿ‘ค No saved session found'); + // Focus on login form + const usernameField = document.getElementById('username'); + if (usernameField) { + setTimeout(() => usernameField.focus(), 100); + } + } + + // Set default admin tab const defaultTab = document.querySelector('#adminTabs button.active'); if (defaultTab) { const targetId = defaultTab.getAttribute("onclick")?.match(/'(.*?)'/)?.[1]; - if (targetId) switchTab(targetId); + if (targetId) { + console.log('๐Ÿ”ง Setting default admin tab:', targetId); + // Use a small delay to ensure all scripts are loaded + setTimeout(() => { + if (typeof window.switchTab === 'function') { + window.switchTab(targetId); + } else { + console.warn('switchTab function not yet available'); + } + }, 200); + } } + + console.log('โœ… Application initialized'); }); \ No newline at end of file diff --git a/public/js/themes.js b/public/js/themes.js index 65254d4..ea623ff 100644 --- a/public/js/themes.js +++ b/public/js/themes.js @@ -1,14 +1,73 @@ -// ๐ŸŽจ Apply saved theme on load +// Modern theme system function applySavedTheme() { - const savedTheme = localStorage.getItem('theme') || 'playground'; - document.body.className = savedTheme; + const savedTheme = localStorage.getItem('theme') || 'default'; + applyTheme(savedTheme); const themeSelector = document.getElementById('themeSelect'); if (themeSelector) themeSelector.value = savedTheme; } -// ๐ŸŽจ Change theme dynamically function changeTheme(themeName) { - document.body.className = themeName; + applyTheme(themeName); localStorage.setItem('theme', themeName); + showToast(`๐ŸŽจ Theme changed to ${themeName}`); +} + +function applyTheme(themeName) { + // Remove existing theme classes + document.body.className = ''; + + // Apply theme-specific styles + switch(themeName) { + case 'playground': + document.body.className = 'theme-playground'; + document.documentElement.style.setProperty('--primary', '#ff6b6b'); + document.documentElement.style.setProperty('--accent', '#4ecdc4'); + document.documentElement.style.setProperty('--bg-primary', '#fff5f5'); + document.documentElement.style.setProperty('--bg-secondary', '#ffebee'); + break; + + case 'chalkboard': + document.body.className = 'theme-chalkboard'; + document.documentElement.style.setProperty('--primary', '#4ade80'); + document.documentElement.style.setProperty('--accent', '#22d3ee'); + document.documentElement.style.setProperty('--bg-primary', '#1f2937'); + document.documentElement.style.setProperty('--bg-secondary', '#374151'); + document.documentElement.style.setProperty('--text-primary', '#f9fafb'); + document.documentElement.style.setProperty('--text-secondary', '#d1d5db'); + document.documentElement.style.setProperty('--border', '#4b5563'); + break; + + case 'galaxy': + document.body.className = 'theme-galaxy'; + document.documentElement.style.setProperty('--primary', '#8b5cf6'); + document.documentElement.style.setProperty('--accent', '#06b6d4'); + document.documentElement.style.setProperty('--bg-primary', '#0f0f23'); + document.documentElement.style.setProperty('--bg-secondary', '#1a1a3f'); + document.documentElement.style.setProperty('--text-primary', '#e0e7ff'); + document.documentElement.style.setProperty('--text-secondary', '#a5b4fc'); + document.documentElement.style.setProperty('--border', '#3730a3'); + break; + + case 'nature': + document.body.className = 'theme-nature'; + document.documentElement.style.setProperty('--primary', '#059669'); + document.documentElement.style.setProperty('--accent', '#84cc16'); + document.documentElement.style.setProperty('--bg-primary', '#f0fdf4'); + document.documentElement.style.setProperty('--bg-secondary', '#dcfce7'); + document.documentElement.style.setProperty('--text-primary', '#14532d'); + break; + + default: // Modern default theme + document.body.className = 'theme-default'; + // Reset to original CSS custom properties + document.documentElement.style.setProperty('--primary', '#3b82f6'); + document.documentElement.style.setProperty('--accent', '#06b6d4'); + document.documentElement.style.setProperty('--bg-primary', '#ffffff'); + document.documentElement.style.setProperty('--bg-secondary', '#f8fafc'); + document.documentElement.style.setProperty('--text-primary', '#1e293b'); + document.documentElement.style.setProperty('--text-secondary', '#64748b'); + document.documentElement.style.setProperty('--border', '#e2e8f0'); + break; + } } \ No newline at end of file diff --git a/public/js/typing.js b/public/js/typing.js index 6e397f6..e730abc 100644 --- a/public/js/typing.js +++ b/public/js/typing.js @@ -5,7 +5,8 @@ const typingState = { currentWord: '' }; -async function startTypingPractice() { +// Make functions globally accessible +window.startTypingPractice = async function() { console.log('Starting typing practice'); const user = JSON.parse(localStorage.getItem('loggedInUser')); @@ -34,7 +35,7 @@ async function startTypingPractice() { } } -function showNextWord() { +window.showNextWord = function() { const promptEl = document.getElementById('typingPrompt'); const inputEl = document.getElementById('typingInput'); if (!promptEl || !inputEl) return; @@ -58,14 +59,8 @@ function showNextWord() { inputEl.focus(); } - - // Reset input - inputEl.value = ''; - inputEl.disabled = false; - inputEl.focus(); - -function submitTyping() { +window.submitTyping = function() { const input = document.getElementById('typingInput')?.value.trim() || ''; const feedback = document.getElementById('typingFeedback'); if (!feedback) return; diff --git a/public/js/ui.js b/public/js/ui.js index 09669e8..f391bdd 100644 --- a/public/js/ui.js +++ b/public/js/ui.js @@ -46,16 +46,58 @@ function showToast(message, duration = 3000) { toast.classList.remove("show"); }, duration); } -function showAdmin() { - console.log("showAdmin fired"); - - const adminPanel = document.getElementById("adminPanel"); - const studentPanel = document.getElementById("studentPanel"); +function showStudent() { + console.log("๐ŸŽ“ Showing student panel"); - loadUserDropdowns(); + // Hide other panels + document.getElementById('adminPanel')?.classList.add('hidden'); + document.getElementById('loginPanel')?.classList.add('hidden'); + + // Show student panel + const studentPanel = document.getElementById('studentPanel'); + if (studentPanel) { + studentPanel.classList.remove('hidden'); + + // Ensure buttons are visible and functional + const gameBtn = document.getElementById('startGameBtn'); + const typingBtn = document.getElementById('startTypingBtn'); + + if (gameBtn && typingBtn) { + gameBtn.classList.remove('hidden'); + typingBtn.classList.remove('hidden'); + console.log("โœ… Student buttons are now visible"); + } + } +} - adminPanel?.classList.remove("hidden"); - studentPanel?.classList.add("hidden"); +function showAdmin() { + console.log("๐Ÿ‘จโ€๐Ÿ’ผ Showing admin panel"); - switchTab("tabUsers"); // optional default tab + // Hide other panels + document.getElementById('studentPanel')?.classList.add('hidden'); + document.getElementById('loginPanel')?.classList.add('hidden'); + + const adminPanel = document.getElementById("adminPanel"); + if (adminPanel) { + adminPanel.classList.remove("hidden"); + + // Load user data for dropdowns + loadUserDropdowns().then(() => { + console.log("โœ… User dropdowns loaded"); + }).catch(e => { + console.warn("Could not load user dropdowns:", e); + }); + + // Set default tab - use a slight delay to ensure DOM is ready + setTimeout(() => { + if (typeof window.switchTab === 'function') { + window.switchTab("tabUsers"); + console.log("โœ… Default tab set"); + } else { + console.error("switchTab function not available"); + } + }, 100); + } else { + console.error("Admin panel not found"); + } } \ No newline at end of file diff --git a/public/styles.css b/public/styles.css index 28b9e8d..3359f29 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,319 +1,397 @@ -/* ๐Ÿ“‹ Global Defaults */ -body { - font-family: sans-serif; - text-align: center; - padding: 2rem; +/* Modern Spelling App Styles */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); + +/* Reset and base styles */ +* { margin: 0; -} - -ul { - list-style: none; padding: 0; -} -li { - margin: 0.25rem 0; + box-sizing: border-box; } +:root { + /* Modern color palette */ + --primary: #3b82f6; + --primary-dark: #2563eb; + --secondary: #f1f5f9; + --accent: #06b6d4; + --success: #10b981; + --warning: #f59e0b; + --error: #ef4444; + --text-primary: #1e293b; + --text-secondary: #64748b; + --border: #e2e8f0; + --bg-primary: #ffffff; + --bg-secondary: #f8fafc; + --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --radius: 0.75rem; + --transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + background: linear-gradient(135deg, var(--bg-secondary) 0%, #ffffff 100%); + color: var(--text-primary); + line-height: 1.6; + min-height: 100vh; + padding: 2rem 1rem; +} + +/* Container and layout */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +/* Typography */ +h1 { + font-size: 3rem; + font-weight: 700; + text-align: center; + margin-bottom: 2rem; + background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +h2 { + font-size: 1.875rem; + font-weight: 600; + margin-bottom: 1.5rem; + color: var(--text-primary); +} + +h3 { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 1rem; + color: var(--text-primary); +} + +/* Form elements */ input, textarea, select { - font-size: 1rem; - padding: 0.5rem; + width: 100%; + max-width: 320px; + padding: 0.75rem 1rem; margin: 0.5rem; - border: 2px solid #ccc; - border-radius: 5px; + border: 2px solid var(--border); + border-radius: var(--radius); + font-size: 1rem; + font-family: inherit; + background: var(--bg-primary); + color: var(--text-primary); + transition: var(--transition); } -/* ๐ŸŽจ Theme Styles */ -body.playground { - background: #fef9e7; - color: #333; - font-family: 'Comic Sans MS', cursive, sans-serif; -} -body.chalkboard { - background: #222; - color: #f0f0f0; - font-family: 'Courier New', monospace; -} -body.galaxy { - background: radial-gradient(circle at center, #0d0d2b, #1a1a3f); - color: #b3e5fc; - font-family: 'Orbitron', sans-serif; -} -body.nature { - background: #e8f5e9; - color: #2e7d32; - font-family: 'Georgia', serif; +input:focus, textarea:focus, select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1); } -/* ๐Ÿ”˜ Buttons */ +textarea { + min-height: 120px; + resize: vertical; + max-width: 500px; +} + +/* Buttons */ button { - background-color: #4caf50; + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + margin: 0.25rem; + background: var(--primary); color: white; border: none; - border-radius: 8px; - padding: 0.6rem 1rem; + border-radius: var(--radius); font-size: 1rem; - margin: 0.4rem; + font-weight: 500; + font-family: inherit; cursor: pointer; - transition: transform 0.2s; - display: inline-block; + transition: var(--transition); + box-shadow: var(--shadow); } + button:hover { - transform: scale(1.05); + background: var(--primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-lg); } -/* ๐ŸŒˆ Theme-specific Button Colors */ -body.chalkboard button { - background-color: #555; -} -body.galaxy button { - background-color: #6200ea; -} -body.nature button { - background-color: #81c784; +button:active { + transform: translateY(0); } -/* ๐Ÿ”ณ Panels and Banners */ +button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* Button variants */ +.btn-secondary { + background: var(--secondary); + color: var(--text-primary); +} + +.btn-secondary:hover { + background: var(--border); +} + +.btn-success { + background: var(--success); +} + +.btn-success:hover { + background: #059669; +} + +.btn-warning { + background: var(--warning); +} + +.btn-warning:hover { + background: #d97706; +} + +.btn-error { + background: var(--error); +} + +.btn-error:hover { + background: #dc2626; +} + +/* Cards and panels */ +.card { + background: var(--bg-primary); + border-radius: var(--radius); + padding: 2rem; + margin: 1rem 0; + box-shadow: var(--shadow); + border: 1px solid var(--border); +} + +.card-header { + padding-bottom: 1rem; + margin-bottom: 1rem; + border-bottom: 1px solid var(--border); +} + +/* Layout utilities */ .hidden { - display: none; + display: none !important; } -#wordBox { - font-size: 2em; - margin: 20px; - min-height: 60px; - padding: 20px; - background-color: #f5f5f5; - border-radius: 8px; - transition: opacity 0.3s ease; + +.flex { + display: flex; } -.word-box { - font-size: 3rem; - font-weight: bold; - margin-bottom: 2rem; - color: #2196f3; - text-shadow: 1px 1px #ccc; + +.flex-col { + flex-direction: column; } -.banner { - padding: 0.5em; + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.gap-4 { + gap: 1rem; +} + +.text-center { text-align: center; - font-weight: bold; - color: white; -} -.banner.admin { - background-color: #b22222; -} -.banner.student { - background-color: #1e90ff; } -/* ๐Ÿ”” Toast Messages */ -.toast { - position: fixed; - bottom: 20px; - right: 20px; - background: #333; - color: #fff; - padding: 0.75em 1.25em; - border-radius: 5px; - opacity: 0; - pointer-events: none; - transition: opacity 0.4s ease; - z-index: 999; -} -.toast.show { - opacity: 1; - pointer-events: auto; +/* Specific component styles */ +#loginPanel { + max-width: 400px; + margin: 0 auto; + background: var(--bg-primary); + padding: 2rem; + border-radius: var(--radius); + box-shadow: var(--shadow-lg); } -/* ๐Ÿงญ Admin Tabs */ -#adminTabs { - margin: 1em 0; +#loginPanel form { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; } -#adminTabs button { - margin-right: 8px; - padding: 10px 16px; - background-color: #f0f0f0; - border: 1px solid #ccc; - border-bottom: none; - border-radius: 6px 6px 0 0; + +/* Banner */ +.banner { + padding: 1rem; + text-align: center; font-weight: 600; - color: #333; + color: white; + border-radius: var(--radius); + margin-bottom: 2rem; + box-shadow: var(--shadow); +} + +.banner.admin { + background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); +} + +.banner.student { + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); +} + +/* Navigation */ +#nav { + text-align: center; + margin-bottom: 2rem; + padding: 1rem; + background: var(--bg-primary); + border-radius: var(--radius); + box-shadow: var(--shadow); +} + +/* Admin tabs */ +#adminTabs { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 2rem; + padding: 0.5rem; + background: var(--bg-secondary); + border-radius: var(--radius); +} + +#adminTabs button { + flex: 1; + min-width: fit-content; + background: transparent; + color: var(--text-secondary); + border: none; + border-radius: calc(var(--radius) - 0.25rem); + padding: 0.75rem 1rem; + font-weight: 500; + transition: var(--transition); + box-shadow: none; cursor: pointer; - transition: background-color 0.2s, color 0.2s; } + #adminTabs button:hover { - background-color: #e0f2ff; - color: #000; + background: var(--bg-primary); + color: var(--text-primary); + transform: none; } + #adminTabs button.active { - background-color: #ffffff; - color: #000; - border-bottom: 2px solid #fff; - box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1); + background: var(--primary) !important; + color: white !important; + box-shadow: var(--shadow); } + .adminTab { - display: none; -} -.adminTab:not(.hidden) { - display: block; + background: var(--bg-primary); + border-radius: var(--radius); + padding: 2rem; + box-shadow: var(--shadow); + border: 1px solid var(--border); } -.typing-sentence { - font-size: 1.5em; - margin: 20px 0; - padding: 15px; - background-color: #f5f5f5; - border-radius: 8px; - border: 1px solid #ddd; - min-height: 50px; - display: block !important; - visibility: visible !important; +.adminTab.hidden { + display: none !important; } -/* Remove these style classes if they exist */ -.typing-section -.typing-container -.typing-prompt -.input-container -#typingInput -#typingFeedback - -/* Fix for start buttons visibility */ -#startGameBtn, #startTypingBtn { - display: inline-block; - margin: 10px; - padding: 12px 24px; - font-size: 1.1em; - background-color: #4CAF50; - color: white; - border: none; - border-radius: 5px; - cursor: pointer; -} - -#startGameBtn:hover, #startTypingBtn:hover { - transform: scale(1.05); - background-color: #45a049; -} - -/* Override any hidden class for these buttons */ -#startGameBtn.hidden, #startTypingBtn.hidden { - display: none !important; -} - -/* Button container for better spacing */ -.button-container { - margin: 20px 0; - padding: 10px; - text-align: center; +/* Game elements */ +#wordBox, .word-box, #typingPrompt { + font-size: 3rem; + font-weight: 700; + text-align: center; + padding: 3rem 2rem; + margin: 2rem 0; + background: linear-gradient(135deg, var(--bg-primary) 0%, var(--secondary) 100%); + border: 2px solid var(--border); + border-radius: var(--radius); + color: var(--primary); + box-shadow: var(--shadow-lg); + min-height: 120px; + display: flex; + align-items: center; + justify-content: center; } .student-controls { - display: block !important; - margin: 20px auto; - padding: 15px; - text-align: center; - visibility: visible !important; + text-align: center; + margin: 2rem 0; + padding: 2rem; + background: var(--bg-primary); + border-radius: var(--radius); + box-shadow: var(--shadow); } -.student-btn { - display: inline-block !important; - visibility: visible !important; - background-color: #4CAF50; - color: white; - padding: 12px 24px; - margin: 10px; - border: none; - border-radius: 5px; - font-size: 16px; - cursor: pointer; - opacity: 1 !important; +/* Lists */ +ul { + list-style: none; } -/* Override any hiding classes for these specific buttons */ -#startGameBtn:not(.hidden), -#startTypingBtn:not(.hidden) { - display: inline-block !important; - visibility: visible !important; - opacity: 1 !important; +li { + padding: 0.75rem 1rem; + margin: 0.5rem 0; + background: var(--bg-secondary); + border-radius: calc(var(--radius) - 0.25rem); + border: 1px solid var(--border); } -/* Remove this if it exists as it might conflict */ -.hidden { - display: none !important; +/* Toast notifications */ +.toast { + position: fixed; + bottom: 2rem; + right: 2rem; + background: var(--text-primary); + color: white; + padding: 1rem 1.5rem; + border-radius: var(--radius); + opacity: 0; + transform: translateY(100%); + transition: var(--transition); + z-index: 1000; + box-shadow: var(--shadow-lg); + font-weight: 500; } -/* Add this to ensure student panel shows properly */ -#studentPanel:not(.hidden) { - display: block !important; +.toast.show { + opacity: 1; + transform: translateY(0); } -/* ๐Ÿ“ Typing Practice Styles */ -.typing-section { - max-width: 800px; - margin: 20px auto; - padding: 20px; -} - -#typingPrompt { - font-size: 2em; - font-weight: bold; - text-align: center; - margin: 20px 0; - padding: 20px; - background: #f5f5f5; - border-radius: 8px; - min-height: 60px; - display: block !important; - visibility: visible !important; -} - -#typingInput { - width: 300px; - padding: 10px; - font-size: 18px; - margin-right: 10px; -} - -.typing-word { - font-size: 32px; - font-weight: bold; - text-align: center; - padding: 20px; - margin: 20px 0; - background: #f5f5f5; - border-radius: 8px; - min-height: 50px; - display: block !important; - visibility: visible !important; -} - -.typing-input-container { - text-align: center; - margin: 20px 0; -} - -/* Fix student panel buttons */ -#startGameBtn, #startTypingBtn { - display: inline-block; - margin: 10px; - padding: 12px 24px; - font-size: 1.1em; - background-color: #4CAF50; - color: white; - border: none; - border-radius: 5px; - cursor: pointer; - visibility: visible !important; -} - -/* Ensure student panel visibility */ -#studentPanel:not(.hidden) { - display: block !important; -} - -/* Remove any conflicting hidden styles */ -#startGameBtn:not(.hidden), -#startTypingBtn:not(.hidden) { - display: inline-block !important; - visibility: visible !important; +/* Responsive design */ +@media (max-width: 768px) { + body { + padding: 1rem 0.5rem; + } + + h1 { + font-size: 2rem; + } + + #adminTabs { + flex-direction: column; + } + + #adminTabs button { + flex: none; + } + + .card, .adminTab, #loginPanel { + padding: 1rem; + } + + #wordBox, .word-box, #typingPrompt { + font-size: 2rem; + padding: 2rem 1rem; + } } diff --git a/server.js b/server.js index 36f9e40..9c8cdc4 100644 --- a/server.js +++ b/server.js @@ -54,12 +54,25 @@ function readJsonSafe(filePath, fallback = {}) { // ๐Ÿ” Verify login app.post('/verifyUser', (req, res) => { const { username, hash } = req.body; + console.log('Login attempt - Username:', username, 'Hash:', hash); + if (typeof username !== 'string' || typeof hash !== 'string') { + console.log('Invalid credentials format'); return res.status(400).send("Invalid credentials format"); } + const users = readJsonSafe(path.join(DATA_DIR, files.users), []); + console.log('Available users:', users.map(u => u.username)); + const user = users.find(u => u.username === username && u.hash === hash); - user ? res.json(user) : res.status(401).send("Invalid login"); + + if (user) { + console.log('Login successful for:', username); + res.json(user); + } else { + console.log('Login failed for:', username); + res.status(401).send("Invalid login"); + } }); // โž• Add user