Merge pull request 'badges' (#2) from badges into main
Reviewed-on: nmemmert/Spelling#2
This commit is contained in:
commit
e7b6e9288a
5 files changed
+138
-23
No files matched your search
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"nate": [
|
||||||
|
"Perfect Round",
|
||||||
|
"No Misses",
|
||||||
|
"Accuracy Master",
|
||||||
|
"Practice Streak",
|
||||||
|
"Vocabulary Pro"
|
||||||
|
]
|
||||||
|
}
|
||||||
+14
-2
@@ -51,6 +51,7 @@
|
|||||||
<button onclick="switchTab('tabWords')">📝 Word Lists</button>
|
<button onclick="switchTab('tabWords')">📝 Word Lists</button>
|
||||||
<button onclick="switchTab('tabAnalytics')">📊 Analytics</button>
|
<button onclick="switchTab('tabAnalytics')">📊 Analytics</button>
|
||||||
<button onclick="switchTab('tabResults')">📊 Results</button>
|
<button onclick="switchTab('tabResults')">📊 Results</button>
|
||||||
|
<button onclick="switchTab('tabBadges')">🎖 Badges</button>
|
||||||
<button onclick="switchTab('tabTheme')">🎨 Theme</button>
|
<button onclick="switchTab('tabTheme')">🎨 Theme</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -119,7 +120,15 @@
|
|||||||
|
|
||||||
<button onclick="refreshAnalytics()">🔄 Refresh</button>
|
<button onclick="refreshAnalytics()">🔄 Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 🎖 Badges -->
|
||||||
|
<div id="tabBadges" class="adminTab hidden">
|
||||||
|
<h3>🎖 Badge Viewer</h3>
|
||||||
|
<ul id="badgeUserList"></ul>
|
||||||
|
<div id="badgeDetails" class="hidden">
|
||||||
|
<h4 id="badgeUserTitle"></h4>
|
||||||
|
<ul id="badgeUserBadges"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- 🎨 Theme Tab -->
|
<!-- 🎨 Theme Tab -->
|
||||||
<div id="tabTheme" class="adminTab hidden">
|
<div id="tabTheme" class="adminTab hidden">
|
||||||
<h3>🎨 Select Theme</h3>
|
<h3>🎨 Select Theme</h3>
|
||||||
@@ -146,7 +155,10 @@
|
|||||||
<input type="text" id="userInput" placeholder="Type your answer" autocomplete="off" />
|
<input type="text" id="userInput" placeholder="Type your answer" autocomplete="off" />
|
||||||
<button onclick="submitAnswer()">Submit</button>
|
<button onclick="submitAnswer()">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="badgeDisplay" class="badgeSection hidden">
|
||||||
|
<h3>🎖 Badges Earned</h3>
|
||||||
|
<ul id="badgeList"></ul>
|
||||||
|
</div>
|
||||||
<div id="summary" class="hidden"></div>
|
<div id="summary" class="hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -193,12 +193,18 @@ function switchTab(targetId) {
|
|||||||
document.querySelectorAll('#adminTabs button').forEach(btn => btn.classList.remove('active'));
|
document.querySelectorAll('#adminTabs button').forEach(btn => btn.classList.remove('active'));
|
||||||
|
|
||||||
document.getElementById(targetId).classList.remove('hidden');
|
document.getElementById(targetId).classList.remove('hidden');
|
||||||
|
|
||||||
const buttons = document.querySelectorAll('#adminTabs button');
|
const buttons = document.querySelectorAll('#adminTabs button');
|
||||||
buttons.forEach(btn => {
|
buttons.forEach(btn => {
|
||||||
if (btn.getAttribute('onclick')?.includes(targetId)) {
|
if (btn.getAttribute('onclick')?.includes(targetId)) {
|
||||||
btn.classList.add('active');
|
btn.classList.add('active');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🏆 Load badge viewer if needed
|
||||||
|
if (targetId === 'tabBadges') {
|
||||||
|
loadBadgeViewer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function loadUserDropdowns() {
|
async function loadUserDropdowns() {
|
||||||
try {
|
try {
|
||||||
@@ -278,3 +284,42 @@ async function refreshAnalytics() {
|
|||||||
summary.innerHTML = '<li style="color:red">Error loading analytics.</li>';
|
summary.innerHTML = '<li style="color:red">Error loading analytics.</li>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function loadBadgeViewer() {
|
||||||
|
const res = await fetch('/getBadges');
|
||||||
|
const badgeData = await res.json();
|
||||||
|
|
||||||
|
const userList = document.getElementById("badgeUserList");
|
||||||
|
userList.innerHTML = "";
|
||||||
|
|
||||||
|
for (const username in badgeData) {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.innerHTML = `<button onclick="showUserBadges('${username}')">${username}</button>`;
|
||||||
|
userList.appendChild(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showUserBadges(username) {
|
||||||
|
fetch('/getBadges')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
const badges = data[username] || [];
|
||||||
|
const title = document.getElementById("badgeUserTitle");
|
||||||
|
const badgeList = document.getElementById("badgeUserBadges");
|
||||||
|
const details = document.getElementById("badgeDetails");
|
||||||
|
|
||||||
|
title.textContent = `Badges for ${username}`;
|
||||||
|
badgeList.innerHTML = "";
|
||||||
|
|
||||||
|
if (!badges.length) {
|
||||||
|
badgeList.innerHTML = "<li><em>No badges earned yet.</em></li>";
|
||||||
|
} else {
|
||||||
|
badges.forEach(b => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.textContent = `🏅 ${b}`;
|
||||||
|
badgeList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
details.classList.remove("hidden");
|
||||||
|
});
|
||||||
|
}
|
||||||
+41
-20
@@ -10,7 +10,6 @@ function showStudent() {
|
|||||||
studentPanel?.classList.remove('hidden');
|
studentPanel?.classList.remove('hidden');
|
||||||
adminPanel?.classList.add('hidden');
|
adminPanel?.classList.add('hidden');
|
||||||
|
|
||||||
// Delay button reveal until DOM is fully settled
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const btn = document.getElementById('startGameBtn');
|
const btn = document.getElementById('startGameBtn');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
@@ -41,6 +40,7 @@ async function startGame() {
|
|||||||
results = [];
|
results = [];
|
||||||
|
|
||||||
document.getElementById('summary')?.classList.add('hidden');
|
document.getElementById('summary')?.classList.add('hidden');
|
||||||
|
document.getElementById('badgeDisplay')?.classList.add('hidden');
|
||||||
showWord();
|
showWord();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,32 +101,57 @@ function showSummary() {
|
|||||||
|
|
||||||
summary.classList.remove('hidden');
|
summary.classList.remove('hidden');
|
||||||
|
|
||||||
// 📨 Save results to server
|
// 🏆 Badge evaluation
|
||||||
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
const earnedBadges = [];
|
||||||
if (user) {
|
if (correct === words.length) earnedBadges.push("Perfect Round");
|
||||||
console.log("📡 Sending results to server:", {
|
if (missed.length === 0) earnedBadges.push("No Misses");
|
||||||
username: user.username,
|
|
||||||
results
|
|
||||||
});
|
|
||||||
|
|
||||||
const score = correct;
|
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||||
const completed = true;
|
if (user) {
|
||||||
|
// 📡 Save structured results
|
||||||
fetch('/saveResults', {
|
fetch('/saveResults', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
username: user.username,
|
username: user.username,
|
||||||
result: {
|
result: {
|
||||||
score,
|
score: correct,
|
||||||
completed,
|
completed: true,
|
||||||
answers: results.map(({ word, attempt }) => ({
|
answers: results.map(({ word, attempt }) => ({
|
||||||
word,
|
word,
|
||||||
correct: word.toLowerCase() === attempt.toLowerCase()
|
correct: word.toLowerCase() === attempt.toLowerCase()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🎖 Send earned badges
|
||||||
|
if (earnedBadges.length) {
|
||||||
|
fetch('/awardBadges', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: user.username,
|
||||||
|
badges: earnedBadges
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🎨 Display badges
|
||||||
|
const badgeSection = document.getElementById("badgeDisplay");
|
||||||
|
const badgeList = document.getElementById("badgeList");
|
||||||
|
|
||||||
|
badgeList.innerHTML = "";
|
||||||
|
earnedBadges.forEach(b => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.textContent = `🏅 ${b}`;
|
||||||
|
badgeList.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
badgeSection.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
document.getElementById("badgeDisplay")?.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔁 Retry only the missed words
|
// 🔁 Retry only the missed words
|
||||||
@@ -136,10 +161,6 @@ function retryMissed(missedWords) {
|
|||||||
results = [];
|
results = [];
|
||||||
|
|
||||||
document.getElementById('summary')?.classList.add('hidden');
|
document.getElementById('summary')?.classList.add('hidden');
|
||||||
|
document.getElementById('badgeDisplay')?.classList.add('hidden');
|
||||||
showWord();
|
showWord();
|
||||||
}
|
}
|
||||||
|
|
||||||
// added remove later
|
|
||||||
console.log("showStudent fired");
|
|
||||||
// added remove later
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,8 @@ if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR);
|
|||||||
const files = {
|
const files = {
|
||||||
users: 'users.json',
|
users: 'users.json',
|
||||||
wordlists: 'wordlists.json',
|
wordlists: 'wordlists.json',
|
||||||
results: 'results.json'
|
results: 'results.json',
|
||||||
|
badges: 'badges.json'
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const file of Object.values(files)) {
|
for (const file of Object.values(files)) {
|
||||||
@@ -164,3 +165,30 @@ app.get('/getUsers', (req, res) => {
|
|||||||
const users = readJsonSafe(path.join(DATA_DIR, files.users), []);
|
const users = readJsonSafe(path.join(DATA_DIR, files.users), []);
|
||||||
res.json(users);
|
res.json(users);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const badgePath = path.join(DATA_DIR, files.badges);
|
||||||
|
app.post('/awardBadges', (req, res) => {
|
||||||
|
const { username, badges } = req.body;
|
||||||
|
if (
|
||||||
|
typeof username !== 'string' ||
|
||||||
|
!Array.isArray(badges)
|
||||||
|
) return res.status(400).send("Invalid badge format");
|
||||||
|
|
||||||
|
const allBadges = readJsonSafe(badgePath);
|
||||||
|
if (!allBadges[username]) allBadges[username] = [];
|
||||||
|
|
||||||
|
badges.forEach(b => {
|
||||||
|
if (!allBadges[username].includes(b)) {
|
||||||
|
allBadges[username].push(b);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(badgePath, JSON.stringify(allBadges, null, 2));
|
||||||
|
res.send(`🎉 Badges updated for ${username}`);
|
||||||
|
});
|
||||||
|
app.get('/getBadges', (req, res) => {
|
||||||
|
const badgePath = path.join(DATA_DIR, files.badges);
|
||||||
|
const badges = readJsonSafe(badgePath);
|
||||||
|
res.json(badges);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user