Fix v1.1.0: Analytics, Sessions, and Error Handling Improvements

This commit is contained in:
nate@necloud.us committed 2025-08-13 19:02:55 -04:00
1 parent 7d8470df84
commit 545cf4c8dc
200 files changed
+91013 -18521

No files matched your search

+301
View File
@@ -0,0 +1,301 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Test Page</title>
<link rel="stylesheet" href="styles.css">
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.button-row {
display: flex;
gap: 10px;
margin-bottom: 1rem;
}
pre {
background: var(--code-bg);
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<h1>API Test Page</h1>
<p>This page allows you to test the API endpoints for the Spelling Practice App.</p>
<div class="card">
<h2>GET /getResults</h2>
<p>Fetch all user sessions from the server.</p>
<div style="margin-bottom: 10px;">
<label for="filterUsername">Filter by Username:</label>
<select id="filterUsername">
<option value="">All Users</option>
<option value="admin1">admin1</option>
<option value="student1">student1</option>
<option value="nate">nate</option>
<option value="testuser">testuser</option>
</select>
<label for="limitResults" style="margin-left: 15px;">Limit Results:</label>
<input type="number" id="limitResults" value="5" min="1" max="100" style="width: 60px;">
<label style="margin-left: 15px;">
<input type="checkbox" id="recentFirst" checked> Recent First
</label>
</div>
<div class="button-row">
<button id="testGetResults" class="btn-primary">Test Endpoint</button>
</div>
<div id="getResultsResponse" class="hidden">
<h3>Response:</h3>
<pre id="getResultsResponseData"></pre>
</div>
</div>
<div class="card">
<h2>POST /saveResults</h2>
<p>Save a new test session.</p>
<div>
<label for="username">Username:</label>
<select id="username">
<option value="admin1">admin1</option>
<option value="student1">student1</option>
<option value="nate">nate</option>
<option value="testuser">testuser</option>
</select>
</div>
<div>
<label for="score">Score:</label>
<input type="number" id="score" value="90" min="0" max="100">
</div>
<div>
<label for="wordCount">Word Count:</label>
<input type="number" id="wordCount" value="10" min="1" max="50">
</div>
<div class="button-row">
<button id="testSaveResults" class="btn-primary">Save New Session</button>
</div>
<div id="saveResultsResponse" class="hidden">
<h3>Response:</h3>
<pre id="saveResultsResponseData"></pre>
</div>
</div>
<div class="card">
<h2>Test Results</h2>
<p>View saved sessions for a specific user.</p>
<div>
<label for="viewUsername">Username:</label>
<select id="viewUsername">
<option value="admin1">admin1</option>
<option value="student1">student1</option>
<option value="nate">nate</option>
<option value="testuser">testuser</option>
</select>
</div>
<div class="button-row">
<button id="viewUserSessions" class="btn-primary">View Sessions</button>
</div>
<div id="userSessionsContainer" class="hidden">
<h3>Sessions for <span id="displayUsername"></span>:</h3>
<ul id="userSessionsList"></ul>
</div>
</div>
</div>
<script>
// Test getResults endpoint
document.getElementById('testGetResults').addEventListener('click', async () => {
try {
const username = document.getElementById('filterUsername').value;
const limit = document.getElementById('limitResults').value;
const recent = document.getElementById('recentFirst').checked;
// Build the URL with query parameters
let url = '/getResults';
const params = [];
if (username) params.push(`username=${encodeURIComponent(username)}`);
if (limit) params.push(`limit=${encodeURIComponent(limit)}`);
if (recent) params.push(`recent=true`);
if (params.length > 0) url += '?' + params.join('&');
const startTime = performance.now();
const response = await fetch(url);
const data = await response.json();
const endTime = performance.now();
const responseArea = document.getElementById('getResultsResponse');
const responseData = document.getElementById('getResultsResponseData');
responseArea.classList.remove('hidden');
// Format the response data
const userCount = Object.keys(data).length;
let sessionCount = 0;
Object.values(data).forEach(sessions => {
if (Array.isArray(sessions)) {
sessionCount += sessions.length;
}
});
const summaryText = `⏱️ Response time: ${Math.round(endTime - startTime)}ms\n` +
`👥 Users found: ${userCount}\n` +
`📊 Total sessions: ${sessionCount}\n` +
`🔍 Filters: ${params.join(', ') || 'None'}\n\n` +
`📝 First 500 chars of raw data:\n` +
JSON.stringify(data, null, 2).substring(0, 500) + '...';
responseData.textContent = summaryText;
} catch (error) {
console.error('Error testing getResults endpoint:', error);
document.getElementById('getResultsResponse').classList.remove('hidden');
document.getElementById('getResultsResponseData').textContent = `❌ Error: ${error.message}`;
}
});
// Test saveResults endpoint
document.getElementById('testSaveResults').addEventListener('click', async () => {
try {
const username = document.getElementById('username').value;
const score = parseInt(document.getElementById('score').value);
const wordCount = parseInt(document.getElementById('wordCount').value);
// Generate random words for the test
const words = [];
const possibleWords = ['apple', 'banana', 'cherry', 'date', 'elderberry',
'fig', 'grape', 'honeydew', 'kiwi', 'lemon',
'mango', 'orange', 'papaya', 'quince', 'raspberry'];
// Calculate how many should be correct based on the score
const correctCount = Math.round((score / 100) * wordCount);
for (let i = 0; i < wordCount; i++) {
const wordIndex = Math.floor(Math.random() * possibleWords.length);
words.push({
word: possibleWords[wordIndex],
correct: i < correctCount // Make first correctCount words correct
});
}
const payload = {
username,
result: {
score: correctCount,
answers: words,
completed: true
}
};
const startTime = performance.now();
const response = await fetch('/saveResults', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.text();
const endTime = performance.now();
const responseArea = document.getElementById('saveResultsResponse');
const responseData = document.getElementById('saveResultsResponseData');
responseArea.classList.remove('hidden');
responseData.textContent = `⏱️ Response time: ${Math.round(endTime - startTime)}ms\n` +
`📊 Response: ${data}\n\n` +
`📝 Request payload:\n` +
JSON.stringify(payload, null, 2);
// Auto-update the view after saving
if (document.getElementById('viewUsername').value === username) {
document.getElementById('viewUserSessions').click();
}
} catch (error) {
console.error('Error testing saveResults endpoint:', error);
document.getElementById('saveResultsResponse').classList.remove('hidden');
document.getElementById('saveResultsResponseData').textContent = `❌ Error: ${error.message}`;
}
});
// View user sessions
document.getElementById('viewUserSessions').addEventListener('click', async () => {
try {
const username = document.getElementById('viewUsername').value;
document.getElementById('displayUsername').textContent = username;
const response = await fetch('/getResults');
const data = await response.json();
const sessions = data[username] || [];
const sessionsList = document.getElementById('userSessionsList');
sessionsList.innerHTML = '';
if (sessions.length === 0) {
sessionsList.innerHTML = '<li>No sessions found for this user.</li>';
} else {
sessions.forEach((session, index) => {
// Support both timestamp and date fields
const sessionDate = session.timestamp || session.date || Date.now();
const date = new Date(sessionDate).toLocaleString();
// Handle different score fields
const score = session.score || session.correct || 0;
// Handle different word count fields
let total = 0;
if (session.answers && Array.isArray(session.answers)) {
total = session.answers.length;
} else if (session.words && Array.isArray(session.words)) {
total = session.words.length;
} else if (session.total) {
total = session.total;
}
const percent = total > 0 ? Math.round((score / total) * 100) : 0;
const li = document.createElement('li');
li.className = 'card';
li.innerHTML = `
<div>
<strong>${date}</strong>
<div>Score: ${score}/${total} (${percent}%)</div>
</div>
<div>
<details>
<summary>View Details</summary>
<pre>${JSON.stringify(session, null, 2)}</pre>
</details>
</div>
`;
sessionsList.appendChild(li);
});
}
document.getElementById('userSessionsContainer').classList.remove('hidden');
} catch (error) {
console.error('Error viewing user sessions:', error);
document.getElementById('userSessionsContainer').classList.remove('hidden');
document.getElementById('userSessionsList').innerHTML = `<li>❌ Error: ${error.message}</li>`;
}
});
</script>
</body>
</html>
+55
View File
@@ -57,6 +57,61 @@
font-weight: 700;
}
.badge-showcase {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.badge-item {
display: flex;
background-color: var(--bg-card, #f5f5f5);
border-radius: 12px;
padding: 1rem;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
width: 100%;
max-width: 450px;
animation: badge-popup 0.5s forwards;
align-items: center;
}
@keyframes badge-popup {
0% { transform: scale(0.8); opacity: 0; }
70% { transform: scale(1.05); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
.badge-icon {
font-size: 3rem;
margin-right: 1rem;
padding: 0.5rem;
background-color: rgba(255,255,255,0.2);
border-radius: 50%;
width: 4.5rem;
height: 4.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.badge-info {
flex: 1;
}
.badge-info h4 {
margin-top: 0;
margin-bottom: 0.5rem;
color: var(--text-primary, #333);
}
.badge-info p {
margin: 0;
font-size: 0.9rem;
color: var(--text-secondary, #666);
}
.badge-count {
display: inline-block;
background: linear-gradient(135deg, #ff9a8b 0%, #ff6a88 55%, #ff99ac 100%);
View File
Whitespace-only changes.
+45 -5
View File
@@ -21,12 +21,20 @@
<script src="js/bible.js"></script>
<script src="js/charts.js"></script>
<script src="js/export.js"></script>
<script src="js/session-helpers.js"></script>
<script src="js/sessions.js"></script>
<script src="js/admin-badges.js"></script>
<script src="js/analytics.js"></script>
<script src="js/gamification.js"></script>
<script src="js/spaced-repetition.js"></script>
<script src="js/main.js"></script>
<!-- Initialization script - loads last -->
<script src="js/chart-safety.js"></script>
<script src="js/user-dropdown-health.js"></script>
<script src="js/input-fix.js"></script>
<script src="js/init.js"></script>
<!-- Production scripts only -->
<script src="js/cleanup.js"></script>
</head>
<body>
<div class="container">
@@ -180,7 +188,7 @@
<option value="month">Last Month</option>
<option value="quarter">Last 3 Months</option>
</select>
<button onclick="refreshAnalytics()" class="btn-primary">🔄 Refresh Analytics</button>
<button onclick="window.analyticsRefresh ? window.analyticsRefresh() : refreshAnalytics()" class="btn-primary">🔄 Refresh Analytics</button>
</div>
</div>
@@ -548,14 +556,46 @@
</div>
<div class="word-box" id="wordBox"></div>
<div class="hidden" id="inputSection" style="text-align: center;">
<input id="userInput" placeholder="Type the word you saw..." type="text" style="max-width: 400px;"/>
<input id="userInput" placeholder="Type the word you saw..." type="text"
style="max-width: 400px;
color: black !important;
background-color: white !important;
border: 2px solid #333 !important;
font-size: 18px !important;
padding: 12px !important;
visibility: visible !important;
opacity: 1 !important;
text-shadow: none !important;"/>
<button onclick="submitAnswer()" class="btn-primary">Submit Answer</button>
<div id="wordFeedback" style="margin-top: 10px; font-weight: bold; min-height: 24px;"></div>
</div>
<!-- Game Summary -->
<div class="hidden" id="summary" class="card"></div>
<div id="summary" class="card hidden">
<h3>Game Results</h3>
<div class="summary-stats">
<div class="stat-item">
<label>Total Words:</label>
<span id="totalWords">0</span>
</div>
<div class="stat-item">
<label>Correct Words:</label>
<span id="correctWords">0</span>
</div>
<div class="stat-item">
<label>Accuracy:</label>
<span id="accuracy">0%</span>
</div>
</div>
<h4>Incorrect Words</h4>
<ul id="incorrectList" class="incorrect-list"></ul>
<div class="summary-actions">
<button onclick="returnToStudentPanel()" class="btn-secondary">Return to Dashboard</button>
<button onclick="startGame()" class="btn-primary">Try Again</button>
</div>
</div>
<!-- Enhanced Badge Display -->
<div class="hidden" id="badgeDisplay" class="card badge-celebration">
<h2>� Achievement Unlocked! 🎊</h2>
<div id="badgeDisplay" class="hidden card badge-celebration">
<h2>🎊 Achievement Unlocked! 🎊</h2>
<div id="badgeList" class="badge-showcase"></div>
<div class="badge-actions">
<button onclick="document.getElementById('badgeDisplay').classList.add('hidden')" class="btn-secondary">Continue</button>
+173 -35
View File
@@ -14,6 +14,7 @@ window.addEventListener('DOMContentLoaded', () => {
}, 0);
});
// Global variables for admin functionality
window.showDebugInfo = true; // Enable additional logging
// --- Weeks input logic ---
// Populate active week dropdown from weeksContainer
window.populateActiveWeekDropdown = function() {
@@ -92,7 +93,8 @@ window.clearWeeks = function() {
document.getElementById('weeksContainer').innerHTML = '';
}
let allWords = [];
let adminUsers = [];
// Make adminUsers available globally
window.adminUsers = [];
// 🧭 Admin tab switcher (make it globally accessible)
window.switchTab = function switchTab(targetId) {
@@ -176,13 +178,29 @@ window.switchTab = function switchTab(targetId) {
};
// Load users from server
async function loadUsers() {
window.loadUsers = async function() {
try {
console.log("📋 Loading users from server...");
const res = await fetch('/getUsers');
adminUsers = await res.json();
console.log("GET /getUsers response:", res.status, res.statusText);
if (!res.ok) {
throw new Error(`Server returned ${res.status}: ${res.statusText}`);
}
const data = await res.json();
console.log(`📋 Loaded ${data.length} users:`, data.map(u => u.username));
window.adminUsers = data;
// Populate all user dropdowns immediately
window.displayUserDropdown();
window.displayUserLists();
return adminUsers;
} catch (err) {
console.error("Failed to load users:", err);
adminUsers = [];
console.error("❌ Failed to load users:", err);
window.adminUsers = [];
return [];
}
}
@@ -224,14 +242,84 @@ async function addNewUser() {
}
// 🗑 Populate and delete users
function displayUserDropdown() {
const dropdown = document.getElementById("userDropdown");
dropdown.innerHTML = `<option value="">-- Select a user --</option>`;
adminUsers.forEach(user => {
dropdown.innerHTML += `<option value="${user.username}">${user.username} (${user.role})</option>`;
window.displayUserDropdown = function() {
console.log("📋 Populating user dropdowns...");
// Get all user dropdowns in the application
const userSelectors = [
"userDropdown",
"wordUserSelect",
"passwordUsername",
"reportUser",
"analyticsUserSelect" // Also update analytics dropdown
];
userSelectors.forEach(id => {
const dropdown = document.getElementById(id);
if (!dropdown) {
console.log(`⚠️ Dropdown with ID "${id}" not found`);
return;
}
// Save currently selected value if any
const currentValue = dropdown.value;
// Clear existing options - keep first option if it exists
const firstOption = dropdown.options.length > 0 ? dropdown.options[0].cloneNode(true) : null;
dropdown.innerHTML = '';
if (firstOption && (firstOption.value === '' || firstOption.value === 'all')) {
dropdown.appendChild(firstOption);
} else {
dropdown.innerHTML = `<option value="">-- Select a user --</option>`;
}
// Add users to dropdown
if (Array.isArray(window.adminUsers) && window.adminUsers.length > 0) {
window.adminUsers.forEach(user => {
const option = document.createElement("option");
option.value = user.username;
option.textContent = `${user.username} (${user.role})`;
dropdown.appendChild(option);
});
console.log(`✅ Added ${window.adminUsers.length} users to dropdown ${id}`);
// Restore previously selected value if it exists in the new options
if (currentValue && [...dropdown.options].some(opt => opt.value === currentValue)) {
dropdown.value = currentValue;
}
} else {
console.warn(`⚠️ No users to populate in dropdown ${id}`);
}
});
}
// Display users in list format
function displayUserLists() {
console.log("📋 Updating user lists in tables...");
const userTableBody = document.getElementById("userTableBody");
if (userTableBody) {
userTableBody.innerHTML = '';
if (Array.isArray(adminUsers) && adminUsers.length > 0) {
adminUsers.forEach((user, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${index + 1}</td>
<td>${user.username}</td>
<td>${user.role}</td>
<td>
<button class="btn-sm btn-danger" onclick="selectUserAction('${user.username}')">🗑️</button>
</td>
`;
userTableBody.appendChild(row);
});
} else {
userTableBody.innerHTML = `<tr><td colspan="4" class="text-center">No users found</td></tr>`;
}
}
}
function selectUserAction(username) {
if (!username) return;
@@ -461,9 +549,11 @@ async function loadUserDropdowns() {
// Analytics functionality has been moved to analytics.js
// This is now just a redirecting function
async function refreshAnalytics() {
// The implementation is in analytics.js
if (typeof window.refreshAnalytics === 'function') {
return window.refreshAnalytics();
// To avoid circular references, we'll call the function by its name in analytics.js
console.log("Trying to call analyticsRefresh from window object...");
if (typeof window.analyticsRefresh === 'function') {
console.log("Found analyticsRefresh in window, calling it...");
return window.analyticsRefresh();
} else {
console.log("Loading legacy analytics implementation...");
try {
@@ -778,15 +868,26 @@ window.loadStudentReport = async function(username) {
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`;
// Safe access to score and answers, handling different data formats
const score = latestReport.score || latestReport.correct || 0;
// Handle both answers array and words array formats
let answers = [];
if (latestReport.answers && Array.isArray(latestReport.answers)) {
answers = latestReport.answers;
} else if (latestReport.words && Array.isArray(latestReport.words)) {
answers = latestReport.words;
}
const total = answers.length || latestReport.total || 0;
document.getElementById("reportScore").textContent = `${score}/${total} correct`;
const wordList = document.getElementById("reportWords");
wordList.innerHTML = "";
if (answers.length > 0) {
answers.forEach(({ word, correct }) => {
answers.forEach((answer) => {
const word = answer.word || '';
const correct = answer.correct || false;
const li = document.createElement("li");
li.textContent = `${word} — ${correct ? '✅' : '❌'}`;
wordList.appendChild(li);
@@ -871,27 +972,64 @@ async function populateSessionUserDropdown() {
}
async function loadUserSessions(username) {
const res = await fetch('/getResults');
const allResults = await res.json();
// Delegate to the window.loadUserSessions function in sessions.js
// This ensures we have consistent behavior
if (typeof window.loadUserSessions === 'function') {
window.loadUserSessions(username);
} else {
console.error("Error: window.loadUserSessions function not found");
// Fallback implementation
const res = await fetch('/getResults');
const allResults = await res.json();
const userSessions = allResults[username] || [];
const list = document.getElementById("userSessionList");
list.innerHTML = "";
const userSessions = allResults[username] || [];
const list = document.getElementById("userSessionList");
list.innerHTML = "";
if (userSessions.length === 0) {
list.innerHTML = '<li style="padding: 1rem; text-align: center;">No sessions found.</li>';
return;
}
userSessions.forEach(({ score, answers, timestamp }) => {
const sessionItem = document.createElement("li");
const dateStr = new Date(timestamp).toLocaleString();
const total = answers.length;
userSessions.forEach((session) => {
const sessionItem = document.createElement("li");
// Support both timestamp and date fields
const sessionDate = session.timestamp || session.date || Date.now();
const dateStr = new Date(sessionDate).toLocaleString();
// Handle various score formats
const score = session.score || session.correct || 0;
// Handle different answer formats
let answersArray = [];
let total = 0;
if (session.answers && Array.isArray(session.answers)) {
answersArray = session.answers;
total = answersArray.length;
} else if (session.words && Array.isArray(session.words)) {
answersArray = session.words;
total = answersArray.length;
} else if (session.total) {
total = session.total;
}
let html = `<p>🗓️ <strong>${dateStr}</strong> — ${score}/${total} correct</p><ul>`;
answers.forEach(({ word, correct }) => {
html += `<li>${correct ? '✅' : '❌'} ${word}</li>`;
let html = `<p>🗓️ <strong>${dateStr}</strong> — ${score}/${total} correct</p><ul>`;
answersArray.forEach((answer) => {
const word = answer.word || '';
const correct = answer.correct || false;
html += `<li>${correct ? '✅' : '❌'} ${word}</li>`;
});
html += "</ul>";
sessionItem.innerHTML = html;
list.appendChild(sessionItem);
});
html += "</ul>";
sessionItem.innerHTML = html;
list.appendChild(sessionItem);
});
}
}
// File upload handler for word lists
+252 -53
View File
@@ -27,7 +27,9 @@ document.addEventListener('DOMContentLoaded', function() {
});
// Main function to refresh analytics
async function refreshAnalytics() {
async function analyticsRefresh() {
// This is the main implementation
console.log('📊 Refreshing analytics data...');
try {
// Update UI to show loading state
updateLoadingState(true);
@@ -40,8 +42,8 @@ async function refreshAnalytics() {
analyticsData = await response.json();
// Populate user dropdown
populateUserDropdown();
// Populate user dropdown (now async)
await populateUserDropdown();
// Render the analytics dashboard with the new data
renderAnalyticsDashboard();
@@ -89,23 +91,109 @@ function updateLoadingState(isLoading) {
}
// Populate user dropdown with available users
function populateUserDropdown() {
async function populateUserDropdown() {
console.log('🔄 Populating analytics user dropdown...');
const userSelect = document.getElementById('analyticsUserSelect');
if (!userSelect || !analyticsData) return;
// Clear existing options except the first one (All Users)
while (userSelect.options.length > 1) {
userSelect.remove(1);
if (!userSelect) {
console.error('⚠️ Analytics user select dropdown not found in the DOM!');
return;
}
// Add all users from the analytics data
const users = Object.keys(analyticsData.userStats || {});
users.forEach(username => {
const option = document.createElement('option');
option.value = username;
option.textContent = username;
userSelect.appendChild(option);
});
// Check if we can use the shared admin users data
if (typeof window.adminUsers !== 'undefined' && Array.isArray(window.adminUsers) && window.adminUsers.length > 0) {
console.log('✅ Using shared admin users data:', window.adminUsers.map(u => u.username));
// Clear existing options except the first one (All Users)
console.log('Clearing existing options...');
while (userSelect.options.length > 1) {
userSelect.remove(1);
}
// Add users to dropdown
window.adminUsers.forEach(user => {
const option = document.createElement('option');
option.value = user.username;
option.textContent = user.username;
userSelect.appendChild(option);
});
console.log(`✅ Added ${window.adminUsers.length} users to analytics dropdown`);
return;
}
// If we can't use shared data, fetch from server
try {
// Clear existing options except the first one (All Users)
console.log('Clearing existing options...');
while (userSelect.options.length > 1) {
userSelect.remove(1);
}
// Always fetch fresh users from server
console.log("📥 Fetching users directly from server...");
// Add a loading option
const loadingOption = document.createElement('option');
loadingOption.value = "";
loadingOption.textContent = "Loading users...";
loadingOption.disabled = true;
userSelect.appendChild(loadingOption);
const response = await fetch('/getUsers');
console.log("GET /getUsers response:", response.status, response.statusText);
// Remove loading option
if (userSelect.contains(loadingOption)) {
userSelect.removeChild(loadingOption);
}
if (!response.ok) {
throw new Error(`Server returned ${response.status}: ${response.statusText}`);
}
const responseText = await response.text();
console.log("Raw server response:", responseText.substring(0, 100) + (responseText.length > 100 ? '...' : ''));
let users = [];
try {
const allUsers = JSON.parse(responseText);
console.log("Parsed users data:", allUsers);
if (Array.isArray(allUsers)) {
users = allUsers.map(user => user.username);
console.log(`✅ Loaded ${users.length} users from server:`, users);
if (users.length === 0) {
console.warn("⚠️ Server returned empty users array!");
// Add a warning option
const warningOption = document.createElement('option');
warningOption.value = "";
warningOption.textContent = "No users found";
warningOption.disabled = true;
userSelect.appendChild(warningOption);
}
} else {
console.error("❌ Server returned non-array data:", allUsers);
throw new Error("Invalid user data format");
}
} catch (jsonError) {
console.error("❌ Error parsing JSON response:", jsonError);
throw jsonError;
}
// Add users to dropdown
console.log(`Adding ${users.length} users to dropdown`);
users.forEach(username => {
const option = document.createElement('option');
option.value = username;
option.textContent = username;
userSelect.appendChild(option);
});
console.log('✅ User dropdown populated successfully');
} catch (error) {
console.error("❌ Error populating user dropdown:", error);
}
}
// Render the entire analytics dashboard
@@ -295,35 +383,101 @@ function renderProgressChart(data) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', width);
svg.setAttribute('height', height);
svg.style.overflow = 'visible';
svg.style.overflow = 'hidden';
container.appendChild(svg);
// Scale functions
const xScale = index => (index / (trendData.length - 1 || 1)) * chartWidth + padding.left;
const yScale = value => chartHeight - (value / 100) * chartHeight + padding.top;
// Create a clipping path to ensure chart doesn't go outside boundaries
const svgDefs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
clipPath.setAttribute('id', 'progress-chart-area');
const clipRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
clipRect.setAttribute('x', padding.left);
clipRect.setAttribute('y', padding.top);
clipRect.setAttribute('width', chartWidth);
clipRect.setAttribute('height', chartHeight);
clipPath.appendChild(clipRect);
svgDefs.appendChild(clipPath);
// Add a group for all chart elements that should be clipped
const chartGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
chartGroup.setAttribute('clip-path', 'url(#progress-chart-area)');
svg.appendChild(chartGroup);
svg.appendChild(svgDefs);
// Scale functions with boundary checks
const xScale = index => {
// Ensure index is within bounds
const safeIndex = Math.max(0, Math.min(index, trendData.length - 1));
return (safeIndex / (trendData.length - 1 || 1)) * chartWidth + padding.left;
};
const yScale = value => {
// Ensure score is within 0-100 range
const safeValue = Math.max(0, Math.min(value, 100));
return chartHeight - (safeValue / 100) * chartHeight + padding.top;
};
// Smooth out the data to prevent sudden drops
const smoothedData = [...trendData];
// Apply simple moving average to smooth out sudden drops
if (smoothedData.length > 2) {
for (let i = 1; i < smoothedData.length - 1; i++) {
const prevAccuracy = smoothedData[i-1].accuracy || 0;
const currAccuracy = smoothedData[i].accuracy || 0;
const nextAccuracy = smoothedData[i+1].accuracy || 0;
// If there's a large drop (over 30%), smooth it out
if (currAccuracy < prevAccuracy - 30 && currAccuracy < nextAccuracy - 30) {
smoothedData[i].accuracy = (prevAccuracy + nextAccuracy) / 2;
}
}
}
// Create path for the line
const pathData = trendData.map((point, i) => {
const pathData = smoothedData.map((point, i) => {
// Ensure we have valid coordinates
const x = xScale(i);
const y = yScale(point.accuracy || 0);
return `${i === 0 ? 'M' : 'L'} ${x} ${y}`;
}).join(' ');
// Skip invalid coordinates
if (isNaN(x) || isNaN(y)) {
console.warn('Invalid coordinate for progress chart:', { x, y, point, i });
return '';
}
return `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`;
}).filter(segment => segment !== '').join(' ');
// Add the path
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pathData);
path.setAttribute('stroke', 'var(--primary)');
path.setAttribute('stroke-width', '3');
path.setAttribute('fill', 'none');
svg.appendChild(path);
// Add area under the curve
const areaPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const areaPathData = `${pathData} L ${xScale(trendData.length - 1)} ${chartHeight + padding.top} L ${xScale(0)} ${chartHeight + padding.top} Z`;
areaPath.setAttribute('d', areaPathData);
areaPath.setAttribute('fill', 'var(--primary)');
areaPath.setAttribute('opacity', '0.1');
svg.appendChild(areaPath);
// Only render if we have valid path data
if (pathData && pathData.length > 0 && !pathData.includes('NaN')) {
// Add the path
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pathData);
path.setAttribute('stroke', 'var(--primary)');
path.setAttribute('stroke-width', '3');
path.setAttribute('fill', 'none');
chartGroup.appendChild(path);
// Add area under the curve
try {
const areaPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const lastValidIndex = trendData.length - 1;
const areaPathData = `${pathData} L ${xScale(lastValidIndex).toFixed(1)} ${(chartHeight + padding.top).toFixed(1)} L ${xScale(0).toFixed(1)} ${(chartHeight + padding.top).toFixed(1)} Z`;
if (areaPathData && !areaPathData.includes('NaN')) {
areaPath.setAttribute('d', areaPathData);
areaPath.setAttribute('fill', 'var(--primary)');
areaPath.setAttribute('opacity', '0.1');
chartGroup.appendChild(areaPath);
}
} catch (error) {
console.error('Error creating area path:', error);
}
} else {
console.warn('Unable to create valid path data for progress chart');
}
// Add data points
trendData.forEach((point, i) => {
@@ -341,7 +495,7 @@ function renderProgressChart(data) {
circle.addEventListener('mouseenter', showTooltip);
circle.addEventListener('mouseleave', hideTooltip);
svg.appendChild(circle);
chartGroup.appendChild(circle);
});
// Add axes
@@ -417,8 +571,25 @@ function showTooltip(event) {
// Position the tooltip
const rect = this.getBoundingClientRect();
tooltip.style.left = `${rect.left}px`;
tooltip.style.top = `${rect.top - tooltip.offsetHeight - 10}px`;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Calculate initial position
let left = rect.left;
let top = rect.top - tooltip.offsetHeight - 10;
// Adjust to keep tooltip within viewport
if (left + tooltip.offsetWidth > viewportWidth) {
left = viewportWidth - tooltip.offsetWidth - 10;
}
if (top < 0) {
// If not enough space above, position below
top = rect.bottom + 10;
}
tooltip.style.left = `${Math.max(10, left)}px`;
tooltip.style.top = `${Math.max(10, top)}px`;
// Show the tooltip
tooltip.style.opacity = '1';
@@ -809,18 +980,42 @@ function createSparkline(container, trendData) {
const xScale = index => (index / (trendData.length - 1)) * width;
const yScale = accuracy => height - (accuracy / 100) * height;
// Create path for the line
const pathData = trendData.map((point, i) => {
return `${i === 0 ? 'M' : 'L'} ${xScale(i)} ${yScale(point.accuracy || 0)}`;
}).join(' ');
// Create path for the line with validation to prevent NaN values
const validPoints = trendData.filter((point) => {
// Ensure we have valid accuracy data
return point && typeof point.accuracy === 'number' && !isNaN(point.accuracy);
});
// Add the path
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pathData);
path.setAttribute('stroke', 'var(--accent)');
path.setAttribute('stroke-width', '2');
path.setAttribute('fill', 'none');
svg.appendChild(path);
if (validPoints.length > 0) {
// Create path only with valid data points
const pathData = validPoints.map((point, i) => {
// Ensure values are finite numbers
const x = xScale(i);
const y = yScale(Math.max(0, Math.min(100, point.accuracy || 0)));
// Verify coordinates are valid numbers
if (isNaN(x) || isNaN(y)) {
console.warn('Invalid coordinate:', { x, y, point });
return '';
}
return `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`;
}).filter(segment => segment !== '').join(' ');
// Only add the path if we have valid path data
if (pathData && !pathData.includes('NaN')) {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pathData);
path.setAttribute('stroke', 'var(--accent)');
path.setAttribute('stroke-width', '2');
path.setAttribute('fill', 'none');
svg.appendChild(path);
} else {
console.warn('Unable to create valid path data:', pathData);
}
} else {
console.warn('No valid data points for trend chart');
}
}
// Render recent activity feed
@@ -903,3 +1098,7 @@ function formatTimeAgo(date) {
return 'just now';
}
// Export functions to window object
window.analyticsRefresh = analyticsRefresh;
window.populateUserDropdown = populateUserDropdown;
+83
View File
@@ -0,0 +1,83 @@
/**
* Enhanced error handling for charts in analytics.js
* This will wrap critical chart functions with try/catch blocks
*/
// Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', function() {
// Add a global error handler for SVG operations
window.addEventListener('error', function(event) {
// Check if error is related to SVG path
if (event.message && (event.message.includes('attribute d') ||
event.message.includes('SVG') ||
event.message.includes('path'))) {
console.error('SVG Error intercepted:', event.message);
event.preventDefault(); // Prevent the error from appearing in console
// Try to find the affected chart container
const containers = [
'progressChartContainer',
'activityHeatmap',
'problemWordsList',
'userComparisonChart'
];
containers.forEach(id => {
const container = document.getElementById(id);
if (container) {
// Add error message to container
const errorMsg = document.createElement('div');
errorMsg.className = 'chart-error';
errorMsg.textContent = 'Error rendering chart. Try refreshing.';
errorMsg.style.color = 'var(--error)';
errorMsg.style.padding = '1rem';
errorMsg.style.textAlign = 'center';
// Clear container and show error
container.innerHTML = '';
container.appendChild(errorMsg);
}
});
return true;
}
return false;
});
// Try to monkey patch the chart rendering functions if they exist
const safeExec = function(fn, fallbackMsg) {
return function(...args) {
try {
return fn.apply(this, args);
} catch (error) {
console.error(`Chart rendering error: ${error.message}`, error);
// Display fallback message if container is provided
if (args[1] && typeof args[1] === 'string') {
const container = document.getElementById(args[1]);
if (container) {
container.innerHTML = `<div class="chart-error">${fallbackMsg || 'Error rendering chart'}</div>`;
}
}
return null;
}
};
};
// Add the safety wrapper to existing functions once analytics.js is loaded
if (window.analyticsRefresh) {
console.log('📊 Adding safety wrappers to chart functions');
// Wait a bit to ensure all analytics functions are loaded
setTimeout(() => {
// Store references to original functions
const originalRenderDashboard = window.renderAnalyticsDashboard;
if (originalRenderDashboard) {
window.renderAnalyticsDashboard = safeExec(originalRenderDashboard,
'Unable to render analytics dashboard. Please try refreshing the page.');
console.log('✅ Added safety wrapper to renderAnalyticsDashboard');
}
}, 1000);
}
});
+37 -10
View File
@@ -25,9 +25,29 @@ function createScoreChart(username, results) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', width);
svg.setAttribute('height', height);
svg.style.overflow = 'visible';
svg.style.overflow = 'hidden';
chartContainer.appendChild(svg);
// Create a clipping path to ensure chart doesn't go outside boundaries
// Create all definitions in one defs element
const svgDefs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
clipPath.setAttribute('id', 'chart-area');
const clipRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
clipRect.setAttribute('x', padding.left);
clipRect.setAttribute('y', padding.top);
clipRect.setAttribute('width', chartWidth);
clipRect.setAttribute('height', chartHeight);
clipPath.appendChild(clipRect);
svgDefs.appendChild(clipPath);
// Add a group for all chart elements that should be clipped
const chartGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
chartGroup.setAttribute('clip-path', 'url(#chart-area)');
svg.appendChild(chartGroup);
svg.appendChild(svgDefs);
// Get data points
const dataPoints = recentResults.map((result, index) => {
const score = result.score || 0;
@@ -41,9 +61,17 @@ function createScoreChart(username, results) {
};
});
// Scale functions
const xScale = index => (index / (dataPoints.length - 1 || 1)) * chartWidth + padding.left;
const yScale = score => chartHeight - (score / 100) * chartHeight + padding.top;
// Scale functions with boundary checks
const xScale = index => {
// Ensure index is within bounds
const safeIndex = Math.max(0, Math.min(index, dataPoints.length - 1));
return (safeIndex / (dataPoints.length - 1 || 1)) * chartWidth + padding.left;
};
const yScale = score => {
// Ensure score is within 0-100 range
const safeScore = Math.max(0, Math.min(score, 100));
return chartHeight - (safeScore / 100) * chartHeight + padding.top;
};
// Create path for the line
const pathData = dataPoints.map((point, i) => {
@@ -56,7 +84,7 @@ function createScoreChart(username, results) {
path.setAttribute('stroke', 'var(--primary)');
path.setAttribute('stroke-width', '3');
path.setAttribute('fill', 'none');
svg.appendChild(path);
chartGroup.appendChild(path);
// Add area under the curve with gradient
const areaPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
@@ -84,10 +112,9 @@ function createScoreChart(username, results) {
gradient.appendChild(stop1);
gradient.appendChild(stop2);
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
defs.appendChild(gradient);
svg.appendChild(defs);
svg.appendChild(areaPath);
// Add gradient to the main SVG defs
svgDefs.appendChild(gradient);
chartGroup.appendChild(areaPath);
// Add data points
dataPoints.forEach((point, i) => {
@@ -105,7 +132,7 @@ function createScoreChart(username, results) {
circle.addEventListener('mouseover', showTooltip);
circle.addEventListener('mouseout', hideTooltip);
svg.appendChild(circle);
chartGroup.appendChild(circle);
});
// Add axes
+52
View File
@@ -0,0 +1,52 @@
/**
* Remove debug elements from the page
*/
(function() {
// Function to remove debug elements
function removeDebugElements() {
// List of possible debug element identifiers
const debugSelectors = [
'div[id*="debug"]',
'div[class*="debug"]',
'div[style*="zIndex: 9999"]',
'div:contains("Debug Tools")',
'button:contains("Check Dropdowns")',
'button:contains("Force Populate")',
'button:contains("Debug Input")',
'[data-debug="true"]'
];
// Try to use the selector directly
debugSelectors.forEach(selector => {
try {
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
if (el) el.remove();
});
} catch (err) {
// Just ignore errors in selectors
}
});
// Also try to find by content
document.querySelectorAll('div').forEach(div => {
if (div.textContent && (
div.textContent.includes('Debug Tools') ||
div.textContent.includes('Check Dropdowns') ||
div.textContent.includes('Force Populate')
)) {
div.remove();
}
});
}
// Run on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', removeDebugElements);
} else {
removeDebugElements();
}
// Also run periodically to catch dynamically added elements
setInterval(removeDebugElements, 1000);
})();
+115
View File
@@ -0,0 +1,115 @@
// Debug script to test the analytics page
// Save this file to public/js/debug-analytics.js
(function() {
console.log('🔍 Analytics Debug Helper loaded');
// Function to check user dropdowns
function checkUserDropdowns() {
console.log('Checking user dropdowns...');
const selectors = [
'analyticsUserSelect',
'userDropdown',
'wordUserSelect',
'passwordUsername',
'reportUser'
];
selectors.forEach(id => {
const dropdown = document.getElementById(id);
if (dropdown) {
console.log(`✅ Found dropdown: ${id}`);
console.log(` - Options: ${dropdown.options.length}`);
console.log(` - Values: ${Array.from(dropdown.options).map(o => o.value).join(', ')}`);
} else {
console.log(`❌ Dropdown not found: ${id}`);
}
});
}
// Function to manually populate the analytics dropdown
function populateAnalyticsDropdown() {
console.log('🔄 Manually populating analytics dropdown...');
const analyticsDropdown = document.getElementById('analyticsUserSelect');
if (!analyticsDropdown) {
console.error('❌ Analytics dropdown not found');
return;
}
// Clear existing options except the first (All Users)
while (analyticsDropdown.options.length > 1) {
analyticsDropdown.remove(1);
}
// Fetch users directly
fetch('/getUsers')
.then(response => {
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
return response.json();
})
.then(users => {
console.log(`✅ Fetched ${users.length} users:`, users.map(u => u.username));
// Add users to dropdown
users.forEach(user => {
const option = document.createElement('option');
option.value = user.username;
option.textContent = user.username;
analyticsDropdown.appendChild(option);
});
console.log(`✅ Added ${users.length} users to analytics dropdown`);
})
.catch(error => {
console.error('❌ Error fetching users:', error);
});
}
// Add helper buttons
window.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
// Create debug control panel
const debugPanel = document.createElement('div');
debugPanel.style.position = 'fixed';
debugPanel.style.bottom = '10px';
debugPanel.style.right = '10px';
debugPanel.style.backgroundColor = 'rgba(0,0,0,0.7)';
debugPanel.style.padding = '10px';
debugPanel.style.borderRadius = '5px';
debugPanel.style.color = 'white';
debugPanel.style.zIndex = '9999';
debugPanel.style.fontSize = '12px';
// Add title
const title = document.createElement('div');
title.textContent = '🐞 Debug Tools';
title.style.fontWeight = 'bold';
title.style.marginBottom = '5px';
debugPanel.appendChild(title);
// Add check dropdowns button
const checkBtn = document.createElement('button');
checkBtn.textContent = '👁️ Check Dropdowns';
checkBtn.style.marginRight = '5px';
checkBtn.style.padding = '3px 6px';
checkBtn.addEventListener('click', checkUserDropdowns);
debugPanel.appendChild(checkBtn);
// Add populate button
const populateBtn = document.createElement('button');
populateBtn.textContent = '🔄 Force Populate';
populateBtn.style.padding = '3px 6px';
populateBtn.addEventListener('click', populateAnalyticsDropdown);
debugPanel.appendChild(populateBtn);
document.body.appendChild(debugPanel);
// Initial check after a delay
setTimeout(checkUserDropdowns, 2000);
}, 1000);
});
})();
+102
View File
@@ -0,0 +1,102 @@
// Add this script in the head element of index.html for temporary debugging
(() => {
// Store the original fetch method to monitor all network requests
const originalFetch = window.fetch;
// Override fetch to log all requests and responses
window.fetch = async function(url, options) {
console.log(`🔍 DEBUG: Fetch request to ${url}`, options);
try {
const response = await originalFetch(url, options);
// Clone the response to leave the original intact
const clone = response.clone();
// Log the response
console.log(`🔍 DEBUG: Fetch response from ${url}`, {
status: clone.status,
statusText: clone.statusText,
headers: Object.fromEntries([...clone.headers.entries()])
});
// If it's a JSON response, log the data
if (clone.headers.get('content-type')?.includes('application/json')) {
try {
const data = await clone.json();
console.log(`🔍 DEBUG: JSON response from ${url}`, data);
} catch (e) {
console.error(`🔍 DEBUG: Error parsing JSON from ${url}`, e);
}
}
return response;
} catch (error) {
console.error(`🔍 DEBUG: Fetch error for ${url}`, error);
throw error;
}
};
// Monitor all select elements for changes
const observeSelects = () => {
const selects = document.querySelectorAll('select');
console.log(`🔍 DEBUG: Found ${selects.length} select elements`);
selects.forEach((select, index) => {
console.log(`🔍 DEBUG: Select #${index}:`, {
id: select.id,
name: select.name,
optionCount: select.options.length,
options: Array.from(select.options).map(opt => ({
value: opt.value,
text: opt.text,
disabled: opt.disabled
}))
});
// Watch for changes in the options
const observer = new MutationObserver(mutations => {
console.log(`🔍 DEBUG: Select #${index} (${select.id}) changed:`, {
optionCount: select.options.length,
options: Array.from(select.options).map(opt => ({
value: opt.value,
text: opt.text,
disabled: opt.disabled
}))
});
});
observer.observe(select, { childList: true });
});
};
// Run once DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
console.log('🔍 DEBUG: DOM loaded, monitoring select elements');
setTimeout(observeSelects, 1000);
// Add click handler for analytics tab
const analyticsTab = document.querySelector('button[data-tab="tabAnalytics"]');
if (analyticsTab) {
const originalClick = analyticsTab.onclick;
analyticsTab.addEventListener('click', () => {
console.log('🔍 DEBUG: Analytics tab clicked');
setTimeout(() => {
const userSelect = document.getElementById('analyticsUserSelect');
if (userSelect) {
console.log('🔍 DEBUG: Analytics user select status:', {
optionCount: userSelect.options.length,
options: Array.from(userSelect.options).map(opt => ({
value: opt.value,
text: opt.text
}))
});
} else {
console.log('🔍 DEBUG: Analytics user select not found');
}
}, 500);
});
}
});
})();
+293 -329
View File
@@ -3,8 +3,10 @@ let words = [];
let currentWord = '';
let currentIndex = 0;
let results = [];
let typingWords = [];
let isSpacedRepetitionMode = false;
// 📝 Check user's answer and move to next
// 📝 Check user's answer and move to next word regardless of correctness
window.submitAnswer = function() {
const input = document.getElementById('userInput').value.trim();
@@ -14,22 +16,41 @@ window.submitAnswer = function() {
attempt: input
});
// Show feedback and move to next word regardless of correctness
if (input.toLowerCase() === currentWord.toLowerCase()) {
currentIndex++;
document.getElementById('inputSection').classList.add('hidden');
setTimeout(() => showWord(), 500); // Brief pause before next word
// Correct answer - add visual feedback
const inputEl = document.getElementById('userInput');
inputEl.style.borderColor = 'var(--success)';
inputEl.style.backgroundColor = '#f0fdf4';
// Add feedback text if element exists
const feedbackEl = document.getElementById('wordFeedback');
if (feedbackEl) {
feedbackEl.textContent = '✓ Correct!';
feedbackEl.className = 'feedback-correct';
feedbackEl.style.color = 'var(--success)';
}
} else {
// Show feedback for incorrect answer
// Incorrect answer - show correct spelling but continue
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 feedback text if element exists
const feedbackEl = document.getElementById('wordFeedback');
if (feedbackEl) {
feedbackEl.textContent = `✗ Correct spelling: ${currentWord}`;
feedbackEl.className = 'feedback-incorrect';
feedbackEl.style.color = 'var(--error)';
}
}
// Always advance to next word
currentIndex++;
document.getElementById('inputSection').classList.add('hidden');
// Small delay before showing next word
setTimeout(() => showWord(), 1500);
}
// Add Enter key support for game input
@@ -42,7 +63,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
}
});[];
});
let currentSentence = '';
function showStudent() {
@@ -110,19 +132,30 @@ window.startGame = async function(customWords = null, isSpacedRepetition = false
console.log('Received words:', data);
let weekWords = [];
// If weeks format and activeWeek is set, use only that week's words
if (data.words && Array.isArray(data.words)) {
weekWords = data.words;
} else if (data.words && data.words.weeks && data.words.activeWeek) {
const active = data.words.activeWeek;
const found = data.words.weeks.find(w => w.date === active);
if (found) weekWords = found.words;
} else if (Array.isArray(data)) {
// If data is already an array
weekWords = data;
// Safely handle various response formats
try {
if (data && data.words && Array.isArray(data.words)) {
// Server returns {words: [array]}
weekWords = data.words;
} else if (data && data.words && typeof data.words === 'object' && data.words.weeks && data.words.activeWeek) {
// Server returns {words: {weeks: [], activeWeek: ""}}
const active = data.words.activeWeek;
const found = data.words.weeks.find(w => w.date === active);
if (found && Array.isArray(found.words)) {
weekWords = found.words;
}
} else if (Array.isArray(data)) {
// Server returns direct array
weekWords = data;
} else if (typeof data === 'object' && Array.isArray(Object.values(data)[0])) {
// Handle unexpected format as best we can
weekWords = Object.values(data)[0];
}
} catch (err) {
console.error("Error processing word list data:", err);
}
wordList = weekWords;
wordList = weekWords.length > 0 ? weekWords : ["default", "practice", "words"];
}
if (!wordList.length) {
@@ -156,325 +189,256 @@ function showWord() {
const wordBox = document.getElementById('wordBox');
const inputSection = document.getElementById('inputSection');
// Reset any feedback text
const feedbackEl = document.getElementById('wordFeedback');
if (feedbackEl) {
feedbackEl.textContent = '';
feedbackEl.className = '';
}
wordBox.textContent = currentWord;
inputSection.classList.add('hidden');
setTimeout(() => {
wordBox.textContent = '';
// Make sure input section is visible
inputSection.classList.remove('hidden');
document.getElementById('userInput').value = '';
document.getElementById('userInput').focus();
inputSection.style.display = 'block';
// Reset and focus the input field with enhanced visibility
const userInput = document.getElementById('userInput');
userInput.value = '';
// Use the class-based approach instead of inline styles
userInput.classList.add('visible-input');
// Focus with a slight delay to ensure UI is ready
setTimeout(() => {
userInput.focus();
// Try to insert a cursor by setting a selection range
try {
userInput.setSelectionRange(0, 0);
} catch (e) {
console.log('Could not set selection range:', e);
}
console.log('🔍 Input focused, ready for typing');
}, 50);
}, 2000);
}
// 📝 Check user’s answer and move to next
function submitAnswer() {
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');
showWord();
} else {
alert('Try again!');
}
}
// 📊 Show results summary
function showSummary() {
const summary = document.getElementById('summary');
let correct = 0;
let missed = [];
// Process results to calculate correct answers and build a report
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('');
// Create detailed results for spaced repetition processing
const detailedResults = results.map(({ word, attempt }) => {
return {
word,
correct: word.toLowerCase() === attempt.toLowerCase(),
attempt
};
});
// Dispatch event for spaced repetition system if in SR mode
if (isSpacedRepetitionMode) {
const event = new CustomEvent('gameResultsAvailable', {
detail: {
isSpacedRepetition: true,
results: detailedResults
}
});
document.dispatchEvent(event);
}
// Build and display summary
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>` : ''}
${isSpacedRepetitionMode ? `<button onclick='backToStudentDashboard()' class="btn-primary">Back to Dashboard</button>` : ''}
`;
summary.classList.remove('hidden');
// 🏆 Badge evaluation
const earnedBadges = [];
// Performance badges
if (correct === words.length) earnedBadges.push({
id: "perfect_round",
name: "Perfect Round",
description: "Got all words correct in a round",
icon: "🎯",
color: "#ffd700",
category: "achievement"
});
if (missed.length === 0) earnedBadges.push({
id: "no_misses",
name: "No Misses",
description: "Completed a round without any mistakes",
icon: "⭐",
color: "#ff9500",
category: "achievement"
});
// Speed badges
const averageTime = totalTime / words.length;
if (averageTime < 5 && words.length >= 5) earnedBadges.push({
id: "speed_demon",
name: "Speed Demon",
description: "Average time less than 5 seconds per word",
icon: "⚡",
color: "#00b4d8",
category: "speed"
});
if (totalTime < 30 && words.length >= 10) earnedBadges.push({
id: "quick_study",
name: "Quick Study",
description: "Complete 10+ words in under 30 seconds",
icon: "🚀",
color: "#8338ec",
category: "speed"
});
// Difficulty badges
const averageLength = words.reduce((sum, word) => sum + word.length, 0) / words.length;
if (averageLength > 8 && correct > words.length * 0.8) earnedBadges.push({
id: "word_wizard",
name: "Word Wizard",
description: "Master of complex vocabulary",
icon: "🧙‍♂️",
color: "#7209b7",
category: "mastery"
});
// Streak tracking (access from localStorage)
let streakCount = parseInt(localStorage.getItem('practice_streak') || '0');
if (correct / words.length >= 0.7) { // At least 70% correct to count for streak
streakCount++;
localStorage.setItem('practice_streak', streakCount);
} else {
streakCount = 0;
localStorage.setItem('practice_streak', '0');
}
// Streak badges
if (streakCount >= 3) earnedBadges.push({
id: "consistent_learner",
name: "Consistent Learner",
description: "3-day practice streak with 70%+ accuracy",
icon: "🔥",
color: "#ff006e",
category: "consistency"
});
if (streakCount >= 7) earnedBadges.push({
id: "weekly_warrior",
name: "Weekly Warrior",
description: "7-day practice streak with 70%+ accuracy",
icon: "⚔️",
color: "#3a0ca3",
category: "consistency"
});
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (user) {
// 📡 Save structured results
fetch('/saveResults', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: user.username,
result: {
score: correct,
completed: true,
answers: results.map(({ word, attempt }) => ({
word,
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 with improved visuals
const badgeSection = document.getElementById("badgeDisplay");
const badgeList = document.getElementById("badgeList");
badgeList.innerHTML = "";
// Add header for the badge showcase
const badgeHeader = document.createElement("div");
badgeHeader.className = "badge-showcase-header";
badgeHeader.innerHTML = `<span class="badge-count">${earnedBadges.length}</span> New Badge${earnedBadges.length > 1 ? 's' : ''} Earned!`;
badgeList.appendChild(badgeHeader);
// Create badge container
const badgeContainer = document.createElement("div");
badgeContainer.className = "badge-container";
earnedBadges.forEach(badge => {
const badgeCard = document.createElement("div");
badgeCard.className = "badge-card";
badgeCard.style.borderColor = badge.color || "#4a5568";
badgeCard.style.boxShadow = `0 4px 6px rgba(0,0,0,0.1), 0 0 10px ${badge.color}40`;
badgeCard.innerHTML = `
<div class="badge-icon" style="background-color: ${badge.color}40">
<span>${badge.icon || "🏅"}</span>
</div>
<div class="badge-info">
<h3>${badge.name}</h3>
<p>${badge.description}</p>
</div>
`;
// Add animation class after a small delay for staggered effect
setTimeout(() => {
badgeCard.classList.add("badge-card-animate");
}, 150 * badgeContainer.children.length);
badgeContainer.appendChild(badgeCard);
});
badgeList.appendChild(badgeContainer);
badgeSection.classList.remove("hidden");
} else {
document.getElementById("badgeDisplay")?.classList.add("hidden");
}
}
}
// 🔁 Retry only the missed words
function retryMissed(missedWords) {
words = missedWords;
current = 0;
results = [];
document.getElementById('summary')?.classList.add('hidden');
document.getElementById('badgeDisplay')?.classList.add('hidden');
showWord();
}
// New function to start typing practice
async function startTypingPractice() {
console.log("⌨️ Starting typing practice...");
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) {
console.error("No user logged in");
return;
}
try {
const res = await fetch(`/getWordList?username=${encodeURIComponent(user.username)}`);
const data = await res.json();
const words = data.words || [];
if (!words.length) {
alert("No words found for practice. Please contact your teacher.");
return;
}
// Hide game section, show typing section
document.getElementById('gameSection')?.classList.add('hidden');
document.getElementById('typingSection').classList.remove('hidden');
// Initialize typing practice
localStorage.setItem('typingWords', JSON.stringify(words));
showNextTypingSentence();
} catch (error) {
console.error('Error starting typing practice:', error);
alert('Error loading typing practice. Please try again.');
}
}
function createSentenceWithWord(word) {
const templates = [
`The word to spell is "${word}."`,
`Please spell the word "${word}."`,
`Type the word "${word}" carefully.`,
`Practice spelling "${word}" in this sentence.`
];
return templates[Math.floor(Math.random() * templates.length)];
}
function showNextTypingSentence() {
console.log('Showing next sentence...'); // Debug log
console.log('Game completed, showing summary');
document.getElementById('inputSection').classList.add('hidden');
if (!typingWords.length) {
document.getElementById('typingPrompt').textContent = '🎉 All done! Great job!';
document.getElementById('typingInput').disabled = true;
return;
// Calculate stats
let correctCount = 0;
const incorrectWords = [];
results.forEach(result => {
if (result.attempt.toLowerCase() === result.word.toLowerCase()) {
correctCount++;
} else {
incorrectWords.push(result);
}
});
const accuracy = Math.round((correctCount / results.length) * 100);
// Show summary
document.getElementById('summary').classList.remove('hidden');
document.getElementById('totalWords').textContent = results.length;
document.getElementById('correctWords').textContent = correctCount;
document.getElementById('accuracy').textContent = `${accuracy}%`;
// Show incorrect words
const incorrectList = document.getElementById('incorrectList');
incorrectList.innerHTML = '';
if (incorrectWords.length === 0) {
incorrectList.innerHTML = '<li>Perfect! No mistakes.</li>';
} else {
incorrectWords.forEach(item => {
incorrectList.innerHTML += `
<li>
<span class="word">${item.word}</span>
<span class="attempt">${item.attempt}</span>
</li>
`;
});
}
// Save results if not in spaced repetition mode
if (!isSpacedRepetitionMode) {
saveResults(results, accuracy);
} else {
console.log('In spaced repetition mode, not saving normal results');
// Update spaced repetition data
updateSpacedRepetitionResults(results);
}
const word = typingWords[0];
currentSentence = createSentenceWithWord(word);
console.log('Current sentence:', currentSentence); // Debug log
document.getElementById('typingPrompt').textContent = currentSentence;
document.getElementById('typingInput').value = '';
document.getElementById('typingInput').disabled = false;
document.getElementById('typingInput').focus();
document.getElementById('typingFeedback').textContent = '';
}
function submitTyping() {
const input = document.getElementById('typingInput').value.trim();
const feedback = document.getElementById('typingFeedback');
if (input.toLowerCase() === currentSentence.toLowerCase()) {
feedback.textContent = '✅ Perfect!';
feedback.style.color = 'green';
typingWords.shift(); // Remove completed word
setTimeout(showNextTypingSentence, 1500);
} else {
feedback.textContent = '❌ Try again. Type the exact sentence as shown.';
feedback.style.color = 'red';
async function saveResults(resultData, accuracy) {
try {
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) return;
// Transform the result data to match server's expected format
const answers = resultData.map(item => ({
word: item.word,
attempt: item.attempt,
correct: item.attempt.toLowerCase() === item.word.toLowerCase()
}));
const payload = {
username: user.username,
result: {
score: accuracy,
completed: true,
answers: answers,
timestamp: new Date().toISOString()
}
};
const response = await fetch('/saveResults', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
console.log('Results saved successfully');
checkForBadges(accuracy, resultData.length);
}
} catch (error) {
console.error('Error saving results:', error);
}
}
}
async function checkForBadges(accuracy, wordCount) {
try {
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) return;
// Check for earned badges
const response = await fetch(`/checkBadges?username=${user.username}&accuracy=${accuracy}&wordCount=${wordCount}`);
const data = await response.json();
if (data && data.newBadges && data.newBadges.length > 0) {
// Show badge notification
showBadgeNotification(data.newBadges);
}
} catch (error) {
console.error('Error checking badges:', error);
}
}
function showBadgeNotification(badges) {
const badgeDisplay = document.getElementById('badgeDisplay');
if (!badgeDisplay) {
console.error('Badge display element not found!');
return;
}
const badgeList = document.getElementById('badgeList');
if (!badgeList) {
console.error('Badge list element not found!');
return;
}
badgeList.innerHTML = '';
badges.forEach(badge => {
// Ensure badge has all required properties
const badgeIcon = badge.icon || '🏅';
const badgeName = badge.name || 'Achievement Unlocked!';
const description = badge.description || 'Great job on your spelling practice!';
// Log the badge data to ensure it's correct
console.log('Badge data:', { icon: badgeIcon, name: badgeName, description: description });
badgeList.innerHTML += `
<div class="badge-item">
<div class="badge-icon">${badgeIcon}</div>
<div class="badge-info">
<h4>${badgeName}</h4>
<p>${description}</p>
</div>
</div>
`;
});
console.log('Showing badge notification for', badges.length, 'badges');
badgeDisplay.classList.remove('hidden');
}
// Function to check if user has completed all words for today
async function checkDailyCompletion() {
try {
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) return;
const response = await fetch(`/checkDailyCompletion?username=${user.username}`);
const data = await response.json();
return data.completed;
} catch (error) {
console.error('Error checking daily completion:', error);
return false;
}
}
// Return to student panel
window.returnToStudentPanel = function() {
// Hide game and typing sections
document.getElementById('gameSection').style.display = 'none';
document.getElementById('typingSection').style.display = 'none';
document.getElementById('bibleSection').style.display = 'none';
// Show student panel
document.getElementById('studentPanel').style.display = 'block';
// Force panel to be visible
showStudent();
}
// Alias for returnToStudentPanel for backward compatibility
window.backToStudentDashboard = function() {
window.returnToStudentPanel();
}
// Handle spaced repetition results
async function updateSpacedRepetitionResults(resultData) {
try {
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) return;
// Process results for spaced repetition algorithm
const processedResults = resultData.map(result => ({
word: result.word,
isCorrect: result.attempt.toLowerCase() === result.word.toLowerCase(),
timestamp: new Date().toISOString()
}));
// Send to server
const response = await fetch('/updateSpacedRepetition', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: user.username,
results: processedResults
})
});
if (response.ok) {
console.log('Spaced repetition data updated successfully');
} else {
console.error('Failed to update spaced repetition data');
}
} catch (error) {
console.error('Error updating spaced repetition data:', error);
}
}
+358
View File
@@ -0,0 +1,358 @@
// 🌍 Game state
let words = [];
let currentWord = '';
let currentIndex = 0;
let results = [];
let typingWords = [];
let isSpacedRepetitionMode = false;
// 📝 Check user's answer and move to next word regardless of correctness
window.submitAnswer = function() {
const input = document.getElementById('userInput').value.trim();
// Record the attempt
results.push({
word: currentWord,
attempt: input
});
// Show feedback and move to next word regardless of correctness
if (input.toLowerCase() === currentWord.toLowerCase()) {
// Correct answer - add visual feedback
const inputEl = document.getElementById('userInput');
inputEl.style.borderColor = 'var(--success)';
inputEl.style.backgroundColor = '#f0fdf4';
// Add feedback text if element exists
const feedbackEl = document.getElementById('wordFeedback');
if (feedbackEl) {
feedbackEl.textContent = '✓ Correct!';
feedbackEl.className = 'feedback-correct';
feedbackEl.style.color = 'var(--success)';
}
} else {
// Incorrect answer - show correct spelling but continue
const inputEl = document.getElementById('userInput');
inputEl.style.borderColor = 'var(--error)';
inputEl.style.backgroundColor = '#fef2f2';
// Add feedback text if element exists
const feedbackEl = document.getElementById('wordFeedback');
if (feedbackEl) {
feedbackEl.textContent = `✗ Correct spelling: ${currentWord}`;
feedbackEl.className = 'feedback-incorrect';
feedbackEl.style.color = 'var(--error)';
}
}
// Always advance to next word
currentIndex++;
document.getElementById('inputSection').classList.add('hidden');
// Small delay before showing next word
setTimeout(() => showWord(), 1500);
}
// 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");
// Hide admin panel
document.getElementById('adminPanel')?.classList.add('hidden');
// Show student panel and its components
const studentPanel = document.getElementById('studentPanel');
studentPanel?.classList.remove('hidden');
// Get the buttons and explicitly set their properties
const gameBtn = document.getElementById('startGameBtn');
const typingBtn = document.getElementById('startTypingBtn');
if (gameBtn && typingBtn) {
console.log("Found buttons, making visible");
// Remove all possible hiding classes/styles
gameBtn.classList.remove('hidden');
typingBtn.classList.remove('hidden');
// Explicitly set display style
gameBtn.style.cssText = 'display: inline-block !important; visibility: visible !important;';
typingBtn.style.cssText = 'display: inline-block !important; visibility: visible !important;';
} else {
console.error("Could not find game buttons");
}
}
// 🚀 Begin game session
window.startGame = async function(customWords = null, isSpacedRepetition = false) {
console.log('🎮 Starting spelling game');
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) {
alert('Please log in first');
return;
}
try {
// Hide student dashboard and other sections
document.getElementById('studentPanel').style.display = 'none';
document.getElementById('typingSection').style.display = 'none';
document.getElementById('bibleSection').style.display = 'none';
// Show game section
document.getElementById('gameSection').classList.remove('hidden');
document.getElementById('gameSection').style.display = 'block';
document.getElementById('wordBox').textContent = 'Loading...';
document.getElementById('summary')?.classList.add('hidden');
document.getElementById('badgeDisplay')?.classList.add('hidden');
let wordList;
// Use custom words if provided (for spaced repetition)
if (customWords && Array.isArray(customWords) && customWords.length > 0) {
wordList = customWords;
isSpacedRepetitionMode = isSpacedRepetition;
console.log('Using custom word list for spaced repetition:', wordList);
} else {
// Fetch words from user's word list
const response = await fetch(`/getWordList?username=${user.username}`);
const data = await response.json();
console.log('Received words:', data);
let weekWords = [];
// Safely handle various response formats
try {
if (data && data.words && Array.isArray(data.words)) {
// Server returns {words: [array]}
weekWords = data.words;
} else if (data && data.words && typeof data.words === 'object' && data.words.weeks && data.words.activeWeek) {
// Server returns {words: {weeks: [], activeWeek: ""}}
const active = data.words.activeWeek;
const found = data.words.weeks.find(w => w.date === active);
if (found && Array.isArray(found.words)) {
weekWords = found.words;
}
} else if (Array.isArray(data)) {
// Server returns direct array
weekWords = data;
} else if (typeof data === 'object' && Array.isArray(Object.values(data)[0])) {
// Handle unexpected format as best we can
weekWords = Object.values(data)[0];
}
} catch (err) {
console.error("Error processing word list data:", err);
}
wordList = weekWords.length > 0 ? weekWords : ["default", "practice", "words"];
}
if (!wordList.length) {
alert('No words found for practice');
return;
}
// Start game
words = [...wordList];
typingWords = [...wordList]; // Initialize typing words too
currentIndex = 0;
results = []; // Reset results array
showWord();
} catch (error) {
console.error('Error:', error);
alert('Failed to load words');
}
}
function showWord() {
if (currentIndex >= words.length) {
showSummary();
return;
}
currentWord = words[currentIndex];
console.log('Current word:', currentWord);
// Show word briefly then hide
const wordBox = document.getElementById('wordBox');
const inputSection = document.getElementById('inputSection');
// Reset any feedback text
const feedbackEl = document.getElementById('wordFeedback');
if (feedbackEl) {
feedbackEl.textContent = '';
feedbackEl.className = '';
}
wordBox.textContent = currentWord;
inputSection.classList.add('hidden');
setTimeout(() => {
wordBox.textContent = '';
inputSection.classList.remove('hidden');
document.getElementById('userInput').value = '';
document.getElementById('userInput').focus();
}, 2000);
}
function showSummary() {
console.log('Game completed, showing summary');
document.getElementById('inputSection').classList.add('hidden');
// Calculate stats
let correctCount = 0;
const incorrectWords = [];
results.forEach(result => {
if (result.attempt.toLowerCase() === result.word.toLowerCase()) {
correctCount++;
} else {
incorrectWords.push(result);
}
});
const accuracy = Math.round((correctCount / results.length) * 100);
// Show summary
document.getElementById('summary').classList.remove('hidden');
document.getElementById('totalWords').textContent = results.length;
document.getElementById('correctWords').textContent = correctCount;
document.getElementById('accuracy').textContent = `${accuracy}%`;
// Show incorrect words
const incorrectList = document.getElementById('incorrectList');
incorrectList.innerHTML = '';
if (incorrectWords.length === 0) {
incorrectList.innerHTML = '<li>Perfect! No mistakes.</li>';
} else {
incorrectWords.forEach(item => {
incorrectList.innerHTML += `
<li>
<span class="word">${item.word}</span>
<span class="attempt">${item.attempt}</span>
</li>
`;
});
}
// Save results if not in spaced repetition mode
if (!isSpacedRepetitionMode) {
saveResults(results, accuracy);
} else {
console.log('In spaced repetition mode, not saving normal results');
// Update spaced repetition data
updateSpacedRepetitionResults(results);
}
}
async function saveResults(resultData, accuracy) {
try {
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) return;
const payload = {
username: user.username,
results: resultData,
accuracy,
timestamp: new Date().toISOString()
};
const response = await fetch('/saveResults', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
console.log('Results saved successfully');
checkForBadges(accuracy, resultData.length);
}
} catch (error) {
console.error('Error saving results:', error);
}
}
async function checkForBadges(accuracy, wordCount) {
try {
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) return;
// Check for earned badges
const response = await fetch(`/checkBadges?username=${user.username}&accuracy=${accuracy}&wordCount=${wordCount}`);
const data = await response.json();
if (data && data.newBadges && data.newBadges.length > 0) {
// Show badge notification
showBadgeNotification(data.newBadges);
}
} catch (error) {
console.error('Error checking badges:', error);
}
}
function showBadgeNotification(badges) {
const badgeDisplay = document.getElementById('badgeDisplay');
if (!badgeDisplay) return;
const badgeList = document.getElementById('badgeList');
badgeList.innerHTML = '';
badges.forEach(badge => {
badgeList.innerHTML += `
<div class="badge-item">
<div class="badge-icon">${badge.icon}</div>
<div class="badge-info">
<h4>${badge.name}</h4>
<p>${badge.description}</p>
</div>
</div>
`;
});
badgeDisplay.classList.remove('hidden');
}
// Function to check if user has completed all words for today
async function checkDailyCompletion() {
try {
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) return;
const response = await fetch(`/checkDailyCompletion?username=${user.username}`);
const data = await response.json();
return data.completed;
} catch (error) {
console.error('Error checking daily completion:', error);
return false;
}
}
// Return to student panel
window.returnToStudentPanel = function() {
// Hide game and typing sections
document.getElementById('gameSection').style.display = 'none';
document.getElementById('typingSection').style.display = 'none';
document.getElementById('bibleSection').style.display = 'none';
// Show student panel
document.getElementById('studentPanel').style.display = 'block';
// Force panel to be visible
showStudent();
}
+141
View File
@@ -0,0 +1,141 @@
/**
* Enhanced unified initialization script for the Spelling Practice app.
* This ensures all components are properly loaded and synchronized.
*/
// Make sure this runs after everything else
document.addEventListener('DOMContentLoaded', () => {
// Run initialization after a small delay to ensure all scripts are loaded
setTimeout(initializeApp, 500);
});
// Keep track of initialization status
window.appInitialized = false;
// Main initialization function
async function initializeApp() {
// Prevent duplicate initialization
if (window.appInitialized) {
console.log('⏭️ App already initialized, skipping');
return;
}
console.log('🚀 Initializing application...');
// Display initialization status
const statusElement = document.createElement('div');
statusElement.id = 'initStatus';
statusElement.style.cssText = 'position:fixed; bottom:20px; left:20px; background:rgba(0,0,0,0.7); color:white; padding:8px 16px; border-radius:4px; z-index:9999; font-size:14px;';
statusElement.textContent = 'Initializing app...';
document.body.appendChild(statusElement);
const updateStatus = (message, isError = false) => {
if (statusElement) {
statusElement.textContent = message;
if (isError) {
statusElement.style.background = 'rgba(220,53,69,0.9)';
}
}
console.log(message);
};
// Step 1: Check server connectivity
try {
updateStatus('Checking server connection...');
const serverCheck = await fetch('/getUsers');
if (serverCheck.ok) {
updateStatus('Server connection: OK');
} else {
updateStatus(`Server error: ${serverCheck.status}: ${serverCheck.statusText}`, true);
setTimeout(() => statusElement.remove(), 5000);
return;
}
} catch (error) {
updateStatus(`Cannot connect to server: ${error.message}`, true);
setTimeout(() => statusElement.remove(), 5000);
return;
}
// Step 2: Load user data (if admin)
const currentUser = getCurrentUser();
if (currentUser && currentUser.role === 'admin') {
try {
updateStatus('Loading user data...');
// Load users using admin.js loadUsers function if available
if (typeof window.loadUsers === 'function') {
await window.loadUsers();
} else {
// Fallback if loadUsers isn't available
const response = await fetch('/getUsers');
if (!response.ok) throw new Error(`Server returned ${response.status}`);
window.adminUsers = await response.json();
}
updateStatus(`User data loaded: ${window.adminUsers?.length || 0} users`);
} catch (error) {
updateStatus(`Failed to load users: ${error.message}`, true);
}
}
// Step 3: Sync data between modules
updateStatus('Synchronizing modules...');
try {
// If adminUsers exists, make sure it's populated in all dropdowns
if (window.adminUsers && window.adminUsers.length > 0) {
// Use displayUserDropdown if it exists
if (typeof window.displayUserDropdown === 'function') {
window.displayUserDropdown();
}
// Also populate analytics dropdown if available
if (typeof populateUserDropdown === 'function') {
await populateUserDropdown();
}
}
} catch (error) {
console.error('Module sync error:', error);
}
// Step 4: Initialize UI components
updateStatus('Setting up UI components...');
// Set up tab navigation
const tabButtons = document.querySelectorAll('#adminTabs button');
tabButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
const tabId = button.getAttribute('data-tab');
if (tabId && typeof window.switchTab === 'function') {
window.switchTab(tabId);
}
});
});
// Check for chart rendering issues
if (typeof window.renderAnalyticsDashboard === 'function' &&
typeof window.safeExec === 'function') {
window.renderAnalyticsDashboard = window.safeExec(
window.renderAnalyticsDashboard,
'Error rendering charts. Please try again.'
);
}
// Mark initialization as complete
window.appInitialized = true;
updateStatus('Initialization complete ✓');
// Remove status after a few seconds
setTimeout(() => {
if (statusElement && statusElement.parentNode) {
statusElement.remove();
}
}, 3000);
}
// Helper function to get current user
function getCurrentUser() {
const savedUser = localStorage.getItem('loggedInUser');
return savedUser ? JSON.parse(savedUser) : null;
}
+78
View File
@@ -0,0 +1,78 @@
/**
* Debug script for input visibility issues
*/
document.addEventListener('DOMContentLoaded', function() {
console.log('🔍 Input visibility fix script loaded');
// No debug button in production
// Just silently apply fixes
// Setup input monitoring
const userInput = document.getElementById('userInput');
if (userInput) {
userInput.addEventListener('input', function(e) {
console.log('📝 Input value:', e.target.value);
});
}
// Apply the fix once at startup
setTimeout(fixInputVisibility, 1000);
});
// Function to fix input visibility - simplified to avoid loops
function fixInputVisibility() {
const userInput = document.getElementById('userInput');
if (!userInput) {
console.log('❌ User input element not found');
return;
}
console.log('🔧 Fixing input visibility');
// Create a style element for stronger CSS
const styleEl = document.createElement('style');
styleEl.textContent = `
#userInput {
color: black !important;
background-color: white !important;
border: 2px solid #333 !important;
font-size: 18px !important;
padding: 12px !important;
visibility: visible !important;
opacity: 1 !important;
text-shadow: none !important;
-webkit-text-fill-color: black !important;
display: inline-block !important;
}
`;
document.head.appendChild(styleEl);
// Create a test notification
const notification = document.createElement('div');
notification.textContent = 'Input visibility fixed';
notification.style.position = 'fixed';
notification.style.top = '20px';
notification.style.left = '50%';
notification.style.transform = 'translateX(-50%)';
notification.style.backgroundColor = '#4CAF50';
notification.style.color = 'white';
notification.style.padding = '10px 20px';
notification.style.borderRadius = '4px';
notification.style.zIndex = '10000';
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
// Add event listener to log key events (only once)
if (!userInput.hasKeyLogger) {
userInput.addEventListener('keydown', function(e) {
console.log(`Key pressed: ${e.key}`);
});
userInput.hasKeyLogger = true;
}
console.log('✅ Input visibility fix applied');
}
+30 -2
View File
@@ -1,6 +1,19 @@
document.addEventListener("DOMContentLoaded", async () => {
console.log('🚀 Application starting...');
// Check if server is available
try {
console.log('🔄 Testing server connection...');
const serverTest = await fetch('/getUsers');
if (serverTest.ok) {
console.log('✅ Server connection successful');
} else {
console.error(`❌ Server responded with error: ${serverTest.status}`);
}
} catch (e) {
console.error('❌ Server connection failed:', e.message);
}
// Ensure all panels are hidden initially
document.getElementById('adminPanel')?.classList.add('hidden');
document.getElementById('studentPanel')?.classList.add('hidden');
@@ -13,6 +26,12 @@ document.addEventListener("DOMContentLoaded", async () => {
// Restore saved theme
applySavedTheme();
// Global error handler for fetch operations
window.handleApiError = function(error, operation) {
console.error(`❌ API Error during ${operation}:`, error);
alert(`Network error during ${operation}. Please check your connection and try again.`);
};
// Restore session if user was logged in
const savedUser = localStorage.getItem("loggedInUser");
if (savedUser) {
@@ -23,11 +42,20 @@ document.addEventListener("DOMContentLoaded", async () => {
// Load users for admin functionality only if user is admin
if (user.role === 'admin') {
try {
console.log('🔄 Loading admin data...');
await loadUsers();
displayUserDropdown();
populateWordUserDropdown();
displayUserLists();
// Initialize all user dropdowns
if (typeof populateWordUserDropdown === 'function') {
populateWordUserDropdown();
}
if (typeof window.populateUserDropdown === 'function') {
window.populateUserDropdown();
}
} catch (e) {
console.warn('Could not load admin data:', e);
console.warn('⚠️ Could not load admin data:', e);
}
}
+63
View File
@@ -0,0 +1,63 @@
// Helper function to render session words in a consistent way
function renderSessionWords(session) {
// Handle different data formats
if (session.answers && Array.isArray(session.answers)) {
return session.answers.map(a =>
`<li>${a.word} - ${a.correct ? '✅ Correct' : '❌ Incorrect'}</li>`
).join('');
}
else if (session.words && Array.isArray(session.words)) {
return session.words.map(w =>
`<li>${w.word} - ${w.correct ? '✅ Correct' : '❌ Incorrect'}</li>`
).join('');
}
return '<li>No word details available</li>';
}
// Update the export function to handle different data formats
window.exportSessionReport = function(username, session) {
if (!session) return;
// Support both timestamp and date fields
const sessionDate = session.timestamp || session.date || Date.now();
const date = new Date(sessionDate).toLocaleDateString();
const score = session.score || session.correct || 0;
// Handle both answers array and words array formats
let total = 0;
let answersArray = [];
if (session.answers && Array.isArray(session.answers)) {
total = session.answers.length;
answersArray = session.answers;
} else if (session.words && Array.isArray(session.words)) {
total = session.words.length;
answersArray = session.words;
} else if (session.total) {
total = session.total;
}
const percent = total > 0 ? Math.round((score / total) * 100) : 0;
// Prepare report
const correctWords = answersArray.filter(a => a.correct).map(a => a.word).join(', ');
const incorrectWords = answersArray.filter(a => !a.correct).map(a => a.word).join(', ');
const reportContent = `
Session Report for ${username}
Date: ${date}
Score: ${score}/${total} (${percent}%)
Correct Words: ${correctWords || 'None'}
Incorrect Words: ${incorrectWords || 'None'}
`;
// Create download link
const blob = new Blob([reportContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${username}-session-${date.replace(/\//g, '-')}.txt`;
link.click();
URL.revokeObjectURL(url);
};
+41 -10
View File
@@ -21,9 +21,25 @@ window.loadUserSessions = async function(username) {
}
userSessions.forEach((session, index) => {
const date = new Date(session.timestamp || Date.now()).toLocaleString();
const score = session.score || 0;
const total = (session.answers && session.answers.length) || 0;
// Support both timestamp and date fields for backward compatibility
const sessionDate = session.timestamp || session.date || Date.now();
const date = new Date(sessionDate).toLocaleString();
const score = session.score || session.correct || 0;
// Handle both answers array and words array formats
let total = 0;
let answersArray = [];
if (session.answers && Array.isArray(session.answers)) {
total = session.answers.length;
answersArray = session.answers;
} else if (session.words && Array.isArray(session.words)) {
total = session.words.length;
answersArray = session.words;
} else if (session.total) {
total = session.total;
}
const percent = total > 0 ? Math.round((score / total) * 100) : 0;
const li = document.createElement('li');
@@ -46,9 +62,7 @@ window.loadUserSessions = async function(username) {
<div class="sessionDetails hidden" id="session${index}">
<h4 style="margin-top: 1rem;">Words:</h4>
<ul>
${session.answers ? session.answers.map(a =>
`<li>${a.word} - ${a.correct ? '✅ Correct' : '❌ Incorrect'}</li>`
).join('') : ''}
${renderSessionWords(session)}
</ul>
</div>
`;
@@ -88,13 +102,30 @@ window.exportAllSessionsCSV = function() {
let csvContent = 'Date,Score,Total Words,Percentage,Words Correct,Words Incorrect\n';
sessions.forEach(session => {
const date = new Date(session.timestamp || Date.now()).toLocaleDateString();
const score = session.score || 0;
const total = (session.answers && session.answers.length) || 0;
// Support both timestamp and date fields
const sessionDate = session.timestamp || session.date || Date.now();
const date = new Date(sessionDate).toLocaleDateString();
const score = session.score || session.correct || 0;
// Handle both answers array and words array formats
let total = 0;
let answersArray = [];
if (session.answers && Array.isArray(session.answers)) {
total = session.answers.length;
answersArray = session.answers;
} else if (session.words && Array.isArray(session.words)) {
total = session.words.length;
answersArray = session.words;
} else if (session.total) {
total = session.total;
}
const percent = total > 0 ? Math.round((score / total) * 100) : 0;
// Count correct and incorrect words
const correctWords = session.answers ? session.answers.filter(a => a.correct).length : 0;
const correctWords = answersArray.filter(a => a.correct).length;
const incorrectWords = total - correctWords;
csvContent += `${date},${score},${total},${percent}%,${correctWords},${incorrectWords}\n`;
+9 -9
View File
@@ -6,9 +6,9 @@
* Words are assigned difficulty levels and review intervals are adjusted accordingly
*/
// Global variables
// Module variables
let spacedRepetitionData = null;
let currentUser = null;
let srCurrentUser = null; // Renamed to avoid conflicts
let currentWords = [];
let reviewQueue = [];
@@ -16,8 +16,8 @@ let reviewQueue = [];
document.addEventListener('DOMContentLoaded', function() {
// Event listener will be triggered when a user logs in
document.addEventListener('userLoggedIn', function(e) {
currentUser = e.detail.username;
initializeSpacedRepetition(currentUser);
srCurrentUser = e.detail.username;
initializeSpacedRepetition(srCurrentUser);
});
});
@@ -234,7 +234,7 @@ async function startSpacedReview() {
// Process results from a spaced repetition session
function processSpacedRepetitionResults(results) {
if (!currentUser || !spacedRepetitionData || !results || results.length === 0) return;
if (!srCurrentUser || !spacedRepetitionData || !results || results.length === 0) return;
const now = new Date();
const today = now.toISOString().split('T')[0];
@@ -304,7 +304,7 @@ function processSpacedRepetitionResults(results) {
// Save the updated spaced repetition data to the server
async function saveSpacedRepetitionData() {
if (!currentUser || !spacedRepetitionData) return;
if (!srCurrentUser || !spacedRepetitionData) return;
try {
const response = await fetch('/updateSpacedRepetitionData', {
@@ -313,7 +313,7 @@ async function saveSpacedRepetitionData() {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: currentUser,
username: srCurrentUser,
data: spacedRepetitionData
})
});
@@ -331,11 +331,11 @@ async function saveSpacedRepetitionData() {
// Import new words to the spaced repetition system from a word list
async function importWordsToSR() {
if (!currentUser || !spacedRepetitionData) return;
if (!srCurrentUser || !spacedRepetitionData) return;
try {
// Get user's word lists
const response = await fetch(`/getWordlistsForUser?username=${encodeURIComponent(currentUser)}`);
const response = await fetch(`/getWordlistsForUser?username=${encodeURIComponent(srCurrentUser)}`);
if (!response.ok) {
throw new Error(`Failed to fetch wordlists: ${response.status}`);
}
+23 -2
View File
@@ -28,8 +28,29 @@ window.startTypingPractice = async function() {
const data = await response.json();
console.log('Words received:', data);
if (data.words?.length) {
typingState.words = [...data.words];
let wordList = [];
// Handle various potential response formats
if (data && data.words && Array.isArray(data.words)) {
// Server returns {words: [array]}
wordList = data.words;
} else if (data && data.words && typeof data.words === 'object' && data.words.weeks && data.words.activeWeek) {
// Server returns {words: {weeks: [], activeWeek: ""}}
const active = data.words.activeWeek;
const found = data.words.weeks.find(w => w.date === active);
if (found && Array.isArray(found.words)) {
wordList = found.words;
}
} else if (Array.isArray(data)) {
// Server returns direct array
wordList = data;
} else if (typeof data === 'object' && Array.isArray(Object.values(data)[0])) {
// Handle unexpected format as best we can
wordList = Object.values(data)[0];
}
if (wordList.length > 0) {
typingState.words = [...wordList];
showNextWord();
} else {
typingPrompt.textContent = 'No words available';
+55 -6
View File
@@ -92,11 +92,18 @@ function showAdmin() {
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);
});
if (typeof window.loadUsers === 'function') {
window.loadUsers().then(() => {
console.log("✅ Users loaded from server");
if (typeof window.displayUserDropdown === 'function') {
window.displayUserDropdown();
}
}).catch(e => {
console.warn("Could not load users from server:", e);
});
} else {
console.warn("loadUsers function not available");
}
// Set default tab to tabWords and enable tab buttons
document.querySelectorAll('#adminTabs button').forEach(btn => btn.disabled = false);
@@ -109,4 +116,46 @@ function showAdmin() {
} else {
console.error("Admin panel not found");
}
}
}
// Tab switching functionality
window.switchTab = function(tabId) {
console.log(`🔄 Switching to tab: ${tabId}`);
// Hide all tabs
document.querySelectorAll('.adminTab').forEach(tab => {
tab.classList.add('hidden');
});
// Remove active class from all tab buttons
document.querySelectorAll('#adminTabs button').forEach(button => {
button.classList.remove('active');
});
// Show the selected tab
const selectedTab = document.getElementById(tabId);
if (selectedTab) {
selectedTab.classList.remove('hidden');
// Highlight the active tab button
const activeButton = document.querySelector(`#adminTabs button[data-tab="${tabId}"]`);
if (activeButton) {
activeButton.classList.add('active');
}
// Special handling for each tab type
if (tabId === 'tabAnalytics' && typeof window.analyticsRefresh === 'function') {
console.log('📊 Loading analytics data...');
window.analyticsRefresh().catch(e => {
console.error('Error loading analytics:', e);
});
} else if (tabId === 'tabResults' && typeof window.loadStudentResults === 'function') {
console.log('📋 Loading results data...');
window.loadStudentResults().catch(e => {
console.error('Error loading results:', e);
});
}
} else {
console.error(`Tab '${tabId}' not found in the DOM`);
}
};
+172
View File
@@ -0,0 +1,172 @@
/**
* User Dropdown Health Check
* This script diagnoses and attempts to fix issues with user dropdown population
*/
document.addEventListener('DOMContentLoaded', function() {
console.log('🩺 Running user dropdown health check...');
// Create a debug panel
const debugPanel = document.createElement('div');
debugPanel.id = 'userDropdownDebug';
debugPanel.style.cssText = 'position:fixed; bottom:10px; right:10px; background:#f5f5f5; border:1px solid #ddd; padding:10px; max-width:350px; max-height:400px; overflow:auto; font-size:12px; z-index:9999; display:none';
debugPanel.innerHTML = `
<h3 style="margin-top:0">User Dropdown Debug</h3>
<div id="debugContent"></div>
<hr>
<button id="forceUserRefresh" class="btn btn-small">Force Refresh Users</button>
<button id="hideDebugPanel" class="btn btn-small btn-error">Close</button>
`;
document.body.appendChild(debugPanel);
// Add key command to show debug panel (Ctrl+Shift+U)
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.shiftKey && e.key === 'U') {
debugPanel.style.display = debugPanel.style.display === 'none' ? 'block' : 'none';
if (debugPanel.style.display === 'block') {
runHealthCheck();
}
}
});
// Add button event listeners
document.getElementById('hideDebugPanel').addEventListener('click', function() {
debugPanel.style.display = 'none';
});
document.getElementById('forceUserRefresh').addEventListener('click', function() {
forceSyncUserData();
});
// Run an initial check
setTimeout(function() {
validateUserDropdowns(true);
}, 2000);
// Run health check and output to debug panel
function runHealthCheck() {
const debugContent = document.getElementById('debugContent');
debugContent.innerHTML = '<p>Running diagnostics...</p>';
// Check window.adminUsers
let report = `<h4>Global User Data</h4>`;
if (typeof window.adminUsers !== 'undefined') {
report += `<p>✅ window.adminUsers exists with ${window.adminUsers.length} users</p>`;
report += `<details><summary>Users list</summary><pre>${JSON.stringify(window.adminUsers.map(u => u.username), null, 2)}</pre></details>`;
} else {
report += `<p>❌ window.adminUsers is undefined!</p>`;
}
// Check all user dropdowns
report += `<h4>Dropdown Population</h4>`;
const dropdowns = [
{id: 'userDropdown', label: 'Main User Dropdown'},
{id: 'wordUserSelect', label: 'Words Tab User Dropdown'},
{id: 'passwordUsername', label: 'Password Tab User Dropdown'},
{id: 'analyticsUserSelect', label: 'Analytics User Dropdown'},
{id: 'reportUser', label: 'Reports User Dropdown'}
];
dropdowns.forEach(dropdown => {
const element = document.getElementById(dropdown.id);
if (element) {
const optionCount = element.options.length;
const status = optionCount > 1 ? '✅' : '❌';
report += `<p>${status} ${dropdown.label}: ${optionCount - 1} users</p>`;
} else {
report += `<p>❓ ${dropdown.label}: Not found in DOM</p>`;
}
});
// Check server connection
report += `<h4>Server Health</h4>`;
report += `<p>Testing connection to /getUsers endpoint...</p>`;
fetch('/getUsers')
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error(`Server returned ${response.status}`);
})
.then(data => {
report += `<p>✅ Server returned ${data.length} users</p>`;
debugContent.innerHTML = report;
})
.catch(error => {
report += `<p>❌ Error fetching users: ${error.message}</p>`;
debugContent.innerHTML = report;
});
}
// Validate all user dropdowns
function validateUserDropdowns(autoFix = false) {
console.log('🔍 Validating user dropdowns...');
// Get all user dropdown elements
const dropdowns = [
document.getElementById('userDropdown'),
document.getElementById('wordUserSelect'),
document.getElementById('passwordUsername'),
document.getElementById('analyticsUserSelect'),
document.getElementById('reportUser')
].filter(Boolean);
// Check if any dropdown is empty or has only the default option
const needsRepopulation = dropdowns.some(dropdown => dropdown.options.length <= 1);
if (needsRepopulation && autoFix) {
console.log('🔧 Found empty dropdowns, attempting to fix...');
forceSyncUserData();
}
return !needsRepopulation;
}
// Force refresh of all user data
async function forceSyncUserData() {
console.log('🔄 Forcing synchronization of user data...');
try {
// Fetch fresh user data from server
const response = await fetch('/getUsers');
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const users = await response.json();
// Update global user data
if (typeof window.adminUsers === 'undefined') {
window.adminUsers = [];
}
window.adminUsers = users;
console.log(`✅ Updated global users list with ${users.length} users`);
// Update all dropdowns
if (typeof window.displayUserDropdown === 'function') {
window.displayUserDropdown();
} else {
console.warn('⚠️ window.displayUserDropdown function not found');
}
// Also call analytics dropdown population if it exists
if (typeof populateUserDropdown === 'function') {
populateUserDropdown();
}
// Run health check again
if (document.getElementById('debugContent')) {
runHealthCheck();
}
return true;
} catch (error) {
console.error('❌ Error syncing user data:', error);
return false;
}
}
});
+128
View File
@@ -85,6 +85,18 @@ input, textarea, select {
transition: var(--transition);
}
/* Special styling for game input */
#userInput {
font-size: 1.2rem;
max-width: 400px;
text-align: center;
background-color: white !important;
color: var(--text-primary) !important;
font-weight: 500;
letter-spacing: 0.5px;
caret-color: var(--primary);
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--primary);
@@ -458,6 +470,25 @@ button:disabled {
border-radius: var(--radius);
color: var(--primary);
box-shadow: var(--shadow-lg);
}
/* Word feedback styles */
#wordFeedback {
font-size: 1.2rem;
padding: 0.5rem;
margin-top: 1rem;
border-radius: var(--radius);
transition: var(--transition);
}
.feedback-correct {
color: var(--success);
background-color: rgba(16, 185, 129, 0.1);
}
.feedback-incorrect {
color: var(--error);
background-color: rgba(239, 68, 68, 0.1);
min-height: 120px;
display: flex;
align-items: center;
@@ -473,6 +504,77 @@ button:disabled {
box-shadow: var(--shadow);
}
/* Game Summary Styling */
#summary {
margin-top: 2rem;
padding: 2rem;
text-align: center;
}
.summary-stats {
display: flex;
justify-content: space-around;
margin: 2rem 0;
flex-wrap: wrap;
}
.stat-item {
padding: 1rem;
margin: 0.5rem;
background: var(--bg-secondary);
border-radius: var(--radius);
min-width: 150px;
}
.stat-item label {
display: block;
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
.stat-item span {
font-size: 2rem;
font-weight: 700;
color: var(--primary);
}
.incorrect-list {
list-style: none;
padding: 0;
margin: 2rem 0;
max-height: 200px;
overflow-y: auto;
}
.incorrect-list li {
display: flex;
justify-content: center;
align-items: center;
padding: 0.75rem;
margin: 0.5rem 0;
background: var(--bg-secondary);
border-radius: var(--radius);
gap: 1rem;
}
.incorrect-list .word {
font-weight: 600;
color: var(--success);
}
.incorrect-list .attempt {
color: var(--error);
text-decoration: line-through;
}
.summary-actions {
margin-top: 2rem;
display: flex;
justify-content: center;
gap: 1rem;
}
/* Student controls specific styling */
.student-controls button {
min-height: 60px !important;
@@ -560,3 +662,29 @@ li {
padding: 2rem 1rem;
}
}
/* Enhanced Input Visibility Styles */
.visible-input {
color: black !important;
background-color: white !important;
border: 2px solid #333 !important;
font-size: 18px !important;
padding: 12px !important;
visibility: visible !important;
opacity: 1 !important;
text-shadow: none !important;
-webkit-text-fill-color: black !important;
display: inline-block !important;
width: 100% !important;
max-width: 400px !important;
box-shadow: 0 0 10px rgba(0,0,0,0.2) !important;
}
/* Hide debug tools */
#debugTools,
div[id*="debug-"],
div[class*="debug-"],
button[id*="debug"],
[data-debug="true"] {
display: none !important;
}
+179
View File
@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Save Results API</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
.result {
margin-top: 20px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 3px;
background-color: #f9f9f9;
white-space: pre-wrap;
}
button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
}
button:hover {
background-color: #45a049;
}
.status {
font-weight: bold;
margin-top: 10px;
}
.success {
color: green;
}
.error {
color: red;
}
</style>
</head>
<body>
<div class="container">
<h1>Test Save Results API</h1>
<h2>1. Test Save Results Endpoint</h2>
<p>This will send a test session for 'testuser' to the /saveResults endpoint</p>
<button id="saveButton">Save Test Session</button>
<div id="saveStatus" class="status"></div>
<div id="saveResult" class="result"></div>
<h2>2. Test Get Results Endpoint</h2>
<p>This will retrieve all sessions from the /getResults endpoint</p>
<button id="getButton">Get All Sessions</button>
<div id="getStatus" class="status"></div>
<div id="getResult" class="result"></div>
</div>
<script>
// Test data for saving a session
const testSession = {
username: 'testuser',
result: {
score: 90,
completed: true,
answers: [
{ word: 'apple', correct: true },
{ word: 'banana', correct: true },
{ word: 'cherry', correct: true },
{ word: 'difficult', correct: false },
{ word: 'elephant', correct: true }
]
}
};
// Function to format JSON with syntax highlighting
function formatJSON(json) {
if (typeof json === 'string') {
try {
json = JSON.parse(json);
} catch(e) {
return json;
}
}
return JSON.stringify(json, null, 2);
}
// Test the save endpoint
document.getElementById('saveButton').addEventListener('click', async () => {
const saveStatus = document.getElementById('saveStatus');
const saveResult = document.getElementById('saveResult');
saveStatus.textContent = 'Sending request...';
saveStatus.className = 'status';
try {
const response = await fetch('/saveResults', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(testSession)
});
const data = await response.text();
if (response.ok) {
saveStatus.textContent = '✅ Success! Status: ' + response.status;
saveStatus.className = 'status success';
} else {
saveStatus.textContent = '❌ Failed! Status: ' + response.status;
saveStatus.className = 'status error';
}
saveResult.textContent = data;
} catch (error) {
saveStatus.textContent = '❌ Error: ' + error.message;
saveStatus.className = 'status error';
saveResult.textContent = error.toString();
}
});
// Test the get endpoint
document.getElementById('getButton').addEventListener('click', async () => {
const getStatus = document.getElementById('getStatus');
const getResult = document.getElementById('getResult');
getStatus.textContent = 'Fetching data...';
getStatus.className = 'status';
try {
const response = await fetch('/getResults');
const data = await response.json();
if (response.ok) {
// Count sessions for each user
const sessionCounts = {};
Object.entries(data).forEach(([user, sessions]) => {
sessionCounts[user] = sessions.length;
});
getStatus.textContent = `✅ Success! Found data for ${Object.keys(data).length} users`;
getStatus.className = 'status success';
const testUserSessions = data.testuser || [];
// Display summary first, then full data
let summary = '// Summary:\n';
summary += `Users: ${Object.keys(data).join(', ')}\n`;
summary += `Sessions per user: ${JSON.stringify(sessionCounts, null, 2)}\n\n`;
if (testUserSessions.length > 0) {
summary += `// Latest testuser session:\n${formatJSON(testUserSessions[testUserSessions.length-1])}\n\n`;
}
summary += '// Full response data:\n';
getResult.textContent = summary + formatJSON(data);
} else {
getStatus.textContent = '❌ Failed! Status: ' + response.status;
getStatus.className = 'status error';
getResult.textContent = formatJSON(data);
}
} catch (error) {
getStatus.textContent = '❌ Error: ' + error.message;
getStatus.className = 'status error';
getResult.textContent = error.toString();
}
});
</script>
</body>
</html>