Fix v1.1.0: Analytics, Sessions, and Error Handling Improvements
This commit is contained in:
1 parent
7d8470df84
commit
545cf4c8dc
200 files changed
+91013
-18521
No files matched your search
@@ -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>
|
||||
@@ -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%);
|
||||
|
||||
Whitespace-only changes.
+45
-5
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -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);
|
||||
});
|
||||
})();
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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`;
|
||||
|
||||
@@ -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
@@ -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
@@ -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`);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user