This commit is contained in:
nate@necloud.us committed 2025-07-02 09:55:00 -04:00
1 parent 1f3b27516f
commit a3d3ab8c46
615 files changed
+69080 -85

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Spelling App with Roles</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Spelling Practice</h1>
<!-- 👤 Login Panel -->
<div id="loginPanel">
<h2>User Login</h2>
<form onsubmit="event.preventDefault(); verifyLogin();">
<input type="text" id="username" placeholder="Username" required />
<input type="password" id="password" placeholder="Password" required />
<button type="submit">Login</button>
</form>
<p id="loginMessage"></p>
</div>
<!-- 🔀 Navigation -->
<div id="nav" class="hidden">
<p><strong id="userInfo"></strong></p>
<button onclick="showAdmin()" id="adminBtn" class="hidden">Admin Panel</button>
<button onclick="showStudent()">Student Panel</button>
<button onclick="logoutUser()">Log Out</button>
</div>
<!-- 👨‍🏫 Admin Panel -->
<div id="adminPanel" class="hidden">
<h2>Add New User</h2>
<form onsubmit="event.preventDefault(); addNewUser();">
<input type="text" id="newUsername" placeholder="Username" required />
<input type="password" id="newPassword" placeholder="Password" required />
<select id="newRole">
<option value="student">Student</option>
<option value="admin">Admin</option>
</select>
<button type="submit">Add User</button>
</form>
<!-- Manage Users -->
<h3>Manage Users</h3>
<select id="userDropdown" onchange="selectUserAction(this.value)">
<option value="">-- Select a user --</option>
</select>
<!-- edit words -->
<h3>Edit Word List for:</h3>
<select id="wordUserSelect" onchange="loadWordsForSelectedUser()">
<option value="">-- Select a user --</option>
</select>
<!-- Theme -->
<div style="margin: 1em 0;">
<label for="themeSelect"><strong>Select Theme:</strong></label><br/>
<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>
<h2>Enter Spelling Words</h2>
<textarea id="wordInput" placeholder="Enter one word per line..."></textarea><br/>
<button onclick="saveWords()">Save Word List</button>
<button onclick="clearWords()">Clear Saved Words</button>
<h3>Or Upload a Word File</h3>
<input type="file" accept=".txt,.csv" onchange="handleFileUpload(event)" />
<p><strong>Current Word List:</strong></p>
<ul id="wordListDisplay"></ul>
</div>
<!-- 👩‍🎓 Student Panel -->
<div id="studentPanel" class="hidden">
<button onclick="startGame()">Start Practice</button>
<div id="wordBox" class="hidden">
<div id="word" class="word-box"></div>
</div>
<div id="inputSection" class="hidden">
<input type="text" id="userInput" placeholder="Spell the word" />
<button onclick="submitAnswer()">Submit</button>
</div>
<div id="summary" class="hidden"></div>
</div>
<script src="script.js"></script>
</body>
</html>
+333
View File
@@ -0,0 +1,333 @@
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
async function hashPassword(password) {
const msgUint8 = new TextEncoder().encode(password)
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8)
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
}
// 👤 Login
async function verifyLogin() {
const uname = document.getElementById('username').value.trim()
const pw = document.getElementById('password').value
const hash = await 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) {
document.getElementById('loginPanel').classList.add('hidden')
document.getElementById('nav').classList.remove('hidden')
document.getElementById('userInfo').textContent = `${user.username} (${user.role})`
if (user.role === "admin") {
document.getElementById('adminBtn').classList.remove('hidden')
await showAdmin()
await loadUsers()
displayUserDropdown()
populateWordUserDropdown()
} else {
document.getElementById('adminBtn').classList.add('hidden')
showStudent()
}
}
function logoutUser() {
localStorage.removeItem('loggedInUser')
document.getElementById('loginPanel').classList.remove('hidden')
document.getElementById('nav').classList.add('hidden')
document.getElementById('adminPanel').classList.add('hidden')
document.getElementById('studentPanel').classList.add('hidden')
}
async function showAdmin() {
document.getElementById('adminPanel').classList.remove('hidden')
document.getElementById('studentPanel').classList.add('hidden')
loadWords()
displayWordList()
await loadUsers()
displayUserDropdown()
}
// ➕ 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")
if (!username || !password) {
msg.textContent = "Please fill out all fields."
return
}
const hash = await hashPassword(password)
await saveUserToServer(username, hash, role)
msg.textContent = `✅ User "${username}" added to server.`
document.getElementById("newUsername").value = ''
document.getElementById("newPassword").value = ''
await loadUsers()
displayUserDropdown()
}
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 loadWords() {
const saved = localStorage.getItem('spellingWords')
allWords = saved ? JSON.parse(saved) : []
}
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 dropdown = document.getElementById("wordUserSelect")
dropdown.innerHTML = `<option value="">-- Select a user --</option>`
users.forEach(user => {
dropdown.innerHTML += `<option value="${user.username}">${user.username}</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()
}
+89
View File
@@ -0,0 +1,89 @@
body {
font-family: sans-serif;
text-align: center;
padding: 2rem;
margin: 0;
}
/* 🎨 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;
}
/* 🔘 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 {
font-size: 1rem;
padding: 0.5rem;
margin: 0.5rem;
border: 2px solid #ccc;
border-radius: 5px;
}
/* 🔳 Panels */
.hidden {
display: none;
}
#wordBox {
margin-top: 5rem;
}
.word-box {
font-size: 3rem;
font-weight: bold;
margin-bottom: 2rem;
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;
}
+12
View File
@@ -0,0 +1,12 @@
[
{
"username": "admin1",
"hash": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
"role": "admin"
},
{
"username": "student1",
"hash": "8d969eef6ecad3c29a3a629280e686cf",
"role": "student"
}
]
+4
View File
@@ -0,0 +1,4 @@
{
"student1": ["apple", "banana", "cherry"],
"student2": ["dog", "cat", "fish"]
}